Adds support for protecting images from accidental deletion.
Implements blueprint protected-images. A new attribute 'protected' is now available to prevent an image from being deleted. If the image is protected a HTTP Forbidden (403) error is returned. The attribute can be changed via an update of the image. Change-Id: I72ba1c6499065441dc73e5ec2fa873d76c7f60d1
This commit is contained in:
parent
1239f61c2b
commit
391dd0e211
11
bin/glance
11
bin/glance
@ -131,6 +131,7 @@ def print_image_formatted(client, image):
|
||||
image['id'])
|
||||
print "Id: %s" % image['id']
|
||||
print "Public: " + (image['is_public'] and "Yes" or "No")
|
||||
print "Protected: " + (image['protected'] and "Yes" or "No")
|
||||
print "Name: %s" % image['name']
|
||||
print "Status: %s" % image['status']
|
||||
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
|
||||
and sets or unsets the image's availability to the public.
|
||||
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',
|
||||
and 'ami'. Default value is 'raw'.
|
||||
container_format Optional. Possible values are 'ovf' and 'ami'.
|
||||
@ -212,6 +216,8 @@ EXAMPLES
|
||||
image_meta = {'name': fields.pop('name'),
|
||||
'is_public': utils.bool_from_string(
|
||||
fields.pop('is_public', False)),
|
||||
'protected': utils.bool_from_string(
|
||||
fields.pop('protected', False)),
|
||||
'disk_format': fields.pop('disk_format', 'raw'),
|
||||
'min_disk': fields.pop('min_disk', 0),
|
||||
'min_ram': fields.pop('min_ram', 0),
|
||||
@ -298,6 +304,8 @@ name A name for the image.
|
||||
location The location of the image.
|
||||
is_public If specified, interpreted as a boolean value
|
||||
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
|
||||
container_format Format of the container
|
||||
|
||||
@ -338,6 +346,9 @@ to spell field names correctly. :)"""
|
||||
if 'is_public' in fields:
|
||||
image_meta['is_public'] = utils.bool_from_string(
|
||||
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
|
||||
image_meta['properties'] = fields
|
||||
|
@ -17,6 +17,6 @@
|
||||
|
||||
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
||||
'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')
|
||||
|
@ -567,6 +567,11 @@ class Controller(controller.BaseController):
|
||||
content_type="text/plain")
|
||||
|
||||
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
|
||||
# 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
|
||||
if 'size' in result:
|
||||
result['size'] = int(result['size'])
|
||||
if 'is_public' in result:
|
||||
result['is_public'] = bool_from_header_value(result['is_public'])
|
||||
if 'deleted' in result:
|
||||
result['deleted'] = bool_from_header_value(result['deleted'])
|
||||
for key in ('is_public', 'deleted', 'protected'):
|
||||
if key in result:
|
||||
result[key] = bool_from_header_value(result[key])
|
||||
return result
|
||||
|
||||
|
||||
|
@ -38,7 +38,7 @@ DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size',
|
||||
|
||||
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
|
||||
'min_ram', 'min_disk', 'size_min', 'size_max',
|
||||
'changes-since']
|
||||
'changes-since', 'protected']
|
||||
|
||||
SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format',
|
||||
'size', 'id', 'created_at', 'updated_at')
|
||||
@ -170,6 +170,14 @@ class Controller(object):
|
||||
except ValueError:
|
||||
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'
|
||||
if req.context.is_admin:
|
||||
deleted_filter = self._parse_deleted_filter(req)
|
||||
@ -226,6 +234,15 @@ class Controller(object):
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
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):
|
||||
"""Parse is_public into something usable."""
|
||||
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
|
||||
# public images.
|
||||
return True
|
||||
is_public = is_public.lower()
|
||||
if is_public == 'none':
|
||||
elif is_public.lower() == 'none':
|
||||
return None
|
||||
elif is_public == 'true' or is_public == '1':
|
||||
return True
|
||||
elif is_public == 'false' or is_public == '0':
|
||||
return False
|
||||
else:
|
||||
raise exc.HTTPBadRequest(_("is_public must be None, True, "
|
||||
"or False"))
|
||||
|
||||
value = self._get_bool(is_public)
|
||||
if value is None:
|
||||
raise exc.HTTPBadRequest(_("is_public must be None, True, or "
|
||||
"False"))
|
||||
|
||||
return value
|
||||
|
||||
def _parse_deleted_filter(self, req):
|
||||
"""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',
|
||||
'disk_format', 'container_format',
|
||||
'min_disk', 'min_ram', 'is_public',
|
||||
'location', 'checksum', 'owner'])
|
||||
'location', 'checksum', 'owner',
|
||||
'protected'])
|
||||
|
||||
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
|
||||
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi',
|
||||
@ -343,6 +344,7 @@ def _image_update(context, values, image_id, purge_props=False):
|
||||
values['min_disk'] = int(values['min_disk'] or 0)
|
||||
|
||||
values['is_public'] = bool(values.get('is_public', False))
|
||||
values['protected'] = bool(values.get('protected', False))
|
||||
image_ref = models.Image()
|
||||
|
||||
# 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_ram = Column(Integer(), nullable=False, default=0)
|
||||
owner = Column(String(255))
|
||||
protected = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
class ImageProperty(BASE, ModelBase):
|
||||
|
@ -667,6 +667,7 @@ class TestApi(functional.FunctionalTest):
|
||||
'X-Image-Meta-Disk-Format': 'vdi',
|
||||
'X-Image-Meta-Size': '19',
|
||||
'X-Image-Meta-Is-Public': 'True',
|
||||
'X-Image-Meta-Protected': 'True',
|
||||
'X-Image-Meta-Property-pants': 'are on'}
|
||||
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||
http = httplib2.Http()
|
||||
@ -684,6 +685,7 @@ class TestApi(functional.FunctionalTest):
|
||||
'X-Image-Meta-Disk-Format': 'vhd',
|
||||
'X-Image-Meta-Size': '20',
|
||||
'X-Image-Meta-Is-Public': 'True',
|
||||
'X-Image-Meta-Protected': 'False',
|
||||
'X-Image-Meta-Property-pants': 'are on'}
|
||||
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||
http = httplib2.Http()
|
||||
@ -701,6 +703,7 @@ class TestApi(functional.FunctionalTest):
|
||||
'X-Image-Meta-Disk-Format': 'ami',
|
||||
'X-Image-Meta-Size': '21',
|
||||
'X-Image-Meta-Is-Public': 'True',
|
||||
'X-Image-Meta-Protected': 'False',
|
||||
'X-Image-Meta-Property-pants': 'are off'}
|
||||
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
|
||||
http = httplib2.Http()
|
||||
@ -717,7 +720,8 @@ class TestApi(functional.FunctionalTest):
|
||||
'X-Image-Meta-Container-Format': 'ami',
|
||||
'X-Image-Meta-Disk-Format': 'ami',
|
||||
'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)
|
||||
http = httplib2.Http()
|
||||
response, content = http.request(path, 'POST', headers=headers)
|
||||
@ -851,7 +855,31 @@ class TestApi(functional.FunctionalTest):
|
||||
for image in data['images']:
|
||||
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
|
||||
params = "property-pants=are%20on"
|
||||
path = "http://%s:%d/v1/images/detail?%s" % (
|
||||
@ -863,7 +891,7 @@ class TestApi(functional.FunctionalTest):
|
||||
for image in data['images']:
|
||||
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
|
||||
# Make sure you quote the url when using more than one param!
|
||||
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['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)
|
||||
iso1 = utils.isotime(dt1)
|
||||
params = "changes-since=%s" % iso1
|
||||
@ -887,7 +915,7 @@ class TestApi(functional.FunctionalTest):
|
||||
data = json.loads(content)
|
||||
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)
|
||||
iso2 = utils.isotime(dt2)
|
||||
params = "changes-since=%s" % iso2
|
||||
@ -897,14 +925,6 @@ class TestApi(functional.FunctionalTest):
|
||||
data = json.loads(content)
|
||||
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()
|
||||
|
||||
@skip_if_disabled
|
||||
|
@ -422,26 +422,26 @@ class TestBinGlance(functional.FunctionalTest):
|
||||
|
||||
self.assertEqual(0, exitcode)
|
||||
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[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"
|
||||
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
|
||||
|
||||
self.assertEqual(0, exitcode)
|
||||
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])
|
||||
|
||||
# 11. Check min_disk filter
|
||||
# 13. Check min_disk filter
|
||||
cmd = "min_disk=7"
|
||||
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
|
||||
|
||||
self.assertEqual(0, exitcode)
|
||||
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.stop_servers()
|
||||
@ -524,9 +524,9 @@ class TestBinGlance(functional.FunctionalTest):
|
||||
|
||||
self.assertEqual(0, exitcode)
|
||||
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[12].split()[1], image_ids[1])
|
||||
self.assertTrue(image_lines[13].split()[1], image_ids[1])
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
@ -600,10 +600,10 @@ class TestBinGlance(functional.FunctionalTest):
|
||||
|
||||
self.assertEqual(0, exitcode)
|
||||
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[12].split()[1], image_ids[1])
|
||||
self.assertTrue(image_lines[23].split()[1], image_ids[4])
|
||||
self.assertTrue(image_lines[13].split()[1], image_ids[1])
|
||||
self.assertTrue(image_lines[25].split()[1], image_ids[4])
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
@ -658,4 +658,88 @@ class TestBinGlance(functional.FunctionalTest):
|
||||
self.assertEqual(0, exitcode)
|
||||
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()
|
||||
|
@ -2586,6 +2586,27 @@ class TestGlanceAPI(unittest.TestCase):
|
||||
res = req.get_response(self.api)
|
||||
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):
|
||||
"""
|
||||
Tests that the /images/detail registry API returns a 400
|
||||
|
Loading…
Reference in New Issue
Block a user