Drop Glance Client
* Completely drop the legacy Glance client tool * bin/glance is gone * glance/client.py is gone * Drop relevant tests Implements bp separate-client Change-Id: Ifcb0bd9bb537e0243aeb5daf466f46868d522986
This commit is contained in:
parent
31338e4ac7
commit
76c3620c7e
1087
bin/glance
1087
bin/glance
File diff suppressed because it is too large
Load Diff
451
glance/client.py
451
glance/client.py
@ -1,451 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack, LLC
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Client classes for callers of a Glance system
|
||||
"""
|
||||
|
||||
import errno
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import glance.api.v1
|
||||
from glance.common import animation
|
||||
from glance.common import client as base_client
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
|
||||
SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS
|
||||
SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS
|
||||
|
||||
warn_msg = ("The 'glance.client' module is deprecated in favor of the "
|
||||
"'glanceclient' module provided by python-glanceclient (see "
|
||||
"http://github.com/openstack/python-glanceclient).")
|
||||
warnings.warn(warn_msg, stacklevel=2)
|
||||
|
||||
|
||||
class V1Client(base_client.BaseClient):
|
||||
|
||||
"""Main client class for accessing Glance resources"""
|
||||
|
||||
DEFAULT_PORT = 9292
|
||||
DEFAULT_DOC_ROOT = "/v1"
|
||||
|
||||
def get_images(self, **kwargs):
|
||||
"""
|
||||
Returns a list of image id/name mappings from Registry
|
||||
|
||||
:param filters: dictionary of attributes by which the resulting
|
||||
collection of images should be filtered
|
||||
:param marker: id after which to start the page of images
|
||||
:param limit: maximum number of items to return
|
||||
:param sort_key: results will be ordered by this image attribute
|
||||
:param sort_dir: direction in which to to order results (asc, desc)
|
||||
"""
|
||||
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
|
||||
res = self.do_request("GET", "/images", params=params)
|
||||
data = json.loads(res.read())['images']
|
||||
return data
|
||||
|
||||
def get_images_detailed(self, **kwargs):
|
||||
"""
|
||||
Returns a list of detailed image data mappings from Registry
|
||||
|
||||
:param filters: dictionary of attributes by which the resulting
|
||||
collection of images should be filtered
|
||||
:param marker: id after which to start the page of images
|
||||
:param limit: maximum number of items to return
|
||||
:param sort_key: results will be ordered by this image attribute
|
||||
:param sort_dir: direction in which to to order results (asc, desc)
|
||||
"""
|
||||
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
|
||||
res = self.do_request("GET", "/images/detail", params=params)
|
||||
data = json.loads(res.read())['images']
|
||||
return data
|
||||
|
||||
def get_image(self, image_id):
|
||||
"""
|
||||
Returns a tuple with the image's metadata and the raw disk image as
|
||||
a mime-encoded blob stream for the supplied opaque image identifier.
|
||||
|
||||
:param image_id: The opaque image identifier
|
||||
|
||||
:retval Tuple containing (image_meta, image_blob)
|
||||
:raises exception.NotFound if image is not found
|
||||
"""
|
||||
res = self.do_request("GET", "/images/%s" % image_id)
|
||||
|
||||
image = utils.get_image_meta_from_headers(res)
|
||||
return image, base_client.ImageBodyIterator(res)
|
||||
|
||||
def get_image_meta(self, image_id):
|
||||
"""
|
||||
Returns a mapping of image metadata from Registry
|
||||
|
||||
:raises exception.NotFound if image is not in registry
|
||||
"""
|
||||
res = self.do_request("HEAD", "/images/%s" % image_id)
|
||||
|
||||
image = utils.get_image_meta_from_headers(res)
|
||||
return image
|
||||
|
||||
def _get_image_size(self, image_data):
|
||||
"""
|
||||
Analyzes the incoming image file and attempts to determine
|
||||
its size.
|
||||
|
||||
:param image_data: The input to the client, typically a file
|
||||
redirected from stdin.
|
||||
:retval The image file's size or None if it cannot be determined.
|
||||
"""
|
||||
# For large images, we need to supply the size of the
|
||||
# image file. See LP Bugs #827660 and #845788.
|
||||
if hasattr(image_data, 'seek') and hasattr(image_data, 'tell'):
|
||||
try:
|
||||
image_data.seek(0, os.SEEK_END)
|
||||
image_size = image_data.tell()
|
||||
image_data.seek(0)
|
||||
return image_size
|
||||
except IOError, e:
|
||||
if e.errno == errno.ESPIPE:
|
||||
# Illegal seek. This means the user is trying
|
||||
# to pipe image data to the client, e.g.
|
||||
# echo testdata | bin/glance add blah..., or
|
||||
# that stdin is empty
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
def add_image(self, image_meta=None, image_data=None, features=None):
|
||||
"""
|
||||
Tells Glance about an image's metadata as well
|
||||
as optionally the image_data itself
|
||||
|
||||
:param image_meta: Optional Mapping of information about the
|
||||
image
|
||||
:param image_data: Optional string of raw image data
|
||||
or file-like object that can be
|
||||
used to read the image data
|
||||
:param features: Optional map of features
|
||||
|
||||
:retval The newly-stored image's metadata.
|
||||
"""
|
||||
headers = utils.image_meta_to_http_headers(image_meta or {})
|
||||
|
||||
if image_data:
|
||||
body = image_data
|
||||
headers['content-type'] = 'application/octet-stream'
|
||||
image_size = self._get_image_size(image_data)
|
||||
if image_size:
|
||||
headers['x-image-meta-size'] = image_size
|
||||
headers['content-length'] = image_size
|
||||
else:
|
||||
body = None
|
||||
|
||||
utils.add_features_to_http_headers(features, headers)
|
||||
|
||||
res = self.do_request("POST", "/images", body, headers)
|
||||
data = json.loads(res.read())
|
||||
return data['image']
|
||||
|
||||
def update_image(self, image_id, image_meta=None, image_data=None,
|
||||
features=None):
|
||||
"""
|
||||
Updates Glance's information about an image
|
||||
|
||||
:param image_id: Required image ID
|
||||
:param image_meta: Optional Mapping of information about the
|
||||
image
|
||||
:param image_data: Optional string of raw image data
|
||||
or file-like object that can be
|
||||
used to read the image data
|
||||
:param features: Optional map of features
|
||||
"""
|
||||
if image_meta is None:
|
||||
image_meta = {}
|
||||
|
||||
headers = utils.image_meta_to_http_headers(image_meta)
|
||||
|
||||
if image_data:
|
||||
body = image_data
|
||||
headers['content-type'] = 'application/octet-stream'
|
||||
image_size = self._get_image_size(image_data)
|
||||
if image_size:
|
||||
headers['x-image-meta-size'] = image_size
|
||||
headers['content-length'] = image_size
|
||||
else:
|
||||
body = None
|
||||
|
||||
utils.add_features_to_http_headers(features, headers)
|
||||
|
||||
res = self.do_request("PUT", "/images/%s" % image_id, body, headers)
|
||||
data = json.loads(res.read())
|
||||
return data['image']
|
||||
|
||||
def delete_image(self, image_id):
|
||||
"""
|
||||
Deletes Glance's information about an image
|
||||
"""
|
||||
self.do_request("DELETE", "/images/%s" % image_id)
|
||||
return True
|
||||
|
||||
def get_cached_images(self, **kwargs):
|
||||
"""
|
||||
Returns a list of images stored in the image cache.
|
||||
"""
|
||||
res = self.do_request("GET", "/cached_images")
|
||||
data = json.loads(res.read())['cached_images']
|
||||
return data
|
||||
|
||||
def get_queued_images(self, **kwargs):
|
||||
"""
|
||||
Returns a list of images queued for caching
|
||||
"""
|
||||
res = self.do_request("GET", "/queued_images")
|
||||
data = json.loads(res.read())['queued_images']
|
||||
return data
|
||||
|
||||
def delete_cached_image(self, image_id):
|
||||
"""
|
||||
Delete a specified image from the cache
|
||||
"""
|
||||
self.do_request("DELETE", "/cached_images/%s" % image_id)
|
||||
return True
|
||||
|
||||
def delete_all_cached_images(self):
|
||||
"""
|
||||
Delete all cached images
|
||||
"""
|
||||
res = self.do_request("DELETE", "/cached_images")
|
||||
data = json.loads(res.read())
|
||||
num_deleted = data['num_deleted']
|
||||
return num_deleted
|
||||
|
||||
def queue_image_for_caching(self, image_id):
|
||||
"""
|
||||
Queue an image for prefetching into cache
|
||||
"""
|
||||
self.do_request("PUT", "/queued_images/%s" % image_id)
|
||||
return True
|
||||
|
||||
def delete_queued_image(self, image_id):
|
||||
"""
|
||||
Delete a specified image from the cache queue
|
||||
"""
|
||||
self.do_request("DELETE", "/queued_images/%s" % image_id)
|
||||
return True
|
||||
|
||||
def delete_all_queued_images(self):
|
||||
"""
|
||||
Delete all queued images
|
||||
"""
|
||||
res = self.do_request("DELETE", "/queued_images")
|
||||
data = json.loads(res.read())
|
||||
num_deleted = data['num_deleted']
|
||||
return num_deleted
|
||||
|
||||
def get_image_members(self, image_id):
|
||||
"""Returns a mapping of image memberships from Registry"""
|
||||
res = self.do_request("GET", "/images/%s/members" % image_id)
|
||||
data = json.loads(res.read())['members']
|
||||
return data
|
||||
|
||||
def get_member_images(self, member_id):
|
||||
"""Returns a mapping of image memberships from Registry"""
|
||||
res = self.do_request("GET", "/shared-images/%s" % member_id)
|
||||
data = json.loads(res.read())['shared_images']
|
||||
return data
|
||||
|
||||
def _validate_assocs(self, assocs):
|
||||
"""
|
||||
Validates membership associations and returns an appropriate
|
||||
list of associations to send to the server.
|
||||
"""
|
||||
validated = []
|
||||
for assoc in assocs:
|
||||
assoc_data = dict(member_id=assoc['member_id'])
|
||||
if 'can_share' in assoc:
|
||||
assoc_data['can_share'] = bool(assoc['can_share'])
|
||||
validated.append(assoc_data)
|
||||
return validated
|
||||
|
||||
def replace_members(self, image_id, *assocs):
|
||||
"""
|
||||
Replaces the membership associations for a given image_id.
|
||||
Each subsequent argument is a dictionary mapping containing a
|
||||
'member_id' that should have access to the image_id. A
|
||||
'can_share' boolean can also be specified to allow the member
|
||||
to further share the image. An example invocation allowing
|
||||
'rackspace' to access image 1 and 'google' to access image 1
|
||||
with permission to share::
|
||||
|
||||
c = glance.client.Client(...)
|
||||
c.update_members(1, {'member_id': 'rackspace'},
|
||||
{'member_id': 'google', 'can_share': True})
|
||||
"""
|
||||
# Understand the associations
|
||||
body = json.dumps(self._validate_assocs(assocs))
|
||||
self.do_request("PUT", "/images/%s/members" % image_id, body,
|
||||
{'content-type': 'application/json'})
|
||||
return True
|
||||
|
||||
def add_member(self, image_id, member_id, can_share=None):
|
||||
"""
|
||||
Adds a membership association between image_id and member_id.
|
||||
If can_share is not specified and the association already
|
||||
exists, no change is made; if the association does not already
|
||||
exist, one is created with can_share defaulting to False.
|
||||
When can_share is specified, the association is created if it
|
||||
doesn't already exist, and the can_share attribute is set
|
||||
accordingly. Example invocations allowing 'rackspace' to
|
||||
access image 1 and 'google' to access image 1 with permission
|
||||
to share::
|
||||
|
||||
c = glance.client.Client(...)
|
||||
c.add_member(1, 'rackspace')
|
||||
c.add_member(1, 'google', True)
|
||||
"""
|
||||
body = None
|
||||
headers = {}
|
||||
# Generate the body if appropriate
|
||||
if can_share is not None:
|
||||
body = json.dumps(dict(member=dict(can_share=bool(can_share))))
|
||||
headers['content-type'] = 'application/json'
|
||||
|
||||
self.do_request("PUT", "/images/%s/members/%s" %
|
||||
(image_id, member_id), body, headers)
|
||||
return True
|
||||
|
||||
def delete_member(self, image_id, member_id):
|
||||
"""
|
||||
Deletes the membership assocation. If the
|
||||
association does not exist, no action is taken; otherwise, the
|
||||
indicated association is deleted. An example invocation
|
||||
removing the accesses of 'rackspace' to image 1 and 'google'
|
||||
to image 2::
|
||||
|
||||
c = glance.client.Client(...)
|
||||
c.delete_member(1, 'rackspace')
|
||||
c.delete_member(2, 'google')
|
||||
"""
|
||||
self.do_request("DELETE", "/images/%s/members/%s" %
|
||||
(image_id, member_id))
|
||||
return True
|
||||
|
||||
|
||||
class ProgressIteratorWrapper(object):
|
||||
|
||||
def __init__(self, wrapped, transfer_info):
|
||||
self.wrapped = wrapped
|
||||
self.transfer_info = transfer_info
|
||||
self.prev_len = 0L
|
||||
|
||||
def __iter__(self):
|
||||
for chunk in self.wrapped:
|
||||
if self.prev_len:
|
||||
self.transfer_info['so_far'] += self.prev_len
|
||||
self.prev_len = len(chunk)
|
||||
yield chunk
|
||||
# report final chunk
|
||||
self.transfer_info['so_far'] += self.prev_len
|
||||
|
||||
|
||||
class ProgressClient(V1Client):
|
||||
|
||||
"""
|
||||
Specialized class that adds progress bar output/interaction into the
|
||||
TTY of the calling client
|
||||
"""
|
||||
def image_iterator(self, connection, headers, body):
|
||||
wrapped = super(ProgressClient, self).image_iterator(connection,
|
||||
headers,
|
||||
body)
|
||||
try:
|
||||
# spawn the animation thread if the connection is good
|
||||
connection.connect()
|
||||
return ProgressIteratorWrapper(wrapped,
|
||||
self.start_animation(headers))
|
||||
except (httplib.HTTPResponse, socket.error):
|
||||
# the connection is out, just "pass"
|
||||
# and let the "glance add" fail with [Errno 111] Connection refused
|
||||
pass
|
||||
|
||||
def start_animation(self, headers):
|
||||
transfer_info = {
|
||||
'so_far': 0L,
|
||||
'size': headers.get('x-image-meta-size', 0L)
|
||||
}
|
||||
pg = animation.UploadProgressStatus(transfer_info)
|
||||
if transfer_info['size'] == 0L:
|
||||
sys.stdout.write("The progressbar doesn't show-up because "
|
||||
"the headers[x-meta-size] is zero or missing\n")
|
||||
sys.stdout.write("Uploading image '%s'\n" %
|
||||
headers.get('x-image-meta-name', ''))
|
||||
pg.start()
|
||||
return transfer_info
|
||||
|
||||
Client = V1Client
|
||||
|
||||
|
||||
def get_client(host, port=None, timeout=None, use_ssl=False, username=None,
|
||||
password=None, tenant=None,
|
||||
auth_url=None, auth_strategy=None,
|
||||
auth_token=None, region=None,
|
||||
is_silent_upload=False, insecure=False):
|
||||
"""
|
||||
Returns a new client Glance client object based on common kwargs.
|
||||
If an option isn't specified falls back to common environment variable
|
||||
defaults.
|
||||
"""
|
||||
|
||||
if auth_url or os.getenv('OS_AUTH_URL'):
|
||||
force_strategy = 'keystone'
|
||||
else:
|
||||
force_strategy = None
|
||||
|
||||
creds = dict(username=username or
|
||||
os.getenv('OS_AUTH_USER', os.getenv('OS_USERNAME')),
|
||||
password=password or
|
||||
os.getenv('OS_AUTH_KEY', os.getenv('OS_PASSWORD')),
|
||||
tenant=tenant or
|
||||
os.getenv('OS_AUTH_TENANT',
|
||||
os.getenv('OS_TENANT_NAME')),
|
||||
auth_url=auth_url or os.getenv('OS_AUTH_URL'),
|
||||
strategy=force_strategy or auth_strategy or
|
||||
os.getenv('OS_AUTH_STRATEGY', 'noauth'),
|
||||
region=region or os.getenv('OS_REGION_NAME'),
|
||||
)
|
||||
|
||||
if creds['strategy'] == 'keystone' and not creds['auth_url']:
|
||||
msg = ("--os_auth_url option or OS_AUTH_URL environment variable "
|
||||
"required when keystone authentication strategy is enabled\n")
|
||||
raise exception.ClientConfigurationError(msg)
|
||||
|
||||
client = (ProgressClient if not is_silent_upload else Client)
|
||||
|
||||
return client(host=host,
|
||||
port=port,
|
||||
timeout=timeout,
|
||||
use_ssl=use_ssl,
|
||||
auth_tok=auth_token or
|
||||
os.getenv('OS_TOKEN'),
|
||||
creds=creds,
|
||||
insecure=insecure)
|
@ -86,76 +86,6 @@ def handle_redirects(func):
|
||||
return wrapped
|
||||
|
||||
|
||||
class ImageBodyIterator(object):
|
||||
|
||||
"""
|
||||
A class that acts as an iterator over an image file's
|
||||
chunks of data. This is returned as part of the result
|
||||
tuple from `glance.client.Client.get_image`
|
||||
"""
|
||||
|
||||
def __init__(self, source):
|
||||
"""
|
||||
Constructs the object from a readable image source
|
||||
(such as an HTTPResponse or file-like object)
|
||||
"""
|
||||
self.source = source
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Exposes an iterator over the chunks of data in the
|
||||
image file.
|
||||
"""
|
||||
while True:
|
||||
chunk = self.source.read(CHUNKSIZE)
|
||||
if chunk:
|
||||
yield chunk
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
class SendFileIterator:
|
||||
"""
|
||||
Emulate iterator pattern over sendfile, in order to allow
|
||||
send progress be followed by wrapping the iteration.
|
||||
"""
|
||||
def __init__(self, connection, body):
|
||||
self.connection = connection
|
||||
self.body = body
|
||||
self.offset = 0
|
||||
self.sending = True
|
||||
|
||||
def __iter__(self):
|
||||
class OfLength:
|
||||
def __init__(self, len):
|
||||
self.len = len
|
||||
|
||||
def __len__(self):
|
||||
return self.len
|
||||
|
||||
while self.sending:
|
||||
try:
|
||||
sent = sendfile.sendfile(self.connection.sock.fileno(),
|
||||
self.body.fileno(),
|
||||
self.offset,
|
||||
CHUNKSIZE)
|
||||
except OSError as e:
|
||||
# suprisingly, sendfile may fail transiently instead of
|
||||
# blocking, in which case we select on the socket in order
|
||||
# to wait on its return to a writeable state before resuming
|
||||
# the send loop
|
||||
if e.errno in (errno.EAGAIN, errno.EBUSY):
|
||||
wlist = [self.connection.sock.fileno()]
|
||||
rfds, wfds, efds = select.select([], wlist, [])
|
||||
if wfds:
|
||||
continue
|
||||
raise
|
||||
|
||||
self.sending = (sent != 0)
|
||||
self.offset += sent
|
||||
yield OfLength(sent)
|
||||
|
||||
|
||||
class HTTPSClientAuthConnection(httplib.HTTPSConnection):
|
||||
"""
|
||||
Class to make a HTTPS connection, with support for
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,12 +15,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from glance.tests import functional
|
||||
import httplib2
|
||||
|
||||
from glance import client
|
||||
from glance.registry import client as registry_client
|
||||
from glance.tests import functional
|
||||
from glance.tests.utils import execute
|
||||
|
||||
|
||||
@ -35,95 +35,55 @@ class TestScrubber(functional.FunctionalTest):
|
||||
|
||||
"""Test that delayed_delete works and the scrubber deletes"""
|
||||
|
||||
def _get_client(self):
|
||||
return client.Client("localhost", self.api_port)
|
||||
|
||||
def _get_registry_client(self):
|
||||
return registry_client.RegistryClient('localhost',
|
||||
self.registry_port)
|
||||
|
||||
def test_immediate_delete(self):
|
||||
"""
|
||||
test that images get deleted immediately by default
|
||||
"""
|
||||
|
||||
self.cleanup()
|
||||
self.start_servers(**self.__dict__.copy())
|
||||
|
||||
client = self._get_client()
|
||||
registry = self._get_registry_client()
|
||||
meta = client.add_image(TEST_IMAGE_META, TEST_IMAGE_DATA)
|
||||
id = meta['id']
|
||||
|
||||
filters = {'deleted': True, 'is_public': 'none',
|
||||
'status': 'pending_delete'}
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertFalse(recs)
|
||||
|
||||
client.delete_image(id)
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertFalse(recs)
|
||||
|
||||
filters = {'deleted': True, 'is_public': 'none', 'status': 'deleted'}
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'deleted')
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
def test_delayed_delete(self):
|
||||
"""
|
||||
test that images don't get deleted immediatly and that the scrubber
|
||||
scrubs them
|
||||
"""
|
||||
|
||||
self.cleanup()
|
||||
self.start_servers(delayed_delete=True, daemon=True)
|
||||
|
||||
client = self._get_client()
|
||||
registry = self._get_registry_client()
|
||||
meta = client.add_image(TEST_IMAGE_META, TEST_IMAGE_DATA)
|
||||
id = meta['id']
|
||||
headers = {
|
||||
'x-image-meta-name': 'test_image',
|
||||
'x-image-meta-is_public': 'true',
|
||||
'x-image-meta-disk_format': 'raw',
|
||||
'x-image-meta-container_format': 'ovf',
|
||||
'content-type': 'application/octet-stream',
|
||||
}
|
||||
path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
http = httplib2.Http()
|
||||
response, content = http.request(path, 'POST', body='XXX',
|
||||
headers=headers)
|
||||
self.assertEqual(response.status, 201)
|
||||
image = json.loads(content)['image']
|
||||
self.assertEqual('active', image['status'])
|
||||
image_id = image['id']
|
||||
|
||||
filters = {'deleted': True, 'is_public': 'none',
|
||||
'status': 'pending_delete'}
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertFalse(recs)
|
||||
path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
|
||||
image_id)
|
||||
http = httplib2.Http()
|
||||
response, content = http.request(path, 'DELETE')
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
client.delete_image(id)
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertTrue(recs)
|
||||
|
||||
filters = {'deleted': True, 'is_public': 'none'}
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'pending_delete')
|
||||
response, content = http.request(path, 'HEAD')
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual('pending_delete', response['x-image-meta-status'])
|
||||
|
||||
# NOTE(jkoelker) The build servers sometimes take longer than
|
||||
# 15 seconds to scrub. Give it up to 5 min, checking
|
||||
# checking every 15 seconds. When/if it flips to
|
||||
# deleted, bail immediatly.
|
||||
deleted = set()
|
||||
recs = []
|
||||
for _ in xrange(3):
|
||||
time.sleep(5)
|
||||
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertTrue(recs)
|
||||
|
||||
# NOTE(jkoelker) Reset the deleted set for this loop
|
||||
deleted = set()
|
||||
for rec in recs:
|
||||
deleted.add(rec['status'] == 'deleted')
|
||||
|
||||
if False not in deleted:
|
||||
response, content = http.request(path, 'HEAD')
|
||||
if response['x-image-meta-status'] == 'deleted' and \
|
||||
response['x-image-meta-deleted'] == 'True':
|
||||
break
|
||||
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'deleted')
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
self.fail('image was never scrubbed')
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
@ -135,31 +95,31 @@ class TestScrubber(functional.FunctionalTest):
|
||||
self.cleanup()
|
||||
self.start_servers(delayed_delete=True, daemon=False)
|
||||
|
||||
client = self._get_client()
|
||||
registry = self._get_registry_client()
|
||||
headers = {
|
||||
'x-image-meta-name': 'test_image',
|
||||
'x-image-meta-is_public': 'true',
|
||||
'x-image-meta-disk_format': 'raw',
|
||||
'x-image-meta-container_format': 'ovf',
|
||||
'content-type': 'application/octet-stream',
|
||||
}
|
||||
path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
http = httplib2.Http()
|
||||
response, content = http.request(path, 'POST', body='XXX',
|
||||
headers=headers)
|
||||
self.assertEqual(response.status, 201)
|
||||
image = json.loads(content)['image']
|
||||
self.assertEqual('active', image['status'])
|
||||
image_id = image['id']
|
||||
|
||||
# add some images and ensure it was successful
|
||||
img_ids = []
|
||||
for i in range(0, 3):
|
||||
meta = client.add_image(TEST_IMAGE_META, TEST_IMAGE_DATA)
|
||||
id = meta['id']
|
||||
img_ids.append(id)
|
||||
filters = {'deleted': True, 'is_public': 'none',
|
||||
'status': 'pending_delete'}
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertFalse(recs)
|
||||
path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
|
||||
image_id)
|
||||
http = httplib2.Http()
|
||||
response, content = http.request(path, 'DELETE')
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
# delete those images
|
||||
for img_id in img_ids:
|
||||
client.delete_image(img_id)
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertTrue(recs)
|
||||
|
||||
filters = {'deleted': True, 'is_public': 'none'}
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'pending_delete')
|
||||
response, content = http.request(path, 'HEAD')
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual('pending_delete', response['x-image-meta-status'])
|
||||
|
||||
# wait for the scrub time on the image to pass
|
||||
time.sleep(self.api_server.scrub_time)
|
||||
@ -170,10 +130,20 @@ class TestScrubber(functional.FunctionalTest):
|
||||
exitcode, out, err = execute(cmd, raise_error=False)
|
||||
self.assertEqual(0, exitcode)
|
||||
|
||||
filters = {'deleted': True, 'is_public': 'none'}
|
||||
recs = registry.get_images_detailed(filters=filters)
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'deleted')
|
||||
# NOTE(jkoelker) The build servers sometimes take longer than
|
||||
# 15 seconds to scrub. Give it up to 5 min, checking
|
||||
# checking every 15 seconds. When/if it flips to
|
||||
# deleted, bail immediatly.
|
||||
for _ in xrange(3):
|
||||
time.sleep(5)
|
||||
|
||||
response, content = http.request(path, 'HEAD')
|
||||
if response['x-image-meta-status'] == 'deleted' and \
|
||||
response['x-image-meta-deleted'] == 'True':
|
||||
break
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
self.fail('image was never scrubbed')
|
||||
|
||||
self.stop_servers()
|
||||
|
@ -124,39 +124,3 @@ class TestMiscellaneous(functional.FunctionalTest):
|
||||
"in output: %s" % out)
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
def test_api_treats_size_as_a_normal_property(self):
|
||||
"""
|
||||
A test for LP bug #825024 -- glance client currently
|
||||
treats size as a normal property.
|
||||
"""
|
||||
|
||||
self.cleanup()
|
||||
self.start_servers()
|
||||
|
||||
# 1. POST /images with public image named Image1
|
||||
# attribute and no custom properties. Verify a 200 OK is returned
|
||||
with tempfile.NamedTemporaryFile() as image_file:
|
||||
image_file.write("XXX")
|
||||
image_file.flush()
|
||||
image_file_name = image_file.name
|
||||
suffix = 'size=12345 --silent-upload < %s' % image_file_name
|
||||
cmd = minimal_add_command(self.api_port, 'MyImage', suffix)
|
||||
|
||||
exitcode, out, err = execute(cmd)
|
||||
|
||||
image_id = out.strip().split(':')[1].strip()
|
||||
self.assertEqual(0, exitcode)
|
||||
self.assertTrue('Found non-settable field size. Removing.' in out)
|
||||
self.assertTrue('Added new image with ID: %s' % image_id in out)
|
||||
|
||||
# 2. Verify image added as public image
|
||||
cmd = "bin/glance --port=%d show %s" % (self.api_port, image_id)
|
||||
|
||||
exitcode, out, err = execute(cmd)
|
||||
|
||||
self.assertEqual(0, exitcode)
|
||||
lines = out.split("\n")[2:-1]
|
||||
self.assertFalse("12345" in out)
|
||||
|
||||
self.stop_servers()
|
||||
|
@ -37,8 +37,6 @@ import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from glance import client as glance_client
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.openstack.common import timeutils
|
||||
from glance.tests import functional
|
||||
@ -1234,96 +1232,3 @@ class TestSSL(functional.FunctionalTest):
|
||||
self.assertEqual(response.status, 404)
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
@skip_if_disabled
|
||||
def test_certificate_validation(self):
|
||||
"""
|
||||
Check SSL client cerificate verification
|
||||
"""
|
||||
self.cleanup()
|
||||
self.start_servers(**self.__dict__.copy())
|
||||
|
||||
# 0. GET /images
|
||||
# Verify no public images
|
||||
path = "https://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
https = httplib2.Http(disable_ssl_certificate_validation=True)
|
||||
response, content = https.request(path, 'GET')
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(content, '{"images": []}')
|
||||
|
||||
# 1. POST /images with public image named Image1
|
||||
headers = {'Content-Type': 'application/octet-stream',
|
||||
'X-Image-Meta-Name': 'Image1',
|
||||
'X-Image-Meta-Status': 'active',
|
||||
'X-Image-Meta-Container-Format': 'ovf',
|
||||
'X-Image-Meta-Disk-Format': 'vdi',
|
||||
'X-Image-Meta-Size': '19',
|
||||
'X-Image-Meta-Is-Public': 'True'}
|
||||
path = "https://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
https = httplib2.Http(disable_ssl_certificate_validation=True)
|
||||
response, content = https.request(path, 'POST', headers=headers)
|
||||
self.assertEqual(response.status, 201)
|
||||
data = json.loads(content)
|
||||
|
||||
image_id = data['image']['id']
|
||||
|
||||
# 2. Attempt to delete the image *without* CA file
|
||||
path = "https://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
secure_cli = glance_client.Client(host="127.0.0.1", port=self.api_port,
|
||||
use_ssl=True, insecure=False)
|
||||
try:
|
||||
secure_cli.delete_image(image_id)
|
||||
self.fail("Client with no CA file deleted image %s" % image_id)
|
||||
except exception.ClientConnectionError, e:
|
||||
pass
|
||||
|
||||
# 3. Delete the image with a secure client *with* CA file
|
||||
secure_cli2 = glance_client.Client(host="127.0.0.1",
|
||||
port=self.api_port, use_ssl=True,
|
||||
ca_file=self.ca_file,
|
||||
insecure=False)
|
||||
try:
|
||||
secure_cli2.delete_image(image_id)
|
||||
except exception.ClientConnectionError, e:
|
||||
self.fail("Secure client failed to delete image %s" % image_id)
|
||||
|
||||
# Verify image is deleted
|
||||
path = "https://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
https = httplib2.Http(disable_ssl_certificate_validation=True)
|
||||
response, content = https.request(path, 'GET')
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(content, '{"images": []}')
|
||||
|
||||
# 4. POST another image
|
||||
headers = {'Content-Type': 'application/octet-stream',
|
||||
'X-Image-Meta-Name': 'Image1',
|
||||
'X-Image-Meta-Status': 'active',
|
||||
'X-Image-Meta-Container-Format': 'ovf',
|
||||
'X-Image-Meta-Disk-Format': 'vdi',
|
||||
'X-Image-Meta-Size': '19',
|
||||
'X-Image-Meta-Is-Public': 'True'}
|
||||
path = "https://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
https = httplib2.Http(disable_ssl_certificate_validation=True)
|
||||
response, content = https.request(path, 'POST', headers=headers)
|
||||
self.assertEqual(response.status, 201)
|
||||
data = json.loads(content)
|
||||
|
||||
image_id = data['image']['id']
|
||||
|
||||
# 5. Delete the image with an insecure client
|
||||
insecure_cli = glance_client.Client(host="127.0.0.1",
|
||||
port=self.api_port, use_ssl=True,
|
||||
insecure=True)
|
||||
try:
|
||||
insecure_cli.delete_image(image_id)
|
||||
except exception.ClientConnectionError, e:
|
||||
self.fail("Insecure client failed to delete image")
|
||||
|
||||
# Verify image is deleted
|
||||
path = "https://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||
https = httplib2.Http(disable_ssl_certificate_validation=True)
|
||||
response, content = https.request(path, 'GET')
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(content, '{"images": []}')
|
||||
|
||||
self.stop_servers()
|
||||
|
@ -186,8 +186,6 @@ def stub_out_registry_and_store_server(stubs, base_dir):
|
||||
glance.common.client.BaseClient._sendable)
|
||||
stubs.Set(glance.common.client.BaseClient, '_sendable',
|
||||
fake_sendable)
|
||||
stubs.Set(glance.common.client.ImageBodyIterator, '__iter__',
|
||||
fake_image_iter)
|
||||
|
||||
|
||||
def stub_out_registry_server(stubs, **kwargs):
|
||||
@ -212,5 +210,3 @@ def stub_out_registry_server(stubs, **kwargs):
|
||||
|
||||
stubs.Set(glance.common.client.BaseClient, 'get_connection_type',
|
||||
fake_get_connection_type)
|
||||
stubs.Set(glance.common.client.ImageBodyIterator, '__iter__',
|
||||
fake_image_iter)
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user