Merge "Adds support for protecting images from accidental deletion."
This commit is contained in:
11
bin/glance
11
bin/glance
@@ -131,6 +131,7 @@ def print_image_formatted(client, image):
|
|||||||
image['id'])
|
image['id'])
|
||||||
print "Id: %s" % image['id']
|
print "Id: %s" % image['id']
|
||||||
print "Public: " + (image['is_public'] and "Yes" or "No")
|
print "Public: " + (image['is_public'] and "Yes" or "No")
|
||||||
|
print "Protected: " + (image['protected'] and "Yes" or "No")
|
||||||
print "Name: %s" % image['name']
|
print "Name: %s" % image['name']
|
||||||
print "Status: %s" % image['status']
|
print "Status: %s" % image['status']
|
||||||
print "Size: %d" % int(image['size'])
|
print "Size: %d" % int(image['size'])
|
||||||
@@ -167,6 +168,9 @@ size Optional. Should be size in bytes of the image if
|
|||||||
is_public Optional. If specified, interpreted as a boolean value
|
is_public Optional. If specified, interpreted as a boolean value
|
||||||
and sets or unsets the image's availability to the public.
|
and sets or unsets the image's availability to the public.
|
||||||
The default value is False.
|
The default value is False.
|
||||||
|
protected Optional. If specified, interpreted as a boolean value
|
||||||
|
and enables or disables deletion protection.
|
||||||
|
The default value is False.
|
||||||
disk_format Optional. Possible values are 'vhd','vmdk','raw', 'qcow2',
|
disk_format Optional. Possible values are 'vhd','vmdk','raw', 'qcow2',
|
||||||
and 'ami'. Default value is 'raw'.
|
and 'ami'. Default value is 'raw'.
|
||||||
container_format Optional. Possible values are 'ovf' and 'ami'.
|
container_format Optional. Possible values are 'ovf' and 'ami'.
|
||||||
@@ -212,6 +216,8 @@ EXAMPLES
|
|||||||
image_meta = {'name': fields.pop('name'),
|
image_meta = {'name': fields.pop('name'),
|
||||||
'is_public': utils.bool_from_string(
|
'is_public': utils.bool_from_string(
|
||||||
fields.pop('is_public', False)),
|
fields.pop('is_public', False)),
|
||||||
|
'protected': utils.bool_from_string(
|
||||||
|
fields.pop('protected', False)),
|
||||||
'disk_format': fields.pop('disk_format', 'raw'),
|
'disk_format': fields.pop('disk_format', 'raw'),
|
||||||
'min_disk': fields.pop('min_disk', 0),
|
'min_disk': fields.pop('min_disk', 0),
|
||||||
'min_ram': fields.pop('min_ram', 0),
|
'min_ram': fields.pop('min_ram', 0),
|
||||||
@@ -291,6 +297,8 @@ name A name for the image.
|
|||||||
location The location of the image.
|
location The location of the image.
|
||||||
is_public If specified, interpreted as a boolean value
|
is_public If specified, interpreted as a boolean value
|
||||||
and sets or unsets the image's availability to the public.
|
and sets or unsets the image's availability to the public.
|
||||||
|
protected If specified, interpreted as a boolean value
|
||||||
|
and enables or disables deletion protection for the image.
|
||||||
disk_format Format of the disk image
|
disk_format Format of the disk image
|
||||||
container_format Format of the container
|
container_format Format of the container
|
||||||
|
|
||||||
@@ -331,6 +339,9 @@ to spell field names correctly. :)"""
|
|||||||
if 'is_public' in fields:
|
if 'is_public' in fields:
|
||||||
image_meta['is_public'] = utils.bool_from_string(
|
image_meta['is_public'] = utils.bool_from_string(
|
||||||
fields.pop('is_public'))
|
fields.pop('is_public'))
|
||||||
|
if 'protected' in fields:
|
||||||
|
image_meta['protected'] = utils.bool_from_string(
|
||||||
|
fields.pop('protected'))
|
||||||
|
|
||||||
# Add custom attributes, which are all the arguments remaining
|
# Add custom attributes, which are all the arguments remaining
|
||||||
image_meta['properties'] = fields
|
image_meta['properties'] = fields
|
||||||
|
@@ -17,6 +17,6 @@
|
|||||||
|
|
||||||
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
||||||
'min_ram', 'min_disk', 'size_min', 'size_max',
|
'min_ram', 'min_disk', 'size_min', 'size_max',
|
||||||
'is_public', 'changes-since']
|
'is_public', 'changes-since', 'protected']
|
||||||
|
|
||||||
SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
|
SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
|
||||||
|
@@ -567,6 +567,11 @@ class Controller(controller.BaseController):
|
|||||||
content_type="text/plain")
|
content_type="text/plain")
|
||||||
|
|
||||||
image = self.get_image_meta_or_404(req, id)
|
image = self.get_image_meta_or_404(req, id)
|
||||||
|
if image['protected']:
|
||||||
|
msg = _("Image is protected")
|
||||||
|
logger.debug(msg)
|
||||||
|
raise HTTPForbidden(msg, request=req,
|
||||||
|
content_type="text/plain")
|
||||||
|
|
||||||
# The image's location field may be None in the case
|
# The image's location field may be None in the case
|
||||||
# of a saving or queued image, therefore don't ask a backend
|
# of a saving or queued image, therefore don't ask a backend
|
||||||
|
@@ -104,10 +104,9 @@ def get_image_meta_from_headers(response):
|
|||||||
result['properties'] = properties
|
result['properties'] = properties
|
||||||
if 'size' in result:
|
if 'size' in result:
|
||||||
result['size'] = int(result['size'])
|
result['size'] = int(result['size'])
|
||||||
if 'is_public' in result:
|
for key in ('is_public', 'deleted', 'protected'):
|
||||||
result['is_public'] = bool_from_header_value(result['is_public'])
|
if key in result:
|
||||||
if 'deleted' in result:
|
result[key] = bool_from_header_value(result[key])
|
||||||
result['deleted'] = bool_from_header_value(result['deleted'])
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@@ -38,7 +38,7 @@ DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size',
|
|||||||
|
|
||||||
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
||||||
'min_ram', 'min_disk', 'size_min', 'size_max',
|
'min_ram', 'min_disk', 'size_min', 'size_max',
|
||||||
'changes-since']
|
'changes-since', 'protected']
|
||||||
|
|
||||||
SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format',
|
SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format',
|
||||||
'size', 'id', 'created_at', 'updated_at')
|
'size', 'id', 'created_at', 'updated_at')
|
||||||
@@ -170,6 +170,14 @@ class Controller(object):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise exc.HTTPBadRequest(_("Unrecognized changes-since value"))
|
raise exc.HTTPBadRequest(_("Unrecognized changes-since value"))
|
||||||
|
|
||||||
|
if 'protected' in filters:
|
||||||
|
value = self._get_bool(filters['protected'])
|
||||||
|
if value is None:
|
||||||
|
raise exc.HTTPBadRequest(_("protected must be True, or "
|
||||||
|
"False"))
|
||||||
|
|
||||||
|
filters['protected'] = value
|
||||||
|
|
||||||
# only allow admins to filter on 'deleted'
|
# only allow admins to filter on 'deleted'
|
||||||
if req.context.is_admin:
|
if req.context.is_admin:
|
||||||
deleted_filter = self._parse_deleted_filter(req)
|
deleted_filter = self._parse_deleted_filter(req)
|
||||||
@@ -226,6 +234,15 @@ class Controller(object):
|
|||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
return sort_dir
|
return sort_dir
|
||||||
|
|
||||||
|
def _get_bool(self, value):
|
||||||
|
value = value.lower()
|
||||||
|
if value == 'true' or value == '1':
|
||||||
|
return True
|
||||||
|
elif value == 'false' or value == '0':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_is_public(self, req):
|
def _get_is_public(self, req):
|
||||||
"""Parse is_public into something usable."""
|
"""Parse is_public into something usable."""
|
||||||
is_public = req.str_params.get('is_public', None)
|
is_public = req.str_params.get('is_public', None)
|
||||||
@@ -234,16 +251,15 @@ class Controller(object):
|
|||||||
# NOTE(vish): This preserves the default value of showing only
|
# NOTE(vish): This preserves the default value of showing only
|
||||||
# public images.
|
# public images.
|
||||||
return True
|
return True
|
||||||
is_public = is_public.lower()
|
elif is_public.lower() == 'none':
|
||||||
if is_public == 'none':
|
|
||||||
return None
|
return None
|
||||||
elif is_public == 'true' or is_public == '1':
|
|
||||||
return True
|
value = self._get_bool(is_public)
|
||||||
elif is_public == 'false' or is_public == '0':
|
if value is None:
|
||||||
return False
|
raise exc.HTTPBadRequest(_("is_public must be None, True, or "
|
||||||
else:
|
"False"))
|
||||||
raise exc.HTTPBadRequest(_("is_public must be None, True, "
|
|
||||||
"or False"))
|
return value
|
||||||
|
|
||||||
def _parse_deleted_filter(self, req):
|
def _parse_deleted_filter(self, req):
|
||||||
"""Parse deleted into something usable."""
|
"""Parse deleted into something usable."""
|
||||||
|
@@ -49,7 +49,8 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
|
|||||||
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
|
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
|
||||||
'disk_format', 'container_format',
|
'disk_format', 'container_format',
|
||||||
'min_disk', 'min_ram', 'is_public',
|
'min_disk', 'min_ram', 'is_public',
|
||||||
'location', 'checksum', 'owner'])
|
'location', 'checksum', 'owner',
|
||||||
|
'protected'])
|
||||||
|
|
||||||
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
|
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
|
||||||
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi',
|
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi',
|
||||||
@@ -347,6 +348,7 @@ def _image_update(context, values, image_id, purge_props=False):
|
|||||||
values['min_disk'] = int(values['min_disk'] or 0)
|
values['min_disk'] = int(values['min_disk'] or 0)
|
||||||
|
|
||||||
values['is_public'] = bool(values.get('is_public', False))
|
values['is_public'] = bool(values.get('is_public', False))
|
||||||
|
values['protected'] = bool(values.get('protected', False))
|
||||||
image_ref = models.Image()
|
image_ref = models.Image()
|
||||||
|
|
||||||
# Need to canonicalize ownership
|
# Need to canonicalize ownership
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 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.
|
||||||
|
|
||||||
|
from sqlalchemy import MetaData, Table, Column, Boolean
|
||||||
|
|
||||||
|
|
||||||
|
meta = MetaData()
|
||||||
|
|
||||||
|
protected = Column('protected', Boolean, default=False)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
images = Table('images', meta, autoload=True)
|
||||||
|
images.create_column(protected)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(migrate_engine):
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
images = Table('images', meta, autoload=True)
|
||||||
|
images.drop_column(protected)
|
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* This is necessary because sqlalchemy has various bugs preventing
|
||||||
|
* downgrades from working correctly.
|
||||||
|
*/
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE images_backup (
|
||||||
|
id VARCHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(255),
|
||||||
|
size INTEGER,
|
||||||
|
status VARCHAR(30) NOT NULL,
|
||||||
|
is_public BOOLEAN NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
deleted_at DATETIME,
|
||||||
|
deleted BOOLEAN NOT NULL,
|
||||||
|
disk_format VARCHAR(20),
|
||||||
|
container_format VARCHAR(20),
|
||||||
|
checksum VARCHAR(32),
|
||||||
|
owner VARCHAR(255),
|
||||||
|
min_disk INTEGER NOT NULL,
|
||||||
|
min_ram INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CHECK (is_public IN (0, 1)),
|
||||||
|
CHECK (deleted IN (0, 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO images_backup
|
||||||
|
SELECT id, name, size, status, is_public, location, created_at, updated_at, deleted_at, deleted, disk_format, container_format, checksum, owner, min_disk, min_ram
|
||||||
|
FROM images;
|
||||||
|
|
||||||
|
DROP TABLE images;
|
||||||
|
|
||||||
|
CREATE TABLE images (
|
||||||
|
id VARCHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(255),
|
||||||
|
size INTEGER,
|
||||||
|
status VARCHAR(30) NOT NULL,
|
||||||
|
is_public BOOLEAN NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
deleted_at DATETIME,
|
||||||
|
deleted BOOLEAN NOT NULL,
|
||||||
|
disk_format VARCHAR(20),
|
||||||
|
container_format VARCHAR(20),
|
||||||
|
checksum VARCHAR(32),
|
||||||
|
owner VARCHAR(255),
|
||||||
|
min_disk INTEGER NOT NULL,
|
||||||
|
min_ram INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CHECK (is_public IN (0, 1)),
|
||||||
|
CHECK (deleted IN (0, 1))
|
||||||
|
);
|
||||||
|
CREATE INDEX ix_images_is_public ON images (is_public);
|
||||||
|
CREATE INDEX ix_images_deleted ON images (deleted);
|
||||||
|
|
||||||
|
INSERT INTO images
|
||||||
|
SELECT id, name, size, status, is_public, location, created_at, updated_at, deleted_at, deleted, disk_format, container_format, checksum, owner, min_disk, min_ram
|
||||||
|
FROM images_backup;
|
||||||
|
|
||||||
|
DROP TABLE images_backup;
|
||||||
|
COMMIT;
|
@@ -110,6 +110,7 @@ class Image(BASE, ModelBase):
|
|||||||
min_disk = Column(Integer(), nullable=False, default=0)
|
min_disk = Column(Integer(), nullable=False, default=0)
|
||||||
min_ram = Column(Integer(), nullable=False, default=0)
|
min_ram = Column(Integer(), nullable=False, default=0)
|
||||||
owner = Column(String(255))
|
owner = Column(String(255))
|
||||||
|
protected = Column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
|
||||||
class ImageProperty(BASE, ModelBase):
|
class ImageProperty(BASE, ModelBase):
|
||||||
|
@@ -667,6 +667,7 @@ class TestApi(functional.FunctionalTest):
|
|||||||
'X-Image-Meta-Disk-Format': 'vdi',
|
'X-Image-Meta-Disk-Format': 'vdi',
|
||||||
'X-Image-Meta-Size': '19',
|
'X-Image-Meta-Size': '19',
|
||||||
'X-Image-Meta-Is-Public': 'True',
|
'X-Image-Meta-Is-Public': 'True',
|
||||||
|
'X-Image-Meta-Protected': 'True',
|
||||||
'X-Image-Meta-Property-pants': 'are on'}
|
'X-Image-Meta-Property-pants': 'are on'}
|
||||||
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||||
http = httplib2.Http()
|
http = httplib2.Http()
|
||||||
@@ -684,6 +685,7 @@ class TestApi(functional.FunctionalTest):
|
|||||||
'X-Image-Meta-Disk-Format': 'vhd',
|
'X-Image-Meta-Disk-Format': 'vhd',
|
||||||
'X-Image-Meta-Size': '20',
|
'X-Image-Meta-Size': '20',
|
||||||
'X-Image-Meta-Is-Public': 'True',
|
'X-Image-Meta-Is-Public': 'True',
|
||||||
|
'X-Image-Meta-Protected': 'False',
|
||||||
'X-Image-Meta-Property-pants': 'are on'}
|
'X-Image-Meta-Property-pants': 'are on'}
|
||||||
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||||
http = httplib2.Http()
|
http = httplib2.Http()
|
||||||
@@ -701,6 +703,7 @@ class TestApi(functional.FunctionalTest):
|
|||||||
'X-Image-Meta-Disk-Format': 'ami',
|
'X-Image-Meta-Disk-Format': 'ami',
|
||||||
'X-Image-Meta-Size': '21',
|
'X-Image-Meta-Size': '21',
|
||||||
'X-Image-Meta-Is-Public': 'True',
|
'X-Image-Meta-Is-Public': 'True',
|
||||||
|
'X-Image-Meta-Protected': 'False',
|
||||||
'X-Image-Meta-Property-pants': 'are off'}
|
'X-Image-Meta-Property-pants': 'are off'}
|
||||||
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||||
http = httplib2.Http()
|
http = httplib2.Http()
|
||||||
@@ -717,7 +720,8 @@ class TestApi(functional.FunctionalTest):
|
|||||||
'X-Image-Meta-Container-Format': 'ami',
|
'X-Image-Meta-Container-Format': 'ami',
|
||||||
'X-Image-Meta-Disk-Format': 'ami',
|
'X-Image-Meta-Disk-Format': 'ami',
|
||||||
'X-Image-Meta-Size': '22',
|
'X-Image-Meta-Size': '22',
|
||||||
'X-Image-Meta-Is-Public': 'False'}
|
'X-Image-Meta-Is-Public': 'False',
|
||||||
|
'X-Image-Meta-Protected': 'False'}
|
||||||
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||||
http = httplib2.Http()
|
http = httplib2.Http()
|
||||||
response, content = http.request(path, 'POST', headers=headers)
|
response, content = http.request(path, 'POST', headers=headers)
|
||||||
@@ -851,7 +855,31 @@ class TestApi(functional.FunctionalTest):
|
|||||||
for image in data['images']:
|
for image in data['images']:
|
||||||
self.assertNotEqual(image['name'], "My Private Image")
|
self.assertNotEqual(image['name'], "My Private Image")
|
||||||
|
|
||||||
# 12. GET /images with property filter
|
# 12. Get /images with protected=False filter
|
||||||
|
# Verify correct images returned with property
|
||||||
|
params = "protected=False"
|
||||||
|
path = "http://%s:%d/v1/images?%s" % (
|
||||||
|
"0.0.0.0", self.api_port, params)
|
||||||
|
response, content = http.request(path, 'GET')
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
data = json.loads(content)
|
||||||
|
self.assertEqual(len(data['images']), 2)
|
||||||
|
for image in data['images']:
|
||||||
|
self.assertNotEqual(image['name'], "Image1")
|
||||||
|
|
||||||
|
# 13. Get /images with protected=True filter
|
||||||
|
# Verify correct images returned with property
|
||||||
|
params = "protected=True"
|
||||||
|
path = "http://%s:%d/v1/images?%s" % (
|
||||||
|
"0.0.0.0", self.api_port, params)
|
||||||
|
response, content = http.request(path, 'GET')
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
data = json.loads(content)
|
||||||
|
self.assertEqual(len(data['images']), 1)
|
||||||
|
for image in data['images']:
|
||||||
|
self.assertEqual(image['name'], "Image1")
|
||||||
|
|
||||||
|
# 14. GET /images with property filter
|
||||||
# Verify correct images returned with property
|
# Verify correct images returned with property
|
||||||
params = "property-pants=are%20on"
|
params = "property-pants=are%20on"
|
||||||
path = "http://%s:%d/v1/images/detail?%s" % (
|
path = "http://%s:%d/v1/images/detail?%s" % (
|
||||||
@@ -863,7 +891,7 @@ class TestApi(functional.FunctionalTest):
|
|||||||
for image in data['images']:
|
for image in data['images']:
|
||||||
self.assertEqual(image['properties']['pants'], "are on")
|
self.assertEqual(image['properties']['pants'], "are on")
|
||||||
|
|
||||||
# 13. GET /images with property filter and name filter
|
# 15. GET /images with property filter and name filter
|
||||||
# Verify correct images returned with property and name
|
# Verify correct images returned with property and name
|
||||||
# Make sure you quote the url when using more than one param!
|
# Make sure you quote the url when using more than one param!
|
||||||
params = "name=My%20Image!&property-pants=are%20on"
|
params = "name=My%20Image!&property-pants=are%20on"
|
||||||
@@ -877,7 +905,7 @@ class TestApi(functional.FunctionalTest):
|
|||||||
self.assertEqual(image['properties']['pants'], "are on")
|
self.assertEqual(image['properties']['pants'], "are on")
|
||||||
self.assertEqual(image['name'], "My Image!")
|
self.assertEqual(image['name'], "My Image!")
|
||||||
|
|
||||||
# 14. GET /images with past changes-since filter
|
# 16. GET /images with past changes-since filter
|
||||||
dt1 = datetime.datetime.utcnow() - datetime.timedelta(1)
|
dt1 = datetime.datetime.utcnow() - datetime.timedelta(1)
|
||||||
iso1 = utils.isotime(dt1)
|
iso1 = utils.isotime(dt1)
|
||||||
params = "changes-since=%s" % iso1
|
params = "changes-since=%s" % iso1
|
||||||
@@ -887,7 +915,7 @@ class TestApi(functional.FunctionalTest):
|
|||||||
data = json.loads(content)
|
data = json.loads(content)
|
||||||
self.assertEqual(len(data['images']), 3)
|
self.assertEqual(len(data['images']), 3)
|
||||||
|
|
||||||
# 15. GET /images with future changes-since filter
|
# 17. GET /images with future changes-since filter
|
||||||
dt2 = datetime.datetime.utcnow() + datetime.timedelta(1)
|
dt2 = datetime.datetime.utcnow() + datetime.timedelta(1)
|
||||||
iso2 = utils.isotime(dt2)
|
iso2 = utils.isotime(dt2)
|
||||||
params = "changes-since=%s" % iso2
|
params = "changes-since=%s" % iso2
|
||||||
@@ -897,14 +925,6 @@ class TestApi(functional.FunctionalTest):
|
|||||||
data = json.loads(content)
|
data = json.loads(content)
|
||||||
self.assertEqual(len(data['images']), 0)
|
self.assertEqual(len(data['images']), 0)
|
||||||
|
|
||||||
# DELETE images
|
|
||||||
for image_id in image_ids:
|
|
||||||
path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
|
|
||||||
image_id)
|
|
||||||
http = httplib2.Http()
|
|
||||||
response, content = http.request(path, 'DELETE')
|
|
||||||
self.assertEqual(response.status, 200)
|
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
|
||||||
@skip_if_disabled
|
@skip_if_disabled
|
||||||
|
@@ -508,26 +508,26 @@ class TestBinGlance(functional.FunctionalTest):
|
|||||||
|
|
||||||
self.assertEqual(0, exitcode)
|
self.assertEqual(0, exitcode)
|
||||||
image_lines = out.split("\n")[1:-1]
|
image_lines = out.split("\n")[1:-1]
|
||||||
self.assertEqual(24, len(image_lines))
|
self.assertEqual(26, len(image_lines))
|
||||||
self.assertEqual(image_lines[1].split()[1], image_ids[1])
|
self.assertEqual(image_lines[1].split()[1], image_ids[1])
|
||||||
self.assertEqual(image_lines[13].split()[1], image_ids[0])
|
self.assertEqual(image_lines[14].split()[1], image_ids[0])
|
||||||
|
|
||||||
# 10. Check min_ram filter
|
# 12. Check min_ram filter
|
||||||
cmd = "min_ram=256"
|
cmd = "min_ram=256"
|
||||||
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
|
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
|
||||||
|
|
||||||
self.assertEqual(0, exitcode)
|
self.assertEqual(0, exitcode)
|
||||||
image_lines = out.split("\n")[2:-1]
|
image_lines = out.split("\n")[2:-1]
|
||||||
self.assertEqual(11, len(image_lines))
|
self.assertEqual(12, len(image_lines))
|
||||||
self.assertEqual(image_lines[0].split()[1], image_ids[2])
|
self.assertEqual(image_lines[0].split()[1], image_ids[2])
|
||||||
|
|
||||||
# 11. Check min_disk filter
|
# 13. Check min_disk filter
|
||||||
cmd = "min_disk=7"
|
cmd = "min_disk=7"
|
||||||
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
|
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
|
||||||
|
|
||||||
self.assertEqual(0, exitcode)
|
self.assertEqual(0, exitcode)
|
||||||
image_lines = out.split("\n")[2:-1]
|
image_lines = out.split("\n")[2:-1]
|
||||||
self.assertEqual(11, len(image_lines))
|
self.assertEqual(12, len(image_lines))
|
||||||
self.assertEqual(image_lines[0].split()[1], image_ids[2])
|
self.assertEqual(image_lines[0].split()[1], image_ids[2])
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
@@ -610,9 +610,9 @@ class TestBinGlance(functional.FunctionalTest):
|
|||||||
|
|
||||||
self.assertEqual(0, exitcode)
|
self.assertEqual(0, exitcode)
|
||||||
image_lines = out.split("\n")[1:-1]
|
image_lines = out.split("\n")[1:-1]
|
||||||
self.assertEqual(22, len(image_lines))
|
self.assertEqual(24, len(image_lines))
|
||||||
self.assertTrue(image_lines[1].split()[1], image_ids[2])
|
self.assertTrue(image_lines[1].split()[1], image_ids[2])
|
||||||
self.assertTrue(image_lines[12].split()[1], image_ids[1])
|
self.assertTrue(image_lines[13].split()[1], image_ids[1])
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
|
||||||
@@ -686,10 +686,10 @@ class TestBinGlance(functional.FunctionalTest):
|
|||||||
|
|
||||||
self.assertEqual(0, exitcode)
|
self.assertEqual(0, exitcode)
|
||||||
image_lines = out.split("\n")[1:-1]
|
image_lines = out.split("\n")[1:-1]
|
||||||
self.assertEqual(33, len(image_lines))
|
self.assertEqual(36, len(image_lines))
|
||||||
self.assertTrue(image_lines[1].split()[1], image_ids[2])
|
self.assertTrue(image_lines[1].split()[1], image_ids[2])
|
||||||
self.assertTrue(image_lines[12].split()[1], image_ids[1])
|
self.assertTrue(image_lines[13].split()[1], image_ids[1])
|
||||||
self.assertTrue(image_lines[23].split()[1], image_ids[4])
|
self.assertTrue(image_lines[25].split()[1], image_ids[4])
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
|
||||||
@@ -744,4 +744,88 @@ class TestBinGlance(functional.FunctionalTest):
|
|||||||
self.assertEqual(0, exitcode)
|
self.assertEqual(0, exitcode)
|
||||||
self.assertEqual('Deleted image %s' % image_id, out.strip())
|
self.assertEqual('Deleted image %s' % image_id, out.strip())
|
||||||
|
|
||||||
|
def test_protected_image(self):
|
||||||
|
"""
|
||||||
|
We test the following:
|
||||||
|
|
||||||
|
0. Verify no public images in index
|
||||||
|
1. Add a public image with a location attr
|
||||||
|
protected and no image data
|
||||||
|
2. Check that image exists in index
|
||||||
|
3. Attempt to delete the image
|
||||||
|
4. Remove protection from image
|
||||||
|
5. Delete the image
|
||||||
|
6. Verify no longer in index
|
||||||
|
"""
|
||||||
|
self.cleanup()
|
||||||
|
self.start_servers()
|
||||||
|
|
||||||
|
api_port = self.api_port
|
||||||
|
registry_port = self.registry_port
|
||||||
|
|
||||||
|
# 0. Verify no public images
|
||||||
|
cmd = "bin/glance --port=%d index" % api_port
|
||||||
|
|
||||||
|
exitcode, out, err = execute(cmd)
|
||||||
|
|
||||||
|
self.assertEqual(0, exitcode)
|
||||||
|
self.assertEqual('', out.strip())
|
||||||
|
|
||||||
|
# 1. Add public image
|
||||||
|
cmd = "echo testdata | bin/glance --port=%d add is_public=True"\
|
||||||
|
" protected=True name=MyImage" % api_port
|
||||||
|
|
||||||
|
exitcode, out, err = execute(cmd)
|
||||||
|
|
||||||
|
self.assertEqual(0, exitcode)
|
||||||
|
self.assertTrue(out.strip().startswith('Added new image with ID:'))
|
||||||
|
|
||||||
|
# 2. Verify image added as public image
|
||||||
|
cmd = "bin/glance --port=%d index" % api_port
|
||||||
|
|
||||||
|
exitcode, out, err = execute(cmd)
|
||||||
|
|
||||||
|
self.assertEqual(0, exitcode)
|
||||||
|
lines = out.split("\n")[2:-1]
|
||||||
|
self.assertEqual(1, len(lines))
|
||||||
|
|
||||||
|
line = lines[0]
|
||||||
|
|
||||||
|
image_id, name, disk_format, container_format, size = \
|
||||||
|
[c.strip() for c in line.split()]
|
||||||
|
self.assertEqual('MyImage', name)
|
||||||
|
|
||||||
|
# 3. Delete the image
|
||||||
|
cmd = "bin/glance --port=%d --force delete %s" % (api_port, image_id)
|
||||||
|
|
||||||
|
exitcode, out, err = execute(cmd, raise_error=False)
|
||||||
|
|
||||||
|
self.assertNotEqual(0, exitcode)
|
||||||
|
self.assertTrue('Image is protected' in err)
|
||||||
|
|
||||||
|
# 4. Remove image protection
|
||||||
|
cmd = "bin/glance --port=%d --force update %s" \
|
||||||
|
" protected=False" % (api_port, image_id)
|
||||||
|
|
||||||
|
exitcode, out, err = execute(cmd)
|
||||||
|
|
||||||
|
self.assertEqual(0, exitcode)
|
||||||
|
self.assertTrue(out.strip().startswith('Updated image'))
|
||||||
|
|
||||||
|
# 5. Delete the image
|
||||||
|
cmd = "bin/glance --port=%d --force delete %s" % (api_port, image_id)
|
||||||
|
|
||||||
|
exitcode, out, err = execute(cmd)
|
||||||
|
|
||||||
|
self.assertEqual(0, exitcode)
|
||||||
|
self.assertTrue(out.strip().startswith('Deleted image'))
|
||||||
|
|
||||||
|
# 6. Verify no public images
|
||||||
|
cmd = "bin/glance --port=%d index" % api_port
|
||||||
|
|
||||||
|
exitcode, out, err = execute(cmd)
|
||||||
|
|
||||||
|
self.assertEqual(0, exitcode)
|
||||||
|
self.assertEqual('', out.strip())
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
@@ -2585,6 +2585,27 @@ class TestGlanceAPI(unittest.TestCase):
|
|||||||
res = req.get_response(self.api)
|
res = req.get_response(self.api)
|
||||||
self.assertEquals(res.status_int, 200)
|
self.assertEquals(res.status_int, 200)
|
||||||
|
|
||||||
|
def test_delete_protected_image(self):
|
||||||
|
fixture_headers = {'x-image-meta-store': 'file',
|
||||||
|
'x-image-meta-name': 'fake image #3',
|
||||||
|
'x-image-meta-protected': 'True'}
|
||||||
|
|
||||||
|
req = webob.Request.blank("/images")
|
||||||
|
req.method = 'POST'
|
||||||
|
for k, v in fixture_headers.iteritems():
|
||||||
|
req.headers[k] = v
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEquals(res.status_int, httplib.CREATED)
|
||||||
|
|
||||||
|
res_body = json.loads(res.body)['image']
|
||||||
|
self.assertEquals('queued', res_body['status'])
|
||||||
|
|
||||||
|
# Now try to delete the image...
|
||||||
|
req = webob.Request.blank("/images/%s" % res_body['id'])
|
||||||
|
req.method = 'DELETE'
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
self.assertEquals(res.status_int, httplib.FORBIDDEN)
|
||||||
|
|
||||||
def test_get_details_invalid_marker(self):
|
def test_get_details_invalid_marker(self):
|
||||||
"""
|
"""
|
||||||
Tests that the /images/detail registry API returns a 400
|
Tests that the /images/detail registry API returns a 400
|
||||||
|
Reference in New Issue
Block a user