diff -Nru glance-2012.1~e4~20120224.1290/bin/glance glance-2012.1~e4/bin/glance --- glance-2012.1~e4~20120224.1290/bin/glance 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/bin/glance 2012-03-01 13:45:51.000000000 +0000 @@ -182,6 +182,12 @@ filesystem at /usr/share/images/some.image.tar.gz you would specify: location=file:///usr/share/images/some.image.tar.gz +copy_from Optional. An external location (HTTP, S3 or Swift URI) to + copy image content from. For example, if the image data is + stored as an object called fedora16 in an S3 bucket named + images, you would specify (with the approriate access and + secret keys): + copy_from=s3://akey:skey@s3.amazonaws.com/images/fedora16 Any other field names are considered to be custom properties so be careful to spell field names correctly. :) @@ -241,6 +247,8 @@ if 'location' in fields.keys(): source = fields.pop('location') image_meta['location'] = source + if 'checksum' in fields.keys(): + image_meta['checksum'] = fields.pop('checksum') elif 'copy_from' in fields.keys(): source = fields.pop('copy_from') features['x-glance-api-copy-from'] = source @@ -317,7 +325,8 @@ name A name for the image. location An external location to serve out from. -copy_from An external location (HTTP, S3 or Swift URI) to copy from. +copy_from An external location (HTTP, S3 or Swift URI) to copy image + content from. 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 @@ -351,6 +360,11 @@ print 'Found non-modifiable field %s. Removing.' % field fields.pop(field) + features = {} + if 'location' not in fields and 'copy_from' in fields: + source = fields.pop('copy_from') + features['x-glance-api-copy-from'] = source + base_image_fields = ['disk_format', 'container_format', 'name', 'min_disk', 'min_ram', 'location', 'owner'] for field in base_image_fields: @@ -371,7 +385,8 @@ if not options.dry_run: try: - image_meta = c.update_image(image_id, image_meta=image_meta) + image_meta = c.update_image(image_id, image_meta=image_meta, + features=features) print "Updated image %s" % image_id if options.verbose: @@ -380,6 +395,9 @@ except exception.NotFound: print "No image with ID %s was found" % image_id return FAILURE + except exception.NotAuthorized: + print "You do not have permission to update image %s" % image_id + return FAILURE except Exception, e: print "Failed to update image. Got error:" pieces = unicode(e).split('\n') @@ -387,10 +405,18 @@ print piece return FAILURE else: + def _dump(dict): + for k, v in sorted(dict.items()): + print " %(k)30s => %(v)s" % locals() + print "Dry run. We would have done the following:" print "Update existing image with metadata:" - for k, v in sorted(image_meta.items()): - print " %(k)30s => %(v)s" % locals() + _dump(image_meta) + + if features: + print "with features enabled:" + _dump(features) + return SUCCESS @@ -420,6 +446,9 @@ except exception.NotFound: print "No image with ID %s was found" % image_id return FAILURE + except exception.NotAuthorized: + print "You do not have permission to delete image %s" % image_id + return FAILURE def image_show(options, args): @@ -769,7 +798,9 @@ os.getenv('OS_TENANT_NAME')), auth_url=options.auth_url or os.getenv('OS_AUTH_URL'), strategy=force_strategy or options.auth_strategy or \ - os.getenv('OS_AUTH_STRATEGY', 'noauth')) + os.getenv('OS_AUTH_STRATEGY', 'noauth'), + region=options.region or os.getenv('OS_REGION_NAME'), + ) if creds['strategy'] == 'keystone' and not creds['auth_url']: msg = ("--auth_url option or OS_AUTH_URL environment variable " @@ -836,6 +867,13 @@ parser.add_option('-K', '--password', dest="password", metavar="PASSWORD", default=None, help="Password used to acquire an authentication token") + parser.add_option('-R', '--region', dest="region", + metavar="REGION", default=None, + help="Region name. When using keystone authentication " + "version 2.0 or later this identifies the region " + "name to use when selecting the service endpoint. A " + "region name must be provided if more than one " + "region endpoint is available") parser.add_option('-T', '--tenant', dest="tenant", metavar="TENANT", default=None, help="Tenant name") diff -Nru glance-2012.1~e4~20120224.1290/debian/changelog glance-2012.1~e4/debian/changelog --- glance-2012.1~e4~20120224.1290/debian/changelog 2012-02-24 16:21:44.000000000 +0000 +++ glance-2012.1~e4/debian/changelog 2012-03-06 14:30:56.000000000 +0000 @@ -1,3 +1,12 @@ +glance (2012.1~e4-0ubuntu1) precise; urgency=low + + * New upstream release. + * debian/control: Add python date-util. (LP: #943748) + * debian/control: Add ca-certificates. (LP: #932800) + * debian/control: Add python-iso8601 to fix testsuite failures. + + -- Chuck Short Tue, 06 Mar 2012 09:30:41 -0500 + glance (2012.1~e4~20120224.1290-0ubuntu1) precise; urgency=low [ Adam Gandelman ] @@ -6,7 +15,7 @@ [ Chuck Short ] * New upstream release. - -- Chuck Short Fri, 24 Feb 2012 11:21:25 -0500 + -- Chuck Short Thu, 01 Mar 2012 09:04:54 -0500 glance (2012.1~e4~20120217.1275-0ubuntu1) precise; urgency=low diff -Nru glance-2012.1~e4~20120224.1290/debian/control glance-2012.1~e4/debian/control --- glance-2012.1~e4~20120224.1290/debian/control 2012-02-24 16:21:44.000000000 +0000 +++ glance-2012.1~e4/debian/control 2012-03-06 14:30:56.000000000 +0000 @@ -23,6 +23,8 @@ python-boto, python-crypto, python-kombu, + python-dateutil, + python-iso8601, curl, pep8 Standards-Version: 3.9.2 @@ -41,6 +43,7 @@ python-kombu, python-httplib2, python-iso8601, + python-dateutil, python-crypto Provides: ${python:Provides} XB-Python-Version: ${python:Versions} @@ -65,6 +68,7 @@ python-xattr, glance-api (= ${source:Version}), glance-registry (= ${source:Version}), + ca-certificates, adduser Description: OpenStack Image Registry and Delivery Service - Daemons The Glance project provides an image registration and discovery service diff -Nru glance-2012.1~e4~20120224.1290/debian/watch glance-2012.1~e4/debian/watch --- glance-2012.1~e4~20120224.1290/debian/watch 2012-02-24 16:21:44.000000000 +0000 +++ glance-2012.1~e4/debian/watch 2012-03-06 14:30:56.000000000 +0000 @@ -1,4 +1,2 @@ version=3 -http://launchpad.net/glance/+download http://launchpad.net/glance/.*/glance-(.*)\.tar\.gz -http://glance.openstack.org/tarballs/ glance-(.*).tar.gz - +https://launchpad.net/glance/+download http://launchpad.net/glance/.*/glance-(.*)\.tar\.gz diff -Nru glance-2012.1~e4~20120224.1290/doc/source/glance.rst glance-2012.1~e4/doc/source/glance.rst --- glance-2012.1~e4~20120224.1290/doc/source/glance.rst 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/doc/source/glance.rst 2012-03-01 13:45:51.000000000 +0000 @@ -104,6 +104,12 @@ requests. The server certificate will not be verified against any certificate authorities. This option should be used with caution. + -R REGION, --region=REGION + When using keystone authentication version 2.0 + or later this identifies the region name to + use when selecting the service endpoint. Where + more than one region endpoint is available a + region must be provided. --limit=LIMIT Page size to use while requesting image metadata --marker=MARKER Image index after which to begin pagination --sort_key=KEY Sort results by this image attribute. diff -Nru glance-2012.1~e4~20120224.1290/doc/source/man/glance.rst glance-2012.1~e4/doc/source/man/glance.rst --- glance-2012.1~e4~20120224.1290/doc/source/man/glance.rst 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/doc/source/man/glance.rst 2012-03-01 13:45:51.000000000 +0000 @@ -99,6 +99,13 @@ The server certificate will not be verified against any certificate authorities. This option should be used with caution. + **-R REGION, --region=REGION** + When using keystone authentication version 2.0 or later this + identifies the region name to use when selecting the service + endpoint. If no region is specified and only one region is + available that region will be used by default. Where more than + one region endpoint is available a region must be provided. + **-A TOKEN, --auth_token=TOKEN** Authentication token to use to identify the client to the glance server diff -Nru glance-2012.1~e4~20120224.1290/glance/api/v1/images.py glance-2012.1~e4/glance/api/v1/images.py --- glance-2012.1~e4~20120224.1290/glance/api/v1/images.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/api/v1/images.py 2012-03-01 13:45:51.000000000 +0000 @@ -227,12 +227,29 @@ } @staticmethod + def _validate_source(source, req): + """ + External sources (as specified via the location or copy-from headers) + are supported only over non-local store types, i.e. S3, Swift, HTTP. + Note the absence of file:// for security reasons, see LP bug #942118. + If the above constraint is violated, we reject with 400 "Bad Request". + """ + if source: + for scheme in ['s3', 'swift', 'http']: + if source.lower().startswith(scheme): + return source + msg = _("External sourcing not supported for store %s") % source + logger.error(msg) + raise HTTPBadRequest(msg, request=req, content_type="text/plain") + + @staticmethod def _copy_from(req): return req.headers.get('x-glance-api-copy-from') @staticmethod def _external_source(image_meta, req): - return image_meta.get('location', Controller._copy_from(req)) + source = image_meta.get('location', Controller._copy_from(req)) + return Controller._validate_source(source, req) @staticmethod def _get_from_store(where): @@ -286,8 +303,7 @@ self.get_store_or_400(req, store) # retrieve the image size from remote store (if not provided) - image_meta['size'] = image_meta.get('size', 0) \ - or get_size_from_backend(location) + image_meta['size'] = self._get_size(image_meta, location) else: # Ensure that the size attribute is set to zero for directly # uploadable images (if not provided). The size will be set @@ -523,6 +539,19 @@ location = self._upload(req, image_meta) return self._activate(req, image_id, location) + def _get_size(self, image_meta, location): + # retrieve the image size from remote store (if not provided) + return image_meta.get('size', 0) or get_size_from_backend(location) + + def _handle_source(self, req, image_id, image_meta, image_data): + if image_data or self._copy_from(req): + image_meta = self._upload_and_activate(req, image_meta) + else: + location = image_meta.get('location') + if location: + image_meta = self._activate(req, image_id, location) + return image_meta + def create(self, req, image_meta, image_data): """ Adds a new image to Glance. Four scenarios exist when creating an @@ -573,14 +602,9 @@ content_type="text/plain") image_meta = self._reserve(req, image_meta) - image_id = image_meta['id'] + id = image_meta['id'] - if image_data or self._copy_from(req): - image_meta = self._upload_and_activate(req, image_meta) - else: - location = image_meta.get('location') - if location: - image_meta = self._activate(req, image_id, location) + image_meta = self._handle_source(req, id, image_meta, image_data) # Prevent client from learning the location, as it # could contain security credentials @@ -627,17 +651,26 @@ # POST /images but originally supply neither a Location|Copy-From # field NOR image data location = self._external_source(image_meta, req) - if not orig_status == 'queued' and location: + reactivating = orig_status != 'queued' and location + activating = orig_status == 'queued' and (location or image_data) + + if reactivating: msg = _("Attempted to update Location field for an image " "not in queued status.") raise HTTPBadRequest(msg, request=req, content_type="text/plain") try: - image_meta = registry.update_image_metadata(req.context, id, + if location: + image_meta['size'] = self._get_size(image_meta, location) + + image_meta = registry.update_image_metadata(req.context, + id, image_meta, purge_props) - if image_data is not None: - image_meta = self._upload_and_activate(req, image_meta) + + if activating: + image_meta = self._handle_source(req, id, image_meta, + image_data) except exception.Invalid, e: msg = (_("Failed to update image metadata. Got error: %(e)s") % locals()) @@ -651,6 +684,12 @@ logger.info(line) self.notifier.info('image.update', msg) raise HTTPNotFound(msg, request=req, content_type="text/plain") + except exception.NotAuthorized, e: + msg = ("Unable to update image: %(e)s" % locals()) + for line in msg.split('\n'): + logger.info(line) + self.notifier.info('image.update', msg) + raise HTTPForbidden(msg, request=req, content_type="text/plain") else: self.notifier.info('image.update', image_meta) @@ -701,6 +740,12 @@ logger.info(line) self.notifier.info('image.delete', msg) raise HTTPNotFound(msg, request=req, content_type="text/plain") + except exception.NotAuthorized, e: + msg = ("Unable to delete image: %(e)s" % locals()) + for line in msg.split('\n'): + logger.info(line) + self.notifier.info('image.delete', msg) + raise HTTPForbidden(msg, request=req, content_type="text/plain") else: self.notifier.info('image.delete', id) diff -Nru glance-2012.1~e4~20120224.1290/glance/client.py glance-2012.1~e4/glance/client.py --- glance-2012.1~e4~20120224.1290/glance/client.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/client.py 2012-03-01 13:45:51.000000000 +0000 @@ -140,7 +140,7 @@ :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 + :param features: Optional map of features :retval The newly-stored image's metadata. """ @@ -162,9 +162,18 @@ data = json.loads(res.read()) return data['image'] - def update_image(self, image_id, image_meta=None, image_data=None): + 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 = {} @@ -181,6 +190,8 @@ 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'] diff -Nru glance-2012.1~e4~20120224.1290/glance/common/auth.py glance-2012.1~e4/glance/common/auth.py --- glance-2012.1~e4~20120224.1290/glance/common/auth.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/common/auth.py 2012-03-01 13:45:51.000000000 +0000 @@ -38,13 +38,9 @@ class BaseStrategy(object): - def __init__(self, creds): - self.creds = creds + def __init__(self): self.auth_token = None - - # TODO(sirp): For now we're just dealing with one endpoint, eventually - # this should expose the entire service catalog so that the client can - # choose which service/region/(public/private net) combo they want. + # TODO(sirp): Should expose selecting public/internal/admin URL. self.management_url = None def authenticate(self): @@ -54,6 +50,10 @@ def is_authenticated(self): raise NotImplementedError + @property + def strategy(self): + raise NotImplementedError + class NoAuthStrategy(BaseStrategy): def authenticate(self): @@ -63,10 +63,32 @@ def is_authenticated(self): return True + @property + def strategy(self): + return 'noauth' + class KeystoneStrategy(BaseStrategy): MAX_REDIRECTS = 10 + def __init__(self, creds): + self.creds = creds + super(KeystoneStrategy, self).__init__() + + def check_auth_params(self): + # Ensure that supplied credential parameters are as required + for required in ('username', 'password', 'auth_url', + 'strategy'): + if required not in self.creds: + raise exception.MissingCredentialError(required=required) + if self.creds['strategy'] != 'keystone': + raise exception.BadAuthStrategy(expected='keystone', + received=self.creds['strategy']) + # For v2.0 also check tenant is present + if self.creds['auth_url'].rstrip('/').endswith('v2.0'): + if 'tenant' not in self.creds: + raise exception.MissingCredentialError(required='tenant') + def authenticate(self): """Authenticate with the Keystone service. @@ -82,16 +104,6 @@ case, we rewrite the url to contain /v2.0/ and retry using the v2 protocol. """ - def check_auth_params(): - # Ensure that supplied credential parameters are as required - for required in ('username', 'password', 'auth_url'): - if required not in self.creds: - raise exception.MissingCredentialError(required=required) - # For v2.0 also check tenant is present - if self.creds['auth_url'].rstrip('/').endswith('v2.0'): - if 'tenant' not in self.creds: - raise exception.MissingCredentialError(required='tenant') - def _authenticate(auth_url): # If OS_AUTH_URL is missing a trailing slash add one if not auth_url.endswith('/'): @@ -104,7 +116,7 @@ else: self._v1_auth(token_url) - check_auth_params() + self.check_auth_params() auth_url = self.creds['auth_url'] for _ in range(self.MAX_REDIRECTS): try: @@ -168,6 +180,32 @@ raise Exception(_('Unexpected response: %s' % resp.status)) def _v2_auth(self, token_url): + def get_endpoint(service_catalog): + """ + Select an endpoint from the service catalog + + We search the full service catalog for services + matching both type and region. If the client + supplied no region then any 'image' endpoint + is considered a match. There must be one -- and + only one -- successful match in the catalog, + otherwise we will raise an exception. + """ + # FIXME(sirp): for now just use the public url. + endpoint = None + region = self.creds.get('region') + for service in service_catalog: + if service['type'] == 'image': + for ep in service['endpoints']: + if region is None or region == ep['region']: + if endpoint is not None: + # This is a second match, abort + raise exception.RegionAmbiguity(region=region) + endpoint = ep + if endpoint is None: + raise exception.NoServiceEndpoint() + return endpoint['publicURL'] + creds = self.creds creds = { @@ -189,17 +227,7 @@ if resp.status == 200: resp_auth = json.loads(resp_body)['access'] - - # FIXME(sirp): for now just using the first endpoint we get back - # from the service catalog for glance, and using the public url. - for service in resp_auth['serviceCatalog']: - if service['type'] == 'image': - glance_endpoint = service['endpoints'][0]['publicURL'] - break - else: - raise exception.NoServiceEndpoint() - - self.management_url = glance_endpoint + self.management_url = get_endpoint(resp_auth['serviceCatalog']) self.auth_token = resp_auth['token']['id'] elif resp.status == 305: raise exception.RedirectException(resp['location']) @@ -216,6 +244,10 @@ def is_authenticated(self): return self.auth_token is not None + @property + def strategy(self): + return 'keystone' + @staticmethod def _do_request(url, method, headers=None, body=None): headers = headers or {} @@ -226,10 +258,10 @@ return resp, resp_body -def get_plugin_from_strategy(strategy): +def get_plugin_from_strategy(strategy, creds=None): if strategy == 'noauth': - return NoAuthStrategy + return NoAuthStrategy() elif strategy == 'keystone': - return KeystoneStrategy + return KeystoneStrategy(creds) else: raise Exception(_("Unknown auth strategy '%s'") % strategy) diff -Nru glance-2012.1~e4~20120224.1290/glance/common/client.py glance-2012.1~e4/glance/common/client.py --- glance-2012.1~e4~20120224.1290/glance/common/client.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/common/client.py 2012-03-01 13:45:51.000000000 +0000 @@ -339,8 +339,7 @@ Returns an instantiated authentication plugin. """ strategy = creds.get('strategy', 'noauth') - plugin_class = auth.get_plugin_from_strategy(strategy) - plugin = plugin_class(creds) + plugin = auth.get_plugin_from_strategy(strategy, creds) return plugin def get_connection_type(self): diff -Nru glance-2012.1~e4~20120224.1290/glance/common/exception.py glance-2012.1~e4/glance/common/exception.py --- glance-2012.1~e4~20120224.1290/glance/common/exception.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/common/exception.py 2012-03-01 13:45:51.000000000 +0000 @@ -63,6 +63,11 @@ message = _("Missing required credential: %(required)s") +class BadAuthStrategy(GlanceException): + message = _("Incorrect auth strategy, expected \"%(expected)s\" but " + "received \"%(received)s\"") + + class NotFound(GlanceException): message = _("An object with the specified identifier was not found.") @@ -180,3 +185,9 @@ class NoServiceEndpoint(GlanceException): message = _("Response from Keystone does not contain a Glance endpoint.") + + +class RegionAmbiguity(GlanceException): + message = _("Multiple 'image' service matches for region %(region)s. This " + "generally means that a region is required and you have not " + "supplied one.") diff -Nru glance-2012.1~e4~20120224.1290/glance/registry/api/v1/images.py glance-2012.1~e4/glance/registry/api/v1/images.py --- glance-2012.1~e4~20120224.1290/glance/registry/api/v1/images.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/registry/api/v1/images.py 2012-03-01 13:45:51.000000000 +0000 @@ -397,13 +397,16 @@ raise exc.HTTPNotFound(body='Image not found', request=req, content_type='text/plain') + except exception.NotAuthorizedPublicImage: + msg = _("Access by %(user)s to update public image %(id)s denied") + logger.info(msg % {'user': req.context.user, 'id': id}) + raise exc.HTTPForbidden() + except exception.NotAuthorized: # If it's private and doesn't belong to them, don't let on # that it exists - msg = _("Access by %(user)s to image %(id)s " - "denied") % ({'user': req.context.user, - 'id': id}) - logger.info(msg) + msg = _("Access by %(user)s to update private image %(id)s denied") + logger.info(msg % {'user': req.context.user, 'id': id}) raise exc.HTTPNotFound(body='Image not found', request=req, content_type='text/plain') diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/functional/__init__.py glance-2012.1~e4/glance/tests/functional/__init__.py --- glance-2012.1~e4~20120224.1290/glance/tests/functional/__init__.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/functional/__init__.py 2012-03-01 13:45:51.000000000 +0000 @@ -92,7 +92,9 @@ def write_conf(self, **kwargs): """ Writes the configuration file for the server to its intended - destination. Returns the name of the configuration file. + destination. Returns the name of the configuration file and + the over-ridden config content (may be useful for populating + error messages). """ if self.conf_file_name: @@ -112,22 +114,24 @@ paste_conf_filepath = conf_filepath.replace(".conf", "-paste.ini") utils.safe_mkdirs(conf_dir) - def override_conf(filepath, base, override): + def override_conf(filepath, overridden): with open(filepath, 'wb') as conf_file: - conf_file.write(base % override) + conf_file.write(overridden) conf_file.flush() return conf_file.name - self.conf_file_name = override_conf(conf_filepath, - self.conf_base, - conf_override) + overridden_core = self.conf_base % conf_override + self.conf_file_name = override_conf(conf_filepath, overridden_core) + overridden_paste = '' if self.paste_conf_base: - override_conf(paste_conf_filepath, - self.paste_conf_base, - conf_override) + overridden_paste = self.paste_conf_base % conf_override + override_conf(paste_conf_filepath, overridden_paste) - return self.conf_file_name + overridden = ('==Core config==\n%s\n==Paste config==\n%s' % + (overridden_core, overridden_paste)) + + return self.conf_file_name, overridden def start(self, expect_exit=True, expected_exitcode=0, **kwargs): """ @@ -138,7 +142,7 @@ """ # Ensure the configuration file is written - self.write_conf(**kwargs) + overridden = self.write_conf(**kwargs)[1] cmd = ("%(server_control)s %(server_name)s start " "%(conf_file_name)s --pid-file=%(pid_file)s " @@ -148,7 +152,8 @@ no_venv=self.no_venv, exec_env=self.exec_env, expect_exit=expect_exit, - expected_exitcode=expected_exitcode) + expected_exitcode=expected_exitcode, + context=overridden) def stop(self): """ diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/functional/keystone_utils.py glance-2012.1~e4/glance/tests/functional/keystone_utils.py --- glance-2012.1~e4~20120224.1290/glance/tests/functional/keystone_utils.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/functional/keystone_utils.py 2012-03-01 13:45:51.000000000 +0000 @@ -245,7 +245,7 @@ super(KeystoneTests, self).start_servers(**kwargs) # Set up the data store - keystone_conf = self.auth_server.write_conf(**kwargs) + keystone_conf = self.auth_server.write_conf(**kwargs)[0] datafile = os.path.join(os.path.dirname(__file__), 'data', 'keystone_data.py') diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/functional/test_bin_glance.py glance-2012.1~e4/glance/tests/functional/test_bin_glance.py --- glance-2012.1~e4~20120224.1290/glance/tests/functional/test_bin_glance.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/functional/test_bin_glance.py 2012-03-01 13:45:51.000000000 +0000 @@ -18,6 +18,8 @@ """Functional test case that utilizes the bin/glance CLI tool""" import datetime +import httplib2 +import json import os import tempfile @@ -41,6 +43,10 @@ # NoAuth strategy. os.environ['OS_AUTH_STRATEGY'] = 'noauth' + def _assertStartsWith(self, str, prefix): + msg = 'expected "%s" to start with "%s"' % (str, prefix) + self.assertTrue(str.startswith(prefix), msg) + def test_add_with_location(self): self.cleanup() self.start_servers(**self.__dict__.copy()) @@ -122,8 +128,74 @@ [c.strip() for c in line.split()] self.assertEqual('MyImage', name) - self.assertEqual('5120', size, "Expected image to be 0 bytes in size," - " but got %s. " % size) + self.assertEqual('5120', size, "Expected image to be 5120 bytes " + " in size, but got %s. " % size) + + def _do_test_update_external_source(self, source): + self.cleanup() + self.start_servers(**self.__dict__.copy()) + + api_port = self.api_port + registry_port = self.registry_port + + # 1. Add public image with no image content + headers = {'X-Image-Meta-Name': 'MyImage', + 'X-Image-Meta-disk_format': 'raw', + 'X-Image-Meta-container_format': 'ovf', + 'X-Image-Meta-Is-Public': 'True'} + path = "http://%s:%d/v1/images" % ("0.0.0.0", api_port) + http = httplib2.Http() + response, content = http.request(path, 'POST', headers=headers) + self.assertEqual(response.status, 201) + data = json.loads(content) + self.assertEqual(data['image']['name'], 'MyImage') + image_id = data['image']['id'] + + # 2. Update image with external source + source = '%s=%s' % (source, get_http_uri(self, 'foobar')) + cmd = "bin/glance update %s %s -p %d" % (image_id, source, api_port) + exitcode, out, err = execute(cmd, raise_error=False) + + self.assertEqual(0, exitcode) + self.assertTrue(out.strip().endswith('Updated image %s' % image_id)) + + # 3. Verify image is now active and of the correct size + cmd = "bin/glance --port=%d show %s" % (api_port, image_id) + + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + + expected_lines = [ + 'URI: http://0.0.0.0:%s/v1/images/%s' % (api_port, image_id), + 'Id: %s' % image_id, + 'Public: Yes', + 'Name: MyImage', + 'Status: active', + 'Size: 5120', + 'Disk format: raw', + 'Container format: ovf', + 'Minimum Ram Required (MB): 0', + 'Minimum Disk Required (GB): 0', + ] + lines = out.split("\n") + self.assertTrue(set(lines) >= set(expected_lines)) + + @requires(setup_http, teardown_http) + def test_update_copying_from(self): + """ + Tests creating an queued image then subsequently updating + with a copy-from source + """ + self._do_test_update_external_source('copy_from') + + @requires(setup_http, teardown_http) + def test_update_location(self): + """ + Tests creating an queued image then subsequently updating + with a location source + """ + self._do_test_update_external_source('location') def test_add_with_location_and_stdin(self): self.cleanup() @@ -201,16 +273,15 @@ image_file.write("XXX") image_file.flush() image_file_name = image_file.name - cmd = minimal_add_command(api_port, - 'MyImage', - '< %s' % image_file_name) + suffix = '--silent-upload < %s' % image_file_name + cmd = minimal_add_command(api_port, 'MyImage', suffix) exitcode, out, err = execute(cmd) self.assertEqual(0, exitcode) msg = out.split("\n") - self.assertTrue(msg[0].startswith('Uploading image')) - self.assertTrue(msg[1].startswith('Added new image with ID:')) + + self._assertStartsWith(msg[0], 'Added new image with ID:') # 2. Verify image added as public image cmd = "bin/glance --port=%d index" % api_port @@ -393,6 +464,70 @@ self.stop_servers() @functional.runs_sql + def test_add_location_with_checksum(self): + """ + We test the following: + + 1. Add an image with location and checksum + 2. Run SQL against DB to verify checksum was entered correctly + """ + self.cleanup() + self.start_servers(**self.__dict__.copy()) + + api_port = self.api_port + registry_port = self.registry_port + + # 1. Add public image + cmd = minimal_add_command(api_port, + 'MyImage', + 'location=http://example.com checksum=1') + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + self.assertTrue(out.strip().startswith('Added new image with ID:')) + + image_id = out.split(":")[1].strip() + + sql = 'SELECT checksum FROM images WHERE id = "%s"' % image_id + recs = self.run_sql_cmd(sql) + + self.assertEqual('1', recs.first()[0]) + + self.stop_servers() + + @functional.runs_sql + def test_add_location_without_checksum(self): + """ + We test the following: + + 1. Add an image with location and no checksum + 2. Run SQL against DB to verify checksum is NULL + """ + self.cleanup() + self.start_servers(**self.__dict__.copy()) + + api_port = self.api_port + registry_port = self.registry_port + + # 1. Add public image + cmd = minimal_add_command(api_port, + 'MyImage', + 'location=http://example.com') + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + self.assertTrue(out.strip().startswith('Added new image with ID:')) + + image_id = out.split(":")[1].strip() + + sql = 'SELECT checksum FROM images WHERE id = "%s"' % image_id + recs = self.run_sql_cmd(sql) + + self.assertEqual(None, recs.first()[0]) + + self.stop_servers() + + @functional.runs_sql def test_add_clear(self): """ We test the following: @@ -760,12 +895,8 @@ image_file.write("XXX") image_file.flush() image_file_name = image_file.name - cmd = "bin/glance --port=%d add is_public=True"\ - " disk_format=raw container_format=ovf " \ - " name=MyImage < %s" % (api_port, image_file_name) - cmd = minimal_add_command(api_port, - 'MyImage', - '< %s' % image_file_name) + suffix = ' --silent-upload < %s' % image_file_name + cmd = minimal_add_command(api_port, 'MyImage', suffix) exitcode, out, err = execute(cmd) @@ -863,7 +994,7 @@ exitcode, out, err = execute(cmd, raise_error=False) self.assertNotEqual(0, exitcode) - self.assertTrue('Image is protected' in err) + self.assertTrue(out.startswith('You do not have permission')) # 4. Remove image protection cmd = "bin/glance --port=%d --force update %s" \ diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/functional/test_copy_to_file.py glance-2012.1~e4/glance/tests/functional/test_copy_to_file.py --- glance-2012.1~e4~20120224.1290/glance/tests/functional/test_copy_to_file.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/functional/test_copy_to_file.py 2012-03-01 13:45:51.000000000 +0000 @@ -43,6 +43,7 @@ import hashlib import httplib2 import json +import tempfile from glance.tests import functional from glance.tests.utils import skip_if_disabled, requires @@ -97,7 +98,7 @@ copy_from = get_uri(self, original_image_id) - # POST /images with public image copied from_store (to Swift) + # POST /images with public image copied from_store (to file) headers = {'X-Image-Meta-Name': 'copied', 'X-Image-Meta-disk_format': 'raw', 'X-Image-Meta-container_format': 'ovf', @@ -187,7 +188,7 @@ copy_from = get_http_uri(self, 'foobar') - # POST /images with public image copied HTTP (to Swift) + # POST /images with public image copied from HTTP (to file) headers = {'X-Image-Meta-Name': 'copied', 'X-Image-Meta-disk_format': 'raw', 'X-Image-Meta-container_format': 'ovf', @@ -223,3 +224,37 @@ self.assertEqual(response.status, 200) self.stop_servers() + + @skip_if_disabled + def test_copy_from_file(self): + """ + Ensure we can't copy from file + """ + self.cleanup() + + self.start_servers(**self.__dict__.copy()) + + api_port = self.api_port + registry_port = self.registry_port + + with tempfile.NamedTemporaryFile() as image_file: + image_file.write("XXX") + image_file.flush() + copy_from = 'file://' + image_file.name + + # POST /images with public image copied from file (to file) + headers = {'X-Image-Meta-Name': 'copied', + 'X-Image-Meta-disk_format': 'raw', + 'X-Image-Meta-container_format': 'ovf', + 'X-Image-Meta-Is-Public': 'True', + 'X-Glance-API-Copy-From': copy_from} + path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port) + http = httplib2.Http() + response, content = http.request(path, 'POST', headers=headers) + self.assertEqual(response.status, 400, content) + + expected = 'External sourcing not supported for store ' + copy_from + msg = 'expected "%s" in "%s"' % (expected, content) + self.assertTrue(expected in content, msg) + + self.stop_servers() diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/functional/test_misc.py glance-2012.1~e4/glance/tests/functional/test_misc.py --- glance-2012.1~e4~20120224.1290/glance/tests/functional/test_misc.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/functional/test_misc.py 2012-03-01 13:45:51.000000000 +0000 @@ -130,9 +130,8 @@ image_file.write("XXX") image_file.flush() image_file_name = image_file.name - cmd = minimal_add_command(self.api_port, - 'MyImage', - 'size=12345 < %s' % 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) diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/functional/test_private_images.py glance-2012.1~e4/glance/tests/functional/test_private_images.py --- glance-2012.1~e4~20120224.1290/glance/tests/functional/test_private_images.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/functional/test_private_images.py 2012-03-01 13:45:51.000000000 +0000 @@ -256,12 +256,12 @@ # Froggy still can't change is-public headers = {'X-Auth-Token': keystone_utils.froggy_token, - 'X-Image-Meta-Is-Public': 'True'} + 'X-Image-Meta-Is-Public': 'False'} path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port, image_id) http = httplib2.Http() response, content = http.request(path, 'PUT', headers=headers) - self.assertEqual(response.status, 404) + self.assertEqual(response.status, 403) # Or give themselves ownership headers = {'X-Auth-Token': keystone_utils.froggy_token, @@ -270,7 +270,7 @@ image_id) http = httplib2.Http() response, content = http.request(path, 'PUT', headers=headers) - self.assertEqual(response.status, 404) + self.assertEqual(response.status, 403) # Froggy can't delete it, either headers = {'X-Auth-Token': keystone_utils.froggy_token} @@ -278,7 +278,7 @@ image_id) http = httplib2.Http() response, content = http.request(path, 'DELETE', headers=headers) - self.assertEqual(response.status, 404) + self.assertEqual(response.status, 403) # But pattieblack can headers = {'X-Auth-Token': keystone_utils.pattieblack_token} @@ -844,6 +844,7 @@ os.environ.pop('OS_AUTH_STRATEGY', None) os.environ.pop('OS_AUTH_USER', None) os.environ.pop('OS_AUTH_KEY', None) + os.environ.pop('OS_REGION_NAME', None) @skip_if_disabled def test_glance_cli_noauth_strategy(self): @@ -878,6 +879,7 @@ os.environ['OS_AUTH_STRATEGY'] = 'keystone' os.environ['OS_AUTH_USER'] = 'pattieblack' os.environ['OS_AUTH_KEY'] = 'secrete' + os.environ['OS_REGION_NAME'] = 'RegionOne' cmd = minimal_add_command(self.api_port, 'MyImage', public=False) self._do_test_glance_cli(cmd) diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/unit/test_api.py glance-2012.1~e4/glance/tests/unit/test_api.py --- glance-2012.1~e4~20120224.1290/glance/tests/unit/test_api.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/unit/test_api.py 2012-03-01 13:45:51.000000000 +0000 @@ -2089,7 +2089,9 @@ self.assertEquals(res.status_int, httplib.OK) res_body = json.loads(res.body)['image'] - self.assertEquals('queued', res_body['status']) + # Once the location is set, the image should be activated + # see LP Bug #939484 + self.assertEquals('active', res_body['status']) self.assertFalse('location' in res_body) # location never shown def test_add_image_no_location_no_content_type(self): diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/unit/test_auth.py glance-2012.1~e4/glance/tests/unit/test_auth.py --- glance-2012.1~e4~20120224.1290/glance/tests/unit/test_auth.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/unit/test_auth.py 2012-03-01 13:45:51.000000000 +0000 @@ -40,6 +40,63 @@ return self.resp.status_int +class V2Token(object): + def __init__(self): + self.tok = self.base_token + + def add_service(self, s_type, region_list=[]): + catalog = self.tok['access']['serviceCatalog'] + service_type = {"type": s_type, "name": "glance"} + catalog.append(service_type) + service = catalog[-1] + endpoint_list = [] + + if region_list == []: + endpoint_list.append(self.base_endpoint) + else: + for region in region_list: + endpoint = self.base_endpoint + endpoint['region'] = region + endpoint_list.append(endpoint) + + service['endpoints'] = endpoint_list + + @property + def token(self): + return self.tok + + @property + def base_endpoint(self): + return { + "adminURL": "http://localhost:9292", + "internalURL": "http://localhost:9292", + "publicURL": "http://localhost:9292" + } + + @property + def base_token(self): + return { + "access": { + "token": { + "expires": "2010-11-23T16:40:53.321584", + "id": "5c7f8799-2e54-43e4-851b-31f81871b6c", + "tenant": {"id": "1", "name": "tenant-ok"} + }, + "serviceCatalog": [ + ], + "user": { + "id": "2", + "roles": [{ + "tenantId": "1", + "id": "1", + "name": "Admin" + }], + "name": "joeadmin" + } + } + } + + class TestKeystoneAuthPlugin(unittest.TestCase): """Test that the Keystone auth plugin works properly""" @@ -78,10 +135,10 @@ try: plugin = auth.KeystoneStrategy(creds) plugin.authenticate() + self.fail("Failed to raise correct exception when supplying " + "bad credentials: %r" % creds) except exception.MissingCredentialError: continue # Expected - self.fail("Failed to raise correct exception when supplying bad " - "credentials: %r" % creds) def test_invalid_auth_url(self): """ @@ -94,13 +151,17 @@ { 'username': 'user1', 'auth_url': 'http://localhost/badauthurl/', - 'password': 'pass' + 'password': 'pass', + 'strategy': 'keystone', + 'region': 'RegionOne' }, # v1 Keystone { 'username': 'user1', 'auth_url': 'http://localhost/badauthurl/v2.0/', 'password': 'pass', - 'tenant': 'tenant1' + 'tenant': 'tenant1', + 'strategy': 'keystone', + 'region': 'RegionOne' } # v2 Keystone ] @@ -138,11 +199,15 @@ { 'username': 'wronguser', 'auth_url': 'http://localhost/badauthurl/', + 'strategy': 'keystone', + 'region': 'RegionOne', 'password': 'pass' }, # wrong username { 'username': 'user1', 'auth_url': 'http://localhost/badauthurl/', + 'strategy': 'keystone', + 'region': 'RegionOne', 'password': 'badpass' }, # bad password... ] @@ -151,16 +216,33 @@ try: plugin = auth.KeystoneStrategy(creds) plugin.authenticate() + self.fail("Failed to raise NotAuthorized when supplying bad " + "credentials: %r" % creds) except exception.NotAuthorized: continue # Expected - self.fail("Failed to raise NotAuthorized when supplying bad " - "credentials: %r" % creds) + + no_strategy_creds = { + 'username': 'user1', + 'auth_url': 'http://localhost/redirect/', + 'password': 'pass', + 'region': 'RegionOne' + } + + try: + plugin = auth.KeystoneStrategy(no_strategy_creds) + plugin.authenticate() + self.fail("Failed to raise MissingCredentialError when " + "supplying no strategy: %r" % no_strategy_creds) + except exception.MissingCredentialError: + pass # Expected good_creds = [ { 'username': 'user1', 'auth_url': 'http://localhost/redirect/', - 'password': 'pass' + 'password': 'pass', + 'strategy': 'keystone', + 'region': 'RegionOne' } ] @@ -170,39 +252,7 @@ def test_v2_auth(self): """Test v2 auth code paths""" - service_type = "image" - - def v2_token(service_type="image"): - # Mock up a token to satisfy v2 auth - token = { - "access": { - "token": { - "expires": "2010-11-23T16:40:53.321584", - "id": "5c7f8799-2e54-43e4-851b-31f81871b6c", - "tenant": {"id": "1", "name": "tenant-ok"} - }, - "serviceCatalog": [{ - "endpoints": [{ - "region": "RegionOne", - "adminURL": "http://localhost:9292", - "internalURL": "http://localhost:9292", - "publicURL": "http://localhost:9292" - }], - "type": service_type, - "name": "glance" - }], - "user": { - "id": "2", - "roles": [{ - "tenantId": "1", - "id": "1", - "name": "Admin" - }], - "name": "joeadmin" - } - } - } - return token + mock_token = None def fake_do_request(cls, url, method, headers=None, body=None): if (not url.rstrip('/').endswith('v2.0/tokens') or @@ -220,10 +270,12 @@ resp.status = 401 else: resp.status = 200 - body = v2_token(service_type) + body = mock_token.token return FakeResponse(resp), json.dumps(body) + mock_token = V2Token() + mock_token.add_service('image', ['RegionOne']) self.stubs.Set(auth.KeystoneStrategy, '_do_request', fake_do_request) unauthorized_creds = [ @@ -231,19 +283,25 @@ 'username': 'wronguser', 'auth_url': 'http://localhost/v2.0', 'password': 'pass', - 'tenant': 'tenant-ok' + 'tenant': 'tenant-ok', + 'strategy': 'keystone', + 'region': 'RegionOne' }, # wrong username { 'username': 'user1', 'auth_url': 'http://localhost/v2.0', 'password': 'badpass', - 'tenant': 'tenant-ok' + 'tenant': 'tenant-ok', + 'strategy': 'keystone', + 'region': 'RegionOne' }, # bad password... { 'username': 'user1', 'auth_url': 'http://localhost/v2.0', 'password': 'pass', - 'tenant': 'carterhayes' + 'tenant': 'carterhayes', + 'strategy': 'keystone', + 'region': 'RegionOne' }, # bad tenant... ] @@ -251,43 +309,157 @@ try: plugin = auth.KeystoneStrategy(creds) plugin.authenticate() + self.fail("Failed to raise NotAuthorized when supplying bad " + "credentials: %r" % creds) except exception.NotAuthorized: continue # Expected - self.fail("Failed to raise NotAuthorized when supplying bad " - "credentials: %r" % creds) + + no_region_creds = { + 'username': 'user1', + 'tenant': 'tenant-ok', + 'auth_url': 'http://localhost/redirect/v2.0/', + 'password': 'pass', + 'strategy': 'keystone' + } + + plugin = auth.KeystoneStrategy(no_region_creds) + self.assertTrue(plugin.authenticate() is None) + self.assertEquals(plugin.management_url, 'http://localhost:9292') + + # Add another image service, with a different region + mock_token.add_service('image', ['RegionTwo']) + + try: + plugin = auth.KeystoneStrategy(no_region_creds) + plugin.authenticate() + self.fail("Failed to raise RegionAmbiguity when no region present " + "and multiple regions exist: %r" % no_region_creds) + except exception.RegionAmbiguity: + pass # Expected + + wrong_region_creds = { + 'username': 'user1', + 'tenant': 'tenant-ok', + 'auth_url': 'http://localhost/redirect/v2.0/', + 'password': 'pass', + 'strategy': 'keystone', + 'region': 'NonExistantRegion' + } + + try: + plugin = auth.KeystoneStrategy(wrong_region_creds) + plugin.authenticate() + self.fail("Failed to raise NoServiceEndpoint when supplying " + "wrong region: %r" % wrong_region_creds) + except exception.NoServiceEndpoint: + pass # Expected + + no_strategy_creds = { + 'username': 'user1', + 'tenant': 'tenant-ok', + 'auth_url': 'http://localhost/redirect/v2.0/', + 'password': 'pass', + 'region': 'RegionOne' + } + + try: + plugin = auth.KeystoneStrategy(no_strategy_creds) + plugin.authenticate() + self.fail("Failed to raise MissingCredentialError when " + "supplying no strategy: %r" % no_strategy_creds) + except exception.MissingCredentialError: + pass # Expected + + bad_strategy_creds = { + 'username': 'user1', + 'tenant': 'tenant-ok', + 'auth_url': 'http://localhost/redirect/v2.0/', + 'password': 'pass', + 'region': 'RegionOne', + 'strategy': 'keypebble' + } + + try: + plugin = auth.KeystoneStrategy(bad_strategy_creds) + plugin.authenticate() + self.fail("Failed to raise BadAuthStrategy when supplying " + "bad auth strategy: %r" % bad_strategy_creds) + except exception.BadAuthStrategy: + pass # Expected + + mock_token = V2Token() + mock_token.add_service('image', ['RegionOne', 'RegionTwo']) good_creds = [ { 'username': 'user1', 'auth_url': 'http://localhost/v2.0/', 'password': 'pass', - 'tenant': 'tenant-ok' + 'tenant': 'tenant-ok', + 'strategy': 'keystone', + 'region': 'RegionOne' }, # auth_url with trailing '/' { 'username': 'user1', 'auth_url': 'http://localhost/v2.0', 'password': 'pass', - 'tenant': 'tenant-ok' - } # auth_url without trailing '/' + 'tenant': 'tenant-ok', + 'strategy': 'keystone', + 'region': 'RegionOne' + }, # auth_url without trailing '/' + { + 'username': 'user1', + 'auth_url': 'http://localhost/v2.0', + 'password': 'pass', + 'tenant': 'tenant-ok', + 'strategy': 'keystone', + 'region': 'RegionTwo' + } # Second region ] for creds in good_creds: plugin = auth.KeystoneStrategy(creds) self.assertTrue(plugin.authenticate() is None) + self.assertEquals(plugin.management_url, 'http://localhost:9292') - creds = { - 'username': 'user1', - 'auth_url': 'http://localhost/v2.0/', - 'password': 'pass', - 'tenant': 'tenant-ok' + ambiguous_region_creds = { + 'username': 'user1', + 'auth_url': 'http://localhost/v2.0/', + 'password': 'pass', + 'tenant': 'tenant-ok', + 'strategy': 'keystone', + 'region': 'RegionOne' } - service_type = "bad-image" + mock_token = V2Token() + # Add two identical services + mock_token.add_service('image', ['RegionOne']) + mock_token.add_service('image', ['RegionOne']) try: - plugin = auth.KeystoneStrategy(creds) + plugin = auth.KeystoneStrategy(ambiguous_region_creds) + plugin.authenticate() + self.fail("Failed to raise RegionAmbiguity when " + "non-unique regions exist: %r" % ambiguous_region_creds) + except exception.RegionAmbiguity: + pass + + mock_token = V2Token() + mock_token.add_service('bad-image', ['RegionOne']) + + good_creds = { + 'username': 'user1', + 'auth_url': 'http://localhost/v2.0/', + 'password': 'pass', + 'tenant': 'tenant-ok', + 'strategy': 'keystone', + 'region': 'RegionOne' + } + + try: + plugin = auth.KeystoneStrategy(good_creds) plugin.authenticate() self.fail("Failed to raise NoServiceEndpoint when bad service " - "type encountered: %r" % service_type) + "type encountered") except exception.NoServiceEndpoint: pass diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/unit/test_clients.py glance-2012.1~e4/glance/tests/unit/test_clients.py --- glance-2012.1~e4~20120224.1290/glance/tests/unit/test_clients.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/unit/test_clients.py 2012-03-01 13:45:51.000000000 +0000 @@ -1616,7 +1616,7 @@ 'container_format': 'bare', 'status': 'active', 'size': 19, - 'location': "file:///tmp/glance-tests/3", + 'location': "http://localhost/glance-tests/3", 'properties': {}} new_image = self.client.add_image(fixture) @@ -1653,7 +1653,7 @@ 'disk_format': 'vhd', 'container_format': 'ovf', 'size': 19, - 'location': "file:///tmp/glance-tests/2", + 'location': "http://localhost/glance-tests/2", } new_image = self.client.add_image(fixture) new_image_id = new_image['id'] @@ -1676,7 +1676,7 @@ 'disk_format': 'vhd', 'container_format': 'ovf', 'size': 19, - 'location': "file:///tmp/glance-tests/2", + 'location': "http://localhost/glance-tests/2", 'properties': {'distro': 'Ubuntu 10.04 LTS'}, } new_image = self.client.add_image(fixture) @@ -1700,7 +1700,7 @@ 'disk_format': 'iso', 'container_format': 'bare', 'size': 19, - 'location': "file:///tmp/glance-tests/2", + 'location': "http://localhost/glance-tests/2", 'properties': {'install': 'Bindows Heaven'}, } new_image = self.client.add_image(fixture) @@ -1728,7 +1728,7 @@ 'disk_format': 'iso', 'container_format': 'vhd', 'size': 19, - 'location': "file:///tmp/glance-tests/3", + 'location': "http://localhost/glance-tests/3", 'properties': {'install': 'Bindows Heaven'}, } @@ -1745,7 +1745,7 @@ 'container_format': 'ovf', 'status': 'bad status', 'size': 19, - 'location': "file:///tmp/glance-tests/2", + 'location': "http://localhost/glance-tests/2", } self.assertRaises(exception.Duplicate, @@ -1760,7 +1760,7 @@ 'container_format': 'ovf', 'status': 'bad status', 'size': 19, - 'location': "file:///tmp/glance-tests/2", + 'location': "http://localhost/glance-tests/2", } new_image = self.client.add_image(fixture) diff -Nru glance-2012.1~e4~20120224.1290/glance/tests/utils.py glance-2012.1~e4/glance/tests/utils.py --- glance-2012.1~e4~20120224.1290/glance/tests/utils.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/glance/tests/utils.py 2012-03-01 13:45:51.000000000 +0000 @@ -190,7 +190,8 @@ no_venv=False, exec_env=None, expect_exit=True, - expected_exitcode=0): + expected_exitcode=0, + context=None): """ Executes a command in a subprocess. Returns a tuple of (exitcode, out, err), where out is the string output @@ -207,6 +208,7 @@ environment variable :param expect_exit: Optional flag true iff timely exit is expected :param expected_exitcode: expected exitcode from the launcher + :param context: additional context for error message """ env = os.environ.copy() @@ -254,6 +256,8 @@ "code of %(exitcode)d."\ "\n\nSTDOUT: %(out)s"\ "\n\nSTDERR: %(err)s" % locals() + if context: + msg += "\n\nCONTEXT: %s" % context raise RuntimeError(msg) return exitcode, out, err diff -Nru glance-2012.1~e4~20120224.1290/glance/vcsversion.py glance-2012.1~e4/glance/vcsversion.py --- glance-2012.1~e4~20120224.1290/glance/vcsversion.py 2012-02-24 16:19:01.000000000 +0000 +++ glance-2012.1~e4/glance/vcsversion.py 2012-03-01 13:47:54.000000000 +0000 @@ -2,6 +2,6 @@ # This file is automatically generated by setup.py, So don't edit it. :) version_info = { 'branch_nick': '(no', - 'revision_id': '9593687d3d1959c8342b60e9b620879899628c2f', - 'revno': 1290 + 'revision_id': '1de0cec728b1e73b8accfc2eb9e4936fd62ca284', + 'revno': 1305 } diff -Nru glance-2012.1~e4~20120224.1290/glance.egg-info/SOURCES.txt glance-2012.1~e4/glance.egg-info/SOURCES.txt --- glance-2012.1~e4~20120224.1290/glance.egg-info/SOURCES.txt 2012-02-24 16:19:02.000000000 +0000 +++ glance-2012.1~e4/glance.egg-info/SOURCES.txt 2012-03-01 13:47:55.000000000 +0000 @@ -216,4 +216,5 @@ tools/install_venv.py tools/nova_to_os_env.sh tools/pip-requires +tools/test-requires tools/with_venv.sh \ No newline at end of file diff -Nru glance-2012.1~e4~20120224.1290/tools/install_venv.py glance-2012.1~e4/tools/install_venv.py --- glance-2012.1~e4~20120224.1290/tools/install_venv.py 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/tools/install_venv.py 2012-03-01 13:45:51.000000000 +0000 @@ -30,6 +30,7 @@ ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) VENV = os.path.join(ROOT, '.venv') PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') +TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires') def die(message, *args): @@ -91,14 +92,19 @@ print 'done.' +def pip_install(*args): + run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(venv=VENV): print 'Installing dependencies with pip (this can take a while)...' - # Install greenlet by hand - just listing it in the requires file does not - # get it in stalled in the right order - venv_tool = 'tools/with_venv.sh' - run_command([venv_tool, 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES], - redirect_output=False) + pip_install('pip') + + pip_install('-r', PIP_REQUIRES) + pip_install('-r', TEST_REQUIRES) # Tell the virtual env how to "import glance" py_ver = _detect_python_version(venv) diff -Nru glance-2012.1~e4~20120224.1290/tools/pip-requires glance-2012.1~e4/tools/pip-requires --- glance-2012.1~e4~20120224.1290/tools/pip-requires 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/tools/pip-requires 2012-03-01 13:45:51.000000000 +0000 @@ -10,9 +10,7 @@ routes webob==1.0.8 wsgiref -sphinx argparse -mox boto==2.1.1 swift sqlalchemy-migrate>=0.7 @@ -41,11 +39,3 @@ Paste passlib - -# Needed for testing -nose -nose-exclude -nosexcover -openstack.nose_plugin -pep8==0.6.1 -pylint==0.19 diff -Nru glance-2012.1~e4~20120224.1290/tools/test-requires glance-2012.1~e4/tools/test-requires --- glance-2012.1~e4~20120224.1290/tools/test-requires 1970-01-01 00:00:00.000000000 +0000 +++ glance-2012.1~e4/tools/test-requires 2012-03-01 13:45:51.000000000 +0000 @@ -0,0 +1,11 @@ +# Packages needed for dev testing +distribute>=0.6.24 + +# Needed for testing +mox +nose +nose-exclude +nosexcover +openstack.nose_plugin +pep8==0.6.1 +sphinx>=1.1.2 diff -Nru glance-2012.1~e4~20120224.1290/tox.ini glance-2012.1~e4/tox.ini --- glance-2012.1~e4~20120224.1290/tox.ini 2012-02-24 16:16:28.000000000 +0000 +++ glance-2012.1~e4/tox.ini 2012-03-01 13:45:51.000000000 +0000 @@ -4,16 +4,13 @@ [testenv] setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/tools/pip-requires + -r{toxinidir}/tools/test-requires commands = nosetests [testenv:pep8] deps = pep8 commands = pep8 --repeat --show-source bin glance setup.py -[testenv:pylint] -deps = pylint -commands = pylint --rcfile=pylintrc --output-format=parseable glance - [testenv:cover] commands = nosetests --with-coverage --cover-html --cover-erase --cover-package=glance