
For image-updae and image-create commands, glanceclient attempts to determine whether image data should be uploaded based on the presence of data on stdin. Unforunately it is difficult to determine if data is available, especially when standard in is from a pipe. This is especially problematic for update operations, where data must only be uploaded if the image is in queued state. For example data may be uploaded when the user only wants to rename an image, but the rename will be rejected because data cannot be uploaded to an unqueued image. This patch removes the check that attempts to determine if data is available to read as it didn't work for pipes. It also re-introduces a check for image state in the update operation, so that glanceclient only attempts to read data if the image being updated is in queued state. The image state check is part of the original patchset that was removed so the patchset could have a single focus [1] This patch also removes a test for handling empty stdin, and adds a test for reading stdin from a pipe. [1] https://review.openstack.org/#/c/27536/3/glanceclient/v1/shell.py Fixes: bug 1184566 Related to: bug 1173044 Change-Id: I8d37f6412a0bf9ca21cbd75cde6a4d5a174e5545
439 lines
16 KiB
Python
439 lines
16 KiB
Python
# Copyright 2013 OpenStack LLC.
|
|
# Copyright (C) 2013 Yahoo! Inc.
|
|
# 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.
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import testtools
|
|
|
|
from glanceclient import exc
|
|
from glanceclient import shell
|
|
|
|
import glanceclient.v1.client as client
|
|
import glanceclient.v1.images
|
|
import glanceclient.v1.shell as v1shell
|
|
|
|
from tests import utils
|
|
|
|
fixtures = {
|
|
'/v1/images/96d2c7e1-de4e-4612-8aa2-ba26610c804e': {
|
|
'PUT': (
|
|
{
|
|
'Location': 'http://fakeaddress.com:9292/v1/images/'
|
|
'96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
|
'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'X-Openstack-Request-Id':
|
|
'req-b645039d-e1c7-43e5-b27b-2d18a173c42b',
|
|
'Date': 'Mon, 29 Apr 2013 10:24:32 GMT'
|
|
},
|
|
json.dumps({
|
|
'image': {
|
|
'status': 'active', 'name': 'testimagerename',
|
|
'deleted': False,
|
|
'container_format': 'ami',
|
|
'created_at': '2013-04-25T15:47:43',
|
|
'disk_format': 'ami',
|
|
'updated_at': '2013-04-29T10:24:32',
|
|
'id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
|
'min_disk': 0,
|
|
'protected': False,
|
|
'min_ram': 0,
|
|
'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'owner': '1310db0cce8f40b0987a5acbe139765a',
|
|
'is_public': True,
|
|
'deleted_at': None,
|
|
'properties': {
|
|
'kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
|
'ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114'
|
|
},
|
|
'size': 25165824
|
|
}
|
|
})
|
|
),
|
|
'HEAD': (
|
|
{
|
|
'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
|
'x-image-meta-status': 'active'
|
|
},
|
|
None
|
|
),
|
|
'GET': (
|
|
{
|
|
'x-image-meta-status': 'active',
|
|
'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a',
|
|
'x-image-meta-name': 'cirros-0.3.1-x86_64-uec',
|
|
'x-image-meta-container_format': 'ami',
|
|
'x-image-meta-created_at': '2013-04-25T15:47:43',
|
|
'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'location': 'http://fakeaddress.com:9292/v1/images/'
|
|
'96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
|
'x-image-meta-min_ram': '0',
|
|
'x-image-meta-updated_at': '2013-04-25T15:47:43',
|
|
'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
|
'x-image-meta-property-ramdisk_id':
|
|
'b759bee9-0669-4394-a05c-fa2529b1c114',
|
|
'date': 'Mon, 29 Apr 2013 09:25:17 GMT',
|
|
'x-image-meta-property-kernel_id':
|
|
'1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
|
'x-openstack-request-id':
|
|
'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f',
|
|
'x-image-meta-deleted': 'False',
|
|
'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'x-image-meta-protected': 'False',
|
|
'x-image-meta-min_disk': '0',
|
|
'x-image-meta-size': '25165824',
|
|
'x-image-meta-is_public': 'True',
|
|
'content-type': 'text/html; charset=UTF-8',
|
|
'x-image-meta-disk_format': 'ami',
|
|
},
|
|
None
|
|
)
|
|
},
|
|
'/v1/images/44d2c7e1-de4e-4612-8aa2-ba26610c444f': {
|
|
'PUT': (
|
|
{
|
|
'Location': 'http://fakeaddress.com:9292/v1/images/'
|
|
'44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
|
'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'X-Openstack-Request-Id':
|
|
'req-b645039d-e1c7-43e5-b27b-2d18a173c42b',
|
|
'Date': 'Mon, 29 Apr 2013 10:24:32 GMT'
|
|
},
|
|
json.dumps({
|
|
'image': {
|
|
'status': 'queued', 'name': 'testimagerename',
|
|
'deleted': False,
|
|
'container_format': 'ami',
|
|
'created_at': '2013-04-25T15:47:43',
|
|
'disk_format': 'ami',
|
|
'updated_at': '2013-04-29T10:24:32',
|
|
'id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
|
'min_disk': 0,
|
|
'protected': False,
|
|
'min_ram': 0,
|
|
'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'owner': '1310db0cce8f40b0987a5acbe139765a',
|
|
'is_public': True,
|
|
'deleted_at': None,
|
|
'properties': {
|
|
'kernel_id':
|
|
'1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
|
'ramdisk_id':
|
|
'b759bee9-0669-4394-a05c-fa2529b1c114'
|
|
},
|
|
'size': 25165824
|
|
}
|
|
})
|
|
),
|
|
'HEAD': (
|
|
{
|
|
'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
|
'x-image-meta-status': 'queued'
|
|
},
|
|
None
|
|
),
|
|
'GET': (
|
|
{
|
|
'x-image-meta-status': 'queued',
|
|
'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a',
|
|
'x-image-meta-name': 'cirros-0.3.1-x86_64-uec',
|
|
'x-image-meta-container_format': 'ami',
|
|
'x-image-meta-created_at': '2013-04-25T15:47:43',
|
|
'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'location': 'http://fakeaddress.com:9292/v1/images/'
|
|
'44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
|
'x-image-meta-min_ram': '0',
|
|
'x-image-meta-updated_at': '2013-04-25T15:47:43',
|
|
'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
|
'x-image-meta-property-ramdisk_id':
|
|
'b759bee9-0669-4394-a05c-fa2529b1c114',
|
|
'date': 'Mon, 29 Apr 2013 09:25:17 GMT',
|
|
'x-image-meta-property-kernel_id':
|
|
'1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
|
'x-openstack-request-id':
|
|
'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f',
|
|
'x-image-meta-deleted': 'False',
|
|
'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
|
'x-image-meta-protected': 'False',
|
|
'x-image-meta-min_disk': '0',
|
|
'x-image-meta-size': '25165824',
|
|
'x-image-meta-is_public': 'True',
|
|
'content-type': 'text/html; charset=UTF-8',
|
|
'x-image-meta-disk_format': 'ami',
|
|
},
|
|
None
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
class ShellInvalidEndpointTest(utils.TestCase):
|
|
|
|
# Patch os.environ to avoid required auth info.
|
|
def setUp(self):
|
|
"""Run before each test."""
|
|
super(ShellInvalidEndpointTest, self).setUp()
|
|
self.old_environment = os.environ.copy()
|
|
os.environ = {
|
|
'OS_USERNAME': 'username',
|
|
'OS_PASSWORD': 'password',
|
|
'OS_TENANT_ID': 'tenant_id',
|
|
'OS_TOKEN_ID': 'test',
|
|
'OS_AUTH_URL': 'http://127.0.0.1:5000/v2.0/',
|
|
'OS_AUTH_TOKEN': 'pass',
|
|
'OS_IMAGE_API_VERSION': '1',
|
|
'OS_REGION_NAME': 'test',
|
|
'OS_IMAGE_URL': 'http://no.where'}
|
|
|
|
self.shell = shell.OpenStackImagesShell()
|
|
|
|
def tearDown(self):
|
|
super(ShellInvalidEndpointTest, self).tearDown()
|
|
os.environ = self.old_environment
|
|
|
|
def run_command(self, cmd):
|
|
self.shell.main(cmd.split())
|
|
|
|
def assert_called(self, method, url, body=None, **kwargs):
|
|
return self.shell.cs.assert_called(method, url, body, **kwargs)
|
|
|
|
def assert_called_anytime(self, method, url, body=None):
|
|
return self.shell.cs.assert_called_anytime(method, url, body)
|
|
|
|
def test_image_list_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint, self.run_command, 'image-list')
|
|
|
|
def test_image_details_invalid_endpoint_legacy(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint, self.run_command, 'details')
|
|
|
|
def test_image_update_invalid_endpoint_legacy(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'update {"name":""test}')
|
|
|
|
def test_image_index_invalid_endpoint_legacy(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'index')
|
|
|
|
def test_image_create_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'image-create')
|
|
|
|
def test_image_delete_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'image-delete <fake>')
|
|
|
|
def test_image_download_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'image-download <fake>')
|
|
|
|
def test_image_members_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'image-members fake_id')
|
|
|
|
def test_members_list_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'member-list --image-id fake')
|
|
|
|
def test_member_replace_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'members-replace image_id member_id')
|
|
|
|
def test_image_show_invalid_endpoint_legacy(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint, self.run_command, 'show image')
|
|
|
|
def test_image_show_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'image-show --human-readable <IMAGE_ID>')
|
|
|
|
def test_member_images_invalid_endpoint_legacy(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command, 'member-images member_id')
|
|
|
|
def test_member_create_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command,
|
|
'member-create --can-share <IMAGE_ID> <TENANT_ID>')
|
|
|
|
def test_member_delete_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command,
|
|
'member-delete <IMAGE_ID> <TENANT_ID>')
|
|
|
|
def test_member_add_invalid_endpoint(self):
|
|
self.assertRaises(
|
|
exc.InvalidEndpoint,
|
|
self.run_command,
|
|
'member-add <IMAGE_ID> <TENANT_ID>')
|
|
|
|
|
|
class ShellStdinHandlingTests(testtools.TestCase):
|
|
|
|
def _fake_update_func(self, *args, **kwargs):
|
|
''' Function to replace glanceclient.images.update,
|
|
to determine the parameters that would be supplied with the update
|
|
request
|
|
'''
|
|
|
|
# Store passed in args
|
|
self.collected_args = (args, kwargs)
|
|
|
|
# Return the first arg, which is an image,
|
|
# as do_image_update expects this.
|
|
return args[0]
|
|
|
|
def setUp(self):
|
|
super(ShellStdinHandlingTests, self).setUp()
|
|
self.api = utils.FakeAPI(fixtures)
|
|
self.gc = client.Client("http://fakeaddress.com")
|
|
self.gc.images = glanceclient.v1.images.ImageManager(self.api)
|
|
|
|
# Store real stdin, so it can be restored in tearDown.
|
|
self.real_sys_stdin_fd = os.dup(0)
|
|
|
|
# Replace stdin with a FD that points to /dev/null.
|
|
dev_null = open('/dev/null')
|
|
self.dev_null_fd = dev_null.fileno()
|
|
os.dup2(dev_null.fileno(), 0)
|
|
|
|
# Replace the image update function with a fake,
|
|
# so that we can tell if the data field was set correctly.
|
|
self.real_update_func = self.gc.images.update
|
|
self.collected_args = []
|
|
self.gc.images.update = self._fake_update_func
|
|
|
|
def tearDown(self):
|
|
"""Restore stdin and gc.images.update to their pretest states."""
|
|
super(ShellStdinHandlingTests, self).tearDown()
|
|
|
|
def try_close(fd):
|
|
try:
|
|
os.close(fd)
|
|
except OSError:
|
|
# Already closed
|
|
pass
|
|
|
|
# Restore stdin
|
|
os.dup2(self.real_sys_stdin_fd, 0)
|
|
|
|
# Close duplicate stdin handle
|
|
try_close(self.real_sys_stdin_fd)
|
|
|
|
# Close /dev/null handle
|
|
try_close(self.dev_null_fd)
|
|
|
|
# Restore the real image update function
|
|
self.gc.images.update = self.real_update_func
|
|
|
|
def _do_update(self, image='96d2c7e1-de4e-4612-8aa2-ba26610c804e'):
|
|
"""call v1/shell's do_image_update function"""
|
|
|
|
v1shell.do_image_update(
|
|
self.gc, argparse.Namespace(
|
|
image=image,
|
|
name='testimagerename',
|
|
property={},
|
|
purge_props=False,
|
|
human_readable=False,
|
|
file=None
|
|
)
|
|
)
|
|
|
|
def test_image_update_closed_stdin(self):
|
|
"""Supply glanceclient with a closed stdin, and perform an image
|
|
update to an active image. Glanceclient should not attempt to read
|
|
stdin.
|
|
"""
|
|
|
|
# NOTE(hughsaunders) Close stdin, which is repointed to /dev/null by
|
|
# setUp()
|
|
os.close(0)
|
|
|
|
self._do_update()
|
|
|
|
self.assertTrue(
|
|
'data' not in self.collected_args[1]
|
|
or self.collected_args[1]['data'] is None
|
|
)
|
|
|
|
def test_image_update_data_is_read_from_file(self):
|
|
"""Ensure that data is read from a file."""
|
|
|
|
try:
|
|
|
|
# NOTE(hughsaunders) Create a tmpfile, write some data to it and
|
|
# set it as stdin
|
|
f = open(tempfile.mktemp(), 'w+')
|
|
f.write('Some Data')
|
|
f.flush()
|
|
f.seek(0)
|
|
os.dup2(f.fileno(), 0)
|
|
|
|
self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f')
|
|
|
|
self.assertTrue('data' in self.collected_args[1])
|
|
self.assertIsInstance(self.collected_args[1]['data'], file)
|
|
self.assertEqual(self.collected_args[1]['data'].read(),
|
|
'Some Data')
|
|
|
|
finally:
|
|
try:
|
|
f.close()
|
|
os.remove(f.name)
|
|
except:
|
|
pass
|
|
|
|
def test_image_update_data_is_read_from_pipe(self):
|
|
"""Ensure that data is read from a pipe."""
|
|
|
|
try:
|
|
|
|
# NOTE(hughsaunders): Setup a pipe, duplicate it to stdin
|
|
# ensure it is read.
|
|
process = subprocess.Popen(['/bin/echo', 'Some Data'],
|
|
stdout=subprocess.PIPE)
|
|
os.dup2(process.stdout.fileno(), 0)
|
|
|
|
self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f')
|
|
|
|
self.assertTrue('data' in self.collected_args[1])
|
|
self.assertIsInstance(self.collected_args[1]['data'], file)
|
|
self.assertEqual(self.collected_args[1]['data'].read(),
|
|
'Some Data\n')
|
|
|
|
finally:
|
|
try:
|
|
process.stdout.close()
|
|
except:
|
|
pass
|