Merge lp:~jaypipes/glance/bug713126 into lp:~glance-coresec/glance/cactus-trunk

Proposed by Jay Pipes
Status: Merged
Approved by: Jay Pipes
Approved revision: 83
Merged at revision: 82
Proposed branch: lp:~jaypipes/glance/bug713126
Merge into: lp:~glance-coresec/glance/cactus-trunk
Diff against target: 1426 lines (+704/-506)
11 files modified
doc/source/installing.rst (+1/-1)
etc/glance.conf.sample (+27/-4)
glance/server.py (+2/-1)
glance/store/filesystem.py (+11/-2)
glance/store/swift.py (+224/-67)
tests/stubs.py (+3/-73)
tests/unit/swiftfakehttp.py (+0/-294)
tests/unit/test_filesystem_store.py (+135/-0)
tests/unit/test_stores.py (+0/-64)
tests/unit/test_swift_store.py (+300/-0)
tools/pip-requires (+1/-0)
To merge this branch: bzr merge lp:~jaypipes/glance/bug713126
Reviewer Review Type Date Requested Status
Rick Harris (community) Approve
Armando Migliaccio (community) Approve
Devin Carlen (community) Approve
Wayne A. Walls Pending
Review via email: mp+51474@code.launchpad.net

Description of the change

Adds ability for Swift to be used as a full-fledged backend.
Adds POST/PUT capabilities to the SwiftBackend
Adds lots of unit tests for both FilesystemBackend and SwiftBackend
Removes now-unused tests.unit.fakeswifthttp module

To post a comment you must log in.
Revision history for this message
Wayne A. Walls (wayne-walls) wrote :

Checking out this patch right now.

cheers!

Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

To me this branch was long overdue :)

At a first _glance_, I couldn't find anything wrong with it...only two small niggley bits:

- maybe if tests don't find swift they should say 'skipped' rather than 'ok', but I don't know if nosetest can do that easily. The effect is the same but it just draws attention to the user
- the default swift store options are a bit weak: DEFAULT_SWIFT_ACCOUNT = 'glance' and DEFAULT_SWIFT_CONTAINER = 'glance' do not seem very unique to me :)

Other than that, I'd like to give this branch a whirl on a live swift deployment we got here before giving a full 'approve' from my side :)

Revision history for this message
Jay Pipes (jaypipes) wrote :

> To me this branch was long overdue :)

:) Yes, I know.

> At a first _glance_, I couldn't find anything wrong with it...only two small
> niggley bits:
>
> - maybe if tests don't find swift they should say 'skipped' rather than 'ok',
> but I don't know if nosetest can do that easily. The effect is the same but it
> just draws attention to the user

Yes, I thought about this. I had originally used unittest2's @skipIf decorator, but I had proposed an earlier branch and people didn't want to require dependency on unittest2. nose has some ability to do skipping; I could check into this.

> - the default swift store options are a bit weak: DEFAULT_SWIFT_ACCOUNT =
> 'glance' and DEFAULT_SWIFT_CONTAINER = 'glance' do not seem very unique to me
> :)

Well, the account and container are more general-purpose than anything to do with authenticating (user and key are for that purpose). What do you think I should set account and container to?

> Other than that, I'd like to give this branch a whirl on a live swift
> deployment we got here before giving a full 'approve' from my side :)

Can't wait for feedback! Thanks, Armando!

-jay

Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

I guess that as long as we use an account that cannot be used by anyone else, then there will never be a risk of having image files mixed up with user data, so in that respect it's safe.

As for the swift_store_auth_address I wonder if we need to append /v1.0 at the end of it.

When I use ST (the tool that comes with swift) without it, I get the following error:

__main__.ClientException: Auth GET failed: https://192.168.1.199:11000 400 Bad Request

Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

Jay, this may sound silly, but can you spot why I am getting this exception?

2011-02-28 14:30:49 ERROR [glance.store.swift] auch
Traceback (most recent call last):
  File "/usr/lib/python2.6/site-packages/eventlet/wsgi.py", line 336, in handle_one_response
    result = self.application(self.environ, start_response)
  File "build/bdist.linux-i686/egg/webob/dec.py", line 159, in __call__
    return resp(environ, start_response)
  File "build/bdist.linux-i686/egg/routes/middleware.py", line 131, in __call__
    response = self.app(environ, start_response)
  File "build/bdist.linux-i686/egg/webob/dec.py", line 159, in __call__
    return resp(environ, start_response)
  File "build/bdist.linux-i686/egg/webob/dec.py", line 147, in __call__
    resp = self.call_func(req, *args, **self.kwargs)
  File "build/bdist.linux-i686/egg/webob/dec.py", line 208, in call_func
    return self.func(req, *args, **kwargs)
  File "/usr/lib/python2.6/site-packages/glance/common/wsgi.py", line 215, in __call__
    result = method(**arg_dict)
  File "/usr/lib/python2.6/site-packages/glance/server.py", line 352, in create
    self._upload_and_activate(req, image_meta)
  File "/usr/lib/python2.6/site-packages/glance/server.py", line 315, in _upload_and_activate
    raise e
ImportError: No module named common.client

I have swift installed in my site-packages, and I can import swift.common.client.Connection using the python interpreter. This exception occurs every time I try to upload. As the problem is here:

272 + connection_class = get_connection_class(conn_class)

Thanks,
Armando

Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

I'd like to see this in trunk asap :)

review: Approve
Revision history for this message
Rick Harris (rconradharris) wrote :

Excellent work Jay. I got it working with my CloudFiles account with some small-fixes:

> 237 + account = options.get('swift_store_account',
> 238 + DEFAULT_SWIFT_ACCOUNT)

Account is referenced but not used. AFAIK, there isn't a distinction between
account and username, so, I think we can remove all references to account.

> 143 -from __future__ import absolute_import

Was this removed for a reason? The `swift` adapter is shadowing the `swift`
module so `import swift.common.client` won't do the right thing without this.

> 215 + def add(cls, id, data, options, conn_class=None):

We probably shouldn't be using `id`, but, we can save that for a cleanup-patch
where we fix that everywhere, all-at-once.

> 308 + "location %(location)s" % locals())

One space to the right :)

> 334 + except Exception, e:

This should probably be `except swift.common.client.ClientException, e` in
order to guarantee that it has the `http_status` attribute.

Since, you'll need swift.common.client in the local-namespace to do this, you
can either:

* Use separate imports in each function that has to use something from the
  swift module

* Use a lazy-import pattern: define a global SWIFT global variable (initialized to None) and then lazily populate it. Then use that to access sub-modules. I believe this is done in Nova.

> 281 + obj_etag = swift_conn.put_object(container, id, data)

Needs to be `str(id)` since the `quote` method used by swift.Client expects a
string.

Since this is used repeatedly (in the head_object call too), probably best to
do something like:

    obj_name = str(id)

at the top of each method.

Currently we're assuming that the container already exists (if it doesn't, the
PUT will fail with a 404). I would vote that we automatically create the
container if it doesn't exist.

Perhaps this behavior should be governed by a configuration option, something
like `swift_store_create_container_on_put`

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

> Jay, this may sound silly, but can you spot why I am getting this exception?
>
> 2011-02-28 14:30:49 ERROR [glance.store.swift] auch
> Traceback (most recent call last):
> File "/usr/lib/python2.6/site-packages/eventlet/wsgi.py", line 336, in
> handle_one_response
> result = self.application(self.environ, start_response)
> File "build/bdist.linux-i686/egg/webob/dec.py", line 159, in __call__
> return resp(environ, start_response)
> File "build/bdist.linux-i686/egg/routes/middleware.py", line 131, in
> __call__
> response = self.app(environ, start_response)
> File "build/bdist.linux-i686/egg/webob/dec.py", line 159, in __call__
> return resp(environ, start_response)
> File "build/bdist.linux-i686/egg/webob/dec.py", line 147, in __call__
> resp = self.call_func(req, *args, **self.kwargs)
> File "build/bdist.linux-i686/egg/webob/dec.py", line 208, in call_func
> return self.func(req, *args, **kwargs)
> File "/usr/lib/python2.6/site-packages/glance/common/wsgi.py", line 215, in
> __call__
> result = method(**arg_dict)
> File "/usr/lib/python2.6/site-packages/glance/server.py", line 352, in
> create
> self._upload_and_activate(req, image_meta)
> File "/usr/lib/python2.6/site-packages/glance/server.py", line 315, in
> _upload_and_activate
> raise e
> ImportError: No module named common.client
>
> I have swift installed in my site-packages, and I can import
> swift.common.client.Connection using the python interpreter. This exception
> occurs every time I try to upload. As the problem is here:
>
> 272 + connection_class = get_connection_class(conn_class)

It's because there is a module glance.store.swift and a module swift.common.client. Local namespacing is confusing Python into thinking it should search for "common.client" at a module path of glance.store.swift.common.client. It's a bug, and there are a couple solutions to it. Rick refers to a couple below. Working on it...

Thanks!
jay

Revision history for this message
Jay Pipes (jaypipes) wrote :

Thanks Rick, working on fixes now. Should push a branch shortly... gah, jury duty is a pain in the business schedule :)

Revision history for this message
Jay Pipes (jaypipes) wrote :
Download full text (3.3 KiB)

> Excellent work Jay. I got it working with my CloudFiles account with some
> small-fixes:
>
> > 237 + account = options.get('swift_store_account',
> > 238 + DEFAULT_SWIFT_ACCOUNT)
>
> Account is referenced but not used. AFAIK, there isn't a distinction between
> account and username, so, I think we can remove all references to account.

account definitely != username in Swift. username/key is used for auth'ing against an authentication service, but account is used to determine which containers a user has access to, at least if I'm understanding the docs correctly. I believe that the auth middleware inserts the X-User-Account header into the environ? Not quite sure...

Hmm, so I was a little unsure about this and was hoping the Swift guys could shine some light. As far as I have read, I guess Swift objects are always in an account and a container, with the URI structure:

auth_url/account/container/objectname

Our previous code was ignoring account so I ignored it too, but I think I will add it now? This relates to LP bug #717431

> > 143 -from __future__ import absolute_import
>
> Was this removed for a reason? The `swift` adapter is shadowing the `swift`
> module so `import swift.common.client` won't do the right thing without this.

I removed it because absolute_import wasn't being used. :) Though, might need to use it per your answer below ;)

> > 215 + def add(cls, id, data, options, conn_class=None):
>
> We probably shouldn't be using `id`, but, we can save that for a cleanup-patch
> where we fix that everywhere, all-at-once.

Ya, cleanup patch.

> > 308 + "location %(location)s" %
> locals())
>
> One space to the right :)

Done.

> > 334 + except Exception, e:
>
> This should probably be `except swift.common.client.ClientException, e` in
> order to guarantee that it has the `http_status` attribute.
>
> Since, you'll need swift.common.client in the local-namespace to do this, you
> can either:
>
> * Use separate imports in each function that has to use something from the
> swift module
>
> * Use a lazy-import pattern: define a global SWIFT global variable
> (initialized to None) and then lazily populate it. Then use that to access
> sub-modules. I believe this is done in Nova.

The main problem is not having the import of glance.store.swift in tests/unit/test_swift_store.py blow up when Swift isn't installed. I could try the Nova solution, but it always seems hacky compared to just wrapping an import swift.common.client in a try: except ImportError: block...

I could solve the problem using nose's skip stuff, too, I think?

> > 281 + obj_etag = swift_conn.put_object(container, id, data)
>
> Needs to be `str(id)` since the `quote` method used by swift.Client expects a
> string.
>
> Since this is used repeatedly (in the head_object call too), probably best to
> do something like:
>
> obj_name = str(id)
>
> at the top of each method.

Done.

> Currently we're assuming that the container already exists (if it doesn't, the
> PUT will fail with a 404). I would vote that we automatically create the
> container if i...

Read more...

Revision history for this message
Rick Harris (rconradharris) wrote :

>Our previous code was ignoring account so I ignored it too, but I think I will add it now? This relates to LP bug #717431

Ah ok. So, yeah, probably need some clarification, but `account` should be
used in crafting the URI.

> 283 + try:
> 284 + swift_conn.head_container(container)
> 285 + except Exception, e:
> 286 + if e.http_status == httplib.NOT_FOUND:
> 287 + add_container = config.get_option(options,
> 288 + 'swift_store_create_container_on_put',
> 289 + type='bool', default=False)
> 290 + if add_container:
> 291 + try:
> 292 + swift_conn.put_container(container)
> 293 + except Exception, e:
> 294 + msg = ("Failed to add container to Swift.\n"
> 295 + "Got error from Swift: %(e)s" % locals())
> 296 + raise glance.store.BackendException(msg)
> 297 + else:
> 298 + msg = ("The container %(container)s does not exist in "
> 299 + "Swift. Please set the "
> 300 + "swift_store_create_container_on_put option"
> 301 + "to add container to Swift automatically."
> 302 + % locals())
> 303 + raise glance.store.BackendException(msg)

The `add()` method is starting to get a bit long. I'd vote that we break this
code out into a separate method called something like
`_create_container_if_needed`.

> 245 + user = options.get('swift_store_user')
...
> 287 + add_container = config.get_option(options,
> 288 + 'swift_store_create_container_on_put',
> 289 + type='bool', default=False)

We're getting options two--really three if you could CLI options--different
ways. One way is to use paste and the other--the new way-- is to use the
ConfigParser api directly.

I'd vote to continue to use the `paste` method for now, and, if we want to
refactor config options, do that as another, config-options-improvement patch.

> 263 + if not user:
> 264 + logger.error(msg)
> 265 + msg = ("Could not find swift_store_user in configuration "
> 266 + "options.")
> 267 + raise glance.store.BackendException(msg)

There is some repeated code similar to this. Wonder if we'd benefit from a
`_get_required_option` function that returns the value if present, and logs
and raises if not.

> 157 +DEFAULT_SWIFT_ACCOUNT = 'glance'
> 158 +DEFAULT_SWIFT_CONTAINER = 'glance'

I don't see a reason to have these constants. We already have the config-file,
we can just treat the `account` and `container` config options as required. Seems
less confusing to me, IMHO.

Revision history for this message
Jay Pipes (jaypipes) wrote :

On Tue, Mar 1, 2011 at 2:20 AM, Rick Harris <email address hidden> wrote:
>> 245   +        user = options.get('swift_store_user')
> ...
>> 287   +                add_container = config.get_option(options,
>> 288   +                                    'swift_store_create_container_on_put',
>> 289   +                                    type='bool', default=False)
>
> We're getting options two--really three if you could CLI options--different
> ways. One way is to use paste and the other--the new way-- is to use the
> ConfigParser api directly.
>
> I'd vote to continue to use the `paste` method for now, and, if we want to
> refactor config options, do that as another, config-options-improvement patch.

Sorry, I'm lost. The above is the glance.common.config.get_option()
function you added in a recent patch that I specifically argued
against using... I'm only following the convention you set in
/glance/registry/db/api.py.

-jay

Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

Does line:

279 + logger.debug("Adding image object to Swift using "
280 + "(auth_address=%(auth_address)s, user=%(user)s, "
281 + "key=%(key)s)")

need % locals() ?

review: Needs Fixing
Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

Rick,

did you specify the version for swift_store_auth_address? like this:

swift_store_auth_address=127.0.0.1:11000/v1.0

If I don't do it, I get this error:

BackendException: Failed to add object to Swift.
Got error from Swift: Auth GET failed: https://127.0.0.1:11000 400 Bad Request

If the version is required, we might want to tweak the default otherwise it will raise a lot of false positives from swift newbies :)

Many thanks,
Armando

Revision history for this message
Rick Harris (rconradharris) wrote :

Jay:

> I'm only following the convention you set in
/glance/registry/db/api.py

You're right. Disregard comment, acquire sanity. :)

Sorry about the mix-up, I'm just generally confused by config-options at this point. Something we can revisit down the road, I guess.

Armando:

> did you specify the version for swift_store_auth_address?

Yep.

> If the version is required, we might want to tweak the default otherwise it will raise a lot of false positives from swift newbies

Yeah I agree, it's a bit confusing. We can use use a BASE_URI which encapsulates both scheme, host, port, and version number like:

https://auth.api.rackspacecloud.com/v1.0/

Or, we can use a separate config for each part:

swift_store_port = 80
swift_store_host = auth.api.rackspacecloud.com
swift_store_api_version = 1.0

I lean towards the separate-config approach since it seems a little simpler.

Revision history for this message
Wayne A. Walls (wayne-walls) wrote :

Greetings, everyone!

So, I am getting VMs to boot, and for all intents and purposes this is 'working.' One thing that I feel is odd, when I jump on my swift proxy, and check the containers...they are empty.

##nova-compute log##
2011-03-01 10:28:02,909 INFO nova.virt.xenapi.vmops [-] Spawning VM instance-00000009 created OpaqueRef:8d097152-7213-607d-a8a0-89acf8b265d6.
2011-03-01 10:28:03,242 INFO nova.virt.xenapi [-] Task [Async.host.call_plugin] OpaqueRef:b0af1d35-2a7a-39be-cecd-377d5f021df5 status: success <value>null</value>
2011-03-01 10:28:03,452 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS) xenserver vm state -> |Running|
2011-03-01 10:28:03,452 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS) xenapi power_state -> |1|
2011-03-01 10:28:03,502 DEBUG nova.virt.xenapi.vmops [-] Instance instance-00000009: booted from (pid=964) _wait_for_boot /root/openstack/nova/nova/virt/xenapi/vmops.py:150
2011-03-01 10:28:03,506 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS) xenserver vm state -> |Running|
2011-03-01 10:28:03,506 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS) xenapi power_state -> |1|

##on domU##
+----+--------+--------+-----------+----------------+
| ID | Name | Status | Public IP | Private IP |
+----+--------+--------+-----------+----------------+
| 7 | test01 | active | | 184.106.17.204 |
| 8 | test01 | active | | 184.106.17.205 |
| 9 | test01 | active | | 184.106.17.206 |
+----+--------+--------+-----------+----------------+

##On swift proxy##
root@colo01:~# st -A https://184.106.17.135:8080/auth/v1.0 -U system:root -K XXXXX stat
   Account: AUTH_XXXXXXXX
Containers: 0
   Objects: 0
     Bytes: 0

Maybe I am setting up something wrong in my glance.conf? Here it is for your review:

# ============ Swift Store Options =============================

# Address where the Swift authentication service lives
swift_store_auth_address = 184.106.17.135:8080/auth/v1.0

# User to authenticate against the Swift authentication service
swift_store_user = root

# Auth key for the user authenticating against the
# Swift authentication service
swift_store_key = AUTH_XXXXXXXXXXXXXXXXXXXXXX

# Account to use for the user:key Swift auth combination
# for storing images in Swift
swift_store_account = root

# Container within the account that the account should use
# for storing images in Swift
swift_store_container = glance

# Do we create the container if it does not exist?
swift_store_create_container_on_put = True

Any ideas? What else are you folks running into?

Cheers!

Revision history for this message
Wayne A. Walls (wayne-walls) wrote :

Oh, and I'm using swauth (bind to 8080) for swift, not devauth.

thanks!

Revision history for this message
Rick Harris (rconradharris) wrote :

Wayne:

I tested Jay's code against production CloudFiles. You could sanity check your setup, by first hitting CloudFiles, then, if that works, repointing to your local instance of Swift.

Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

Wayne,
should your swift_store_account option in glance.conf be system rather than root?

Just guessing here...

> Greetings, everyone!
>
> So, I am getting VMs to boot, and for all intents and purposes this is
> 'working.' One thing that I feel is odd, when I jump on my swift proxy, and
> check the containers...they are empty.
>
> ##nova-compute log##
> 2011-03-01 10:28:02,909 INFO nova.virt.xenapi.vmops [-] Spawning VM
> instance-00000009 created OpaqueRef:8d097152-7213-607d-a8a0-89acf8b265d6.
> 2011-03-01 10:28:03,242 INFO nova.virt.xenapi [-] Task
> [Async.host.call_plugin] OpaqueRef:b0af1d35-2a7a-39be-cecd-377d5f021df5
> status: success <value>null</value>
> 2011-03-01 10:28:03,452 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS)
> xenserver vm state -> |Running|
> 2011-03-01 10:28:03,452 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS) xenapi
> power_state -> |1|
> 2011-03-01 10:28:03,502 DEBUG nova.virt.xenapi.vmops [-] Instance
> instance-00000009: booted from (pid=964) _wait_for_boot
> /root/openstack/nova/nova/virt/xenapi/vmops.py:150
> 2011-03-01 10:28:03,506 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS)
> xenserver vm state -> |Running|
> 2011-03-01 10:28:03,506 INFO nova.virt.xenapi.vm_utils [-] (VM_UTILS) xenapi
> power_state -> |1|
>
> ##on domU##
> +----+--------+--------+-----------+----------------+
> | ID | Name | Status | Public IP | Private IP |
> +----+--------+--------+-----------+----------------+
> | 7 | test01 | active | | 184.106.17.204 |
> | 8 | test01 | active | | 184.106.17.205 |
> | 9 | test01 | active | | 184.106.17.206 |
> +----+--------+--------+-----------+----------------+
>
> ##On swift proxy##
> root@colo01:~# st -A https://184.106.17.135:8080/auth/v1.0 -U system:root -K
> XXXXX stat
> Account: AUTH_XXXXXXXX
> Containers: 0
> Objects: 0
> Bytes: 0
>
> Maybe I am setting up something wrong in my glance.conf? Here it is for your
> review:
>
> # ============ Swift Store Options =============================
>
> # Address where the Swift authentication service lives
> swift_store_auth_address = 184.106.17.135:8080/auth/v1.0
>
> # User to authenticate against the Swift authentication service
> swift_store_user = root
>
> # Auth key for the user authenticating against the
> # Swift authentication service
> swift_store_key = AUTH_XXXXXXXXXXXXXXXXXXXXXX
>
> # Account to use for the user:key Swift auth combination
> # for storing images in Swift
> swift_store_account = root
>
> # Container within the account that the account should use
> # for storing images in Swift
> swift_store_container = glance
>
> # Do we create the container if it does not exist?
> swift_store_create_container_on_put = True
>
>
>
> Any ideas? What else are you folks running into?
>
> Cheers!

Revision history for this message
Wayne A. Walls (wayne-walls) wrote :

@Rick, alright I'll give that a try, good idea.

@Armando, I tried 'root' 'system:root' 'system', and a variety of other names. When using cyberduck, I have to use system:root and point to the right auth path "/auth/v1.0" This worked before I changed to swauth, now it's back to giving me errors...shrug

Revision history for this message
Wayne A. Walls (wayne-walls) wrote :

@Rick -- Well, same result. VMs are booting, but no containers are being made on my CloudFiles account. I'm not even sure /where/ these VMs are going... I'll continue to check this out...

Revision history for this message
Jay Pipes (jaypipes) wrote :

On Tue, Mar 1, 2011 at 3:11 PM, Wayne A. Walls <email address hidden> wrote:
> @Armando, I tried 'root' 'system:root' 'system', and a variety of other names.  When using cyberduck, I have to use system:root and point to the right auth path "/auth/v1.0" This worked before I changed to swauth, now it's back to giving me errors...shrug

The account isn't even being used. In fact, in the swift client code
(/swift/common/client.py in Swift) there's no mention of account
anywhere. We are still trying to figure out how to even specify the
thing or even what it is or if it's different between Cloud Files and
Swift. Some people say that this is the URI structure:

http://user:key@authurl/account/container/objname

Others say this is:

http://user:account:key@authurl/container/objname

Frankly, after going through the Swift docs, I'm at a loss as to what
the account is and how it's different from the swift auth user... if
someone could explain this, we could code it up.

-jay

Revision history for this message
Jay Pipes (jaypipes) wrote :

> Does line:
>
> 279 + logger.debug("Adding image object to Swift using "
> 280 + "(auth_address=%(auth_address)s, user=%(user)s, "
> 281 + "key=%(key)s)")
>
> need % locals() ?

Yes it does :) Good catch!

Revision history for this message
Jay Pipes (jaypipes) wrote :

Added fixes from reviews. Please do re-review. Thanks :)

Revision history for this message
Rick Harris (rconradharris) wrote :

Looks good, unit-tests pass, and I confirmed it works functionally.

Just a couple of femto-nits:

> 189 + conn_class=None):

Looks like several methods are taking the `conn_class` kwarg that is never
used.

> import urllib

Not directly related, but it looks like this import isn't used anymore.

> 377 + except ClientException, e:
> 378 + if e.http_status == httplib.NOT_FOUND:
> 379 + add_container = config.get_option(options,
> 380 + 'swift_store_create_container_on_put',
> 381 + type='bool', default=False)
> 382 + if add_container:
> 383 + try:
> 384 + swift_conn.put_container(container)
> 385 + except ClientException, e:
> 386 + msg = ("Failed to add container to Swift.\n"
> 387 + "Got error from Swift: %(e)s" % locals())
> 388 + raise glance.store.BackendException(msg)
> 389 + else:
> 390 + msg = ("The container %(container)s does not exist in "
> 391 + "Swift. Please set the "
> 392 + "swift_store_create_container_on_put option"
> 393 + "to add container to Swift automatically."
> 394 + % locals())
> 395 + raise glance.store.BackendException(msg)

In the case where e.http_status != HTTP_FOUND, we probably want to re-raise
the current exception.

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

Yep, all good catches. Fixing now.

Revision history for this message
Devin Carlen (devcamcar) wrote :

210 + location = "swift://%s:%s@%s/%s/%s" % (user, key, authurl,
211 + container, obj)

305 + location = "swift://%(user)s:%(key)s@%(auth_address)s/"\
306 + "%(container)s/%(obj_name)s" % locals()

319 + location = "swift://%s:%s@%s/%s/%s" % (user, key, auth_address,
320 + container, id)

350 + location = "swift://%s:%s@%s/%s/%s" % (user, key, authurl,
351 + container, obj)

Can you add a helper method to make this cleaner?

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

Devin and Rick, pushed fixes from your reviews. Please re-check :) Thanks again!

Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

Jay,

this looks good!

However, I am still a bit confused about the use of the account bit. Were you able to clarify the point below?

> On Tue, Mar 1, 2011 at 3:11 PM, Wayne A. Walls <email address hidden> wrote:
> The account isn't even being used. In fact, in the swift client code
> (/swift/common/client.py in Swift) there's no mention of account
> anywhere. We are still trying to figure out how to even specify the
> thing or even what it is or if it's different between Cloud Files and
> Swift. Some people say that this is the URI structure:
>
> http://user:key@authurl/account/container/objname
>
> Others say this is:
>
> http://user:account:key@authurl/container/objname
>
> Frankly, after going through the Swift docs, I'm at a loss as to what
> the account is and how it's different from the swift auth user... if
> someone could explain this, we could code it up.
>
> -jay

Moreover:

1) It looks that the variable 'account' is still not used in the 'add' method
2) In the glance.conf.sample it's specified:

+# Address where the Swift authentication service lives
+# The auth address should be in the form:
+# <DOMAIN>[:<PORT>]/<VERSION>/<ACCOUNT>
+swift_store_auth_address = 127.0.0.1:8080/v1.0/glance-account

so one would understand that swift_store_account is 'glance-account', but then the code specifies:

+DEFAULT_SWIFT_ACCOUNT = 'glance'

3) swift_store_account is a missing option in the glance.conf.sample
4) swift_store_create_container_on_put = False (I'd rather see True as a default option)

All of these add up to the confusion :)

I would like to see glance.conf.sample with a set of sensible defaults so that one who does not have a great deal of experience with glance/swift can get up and running without troubles. The above mentioned points make me feel we are not quite there yet :)

Hope this help!
Cheers,
Armando

review: Needs Information
Revision history for this message
Jay Pipes (jaypipes) wrote :

On Sun, Mar 6, 2011 at 1:14 PM, Armando Migliaccio
<email address hidden> wrote:
> 1) It looks that the variable 'account' is still not used in the 'add' method
> 2) In the glance.conf.sample it's specified:
>
> +# Address where the Swift authentication service lives
> +# The auth address should be in the form:
> +# <DOMAIN>[:<PORT>]/<VERSION>/<ACCOUNT>
> +swift_store_auth_address = 127.0.0.1:8080/v1.0/glance-account
>
> so one would understand that swift_store_account is 'glance-account', but then the code specifies:
>
> +DEFAULT_SWIFT_ACCOUNT = 'glance'

I meant to remove this. The account should be specified in the
swift_store_auth_address option, and nothing else.

> 3) swift_store_account is a missing option in the glance.conf.sample

I removed this option.

> 4) swift_store_create_container_on_put = False (I'd rather see True as a default option)

I didn't feel comfortable setting it to True by default since that
would change data in Swift (add the container), but I could go either
way I suppose... this is an easy fix obviously.

> All of these add up to the confusion :)
>
> I would like to see glance.conf.sample with a set of sensible defaults so that one who does not have a great deal of experience with glance/swift can get up and running without troubles. The above mentioned points make me feel we are not quite there yet :)

So would I. :) Unfortunately, its not exactly simple to setup Swift
and get everything working without, frankly, knowing what you are
doing and knowing how Swift authentication works (some "auth service"
with a URL like http://localhost:8080/v1.0/account/ authenticates a
user/key combination for an account and returns a storage management
URL that subsequent requests use as their base request URL...

Anyway, I've tried my best to make it sensible to configure *if* you
know how to setup Swift, but I was tentative to try anything more
complex.

-jay

Revision history for this message
Devin Carlen (devcamcar) wrote :

lgtm!

review: Approve
Revision history for this message
Armando Migliaccio (armando-migliaccio) wrote :

> On Sun, Mar 6, 2011 at 1:14 PM, Armando Migliaccio
> <email address hidden> wrote:
> > 1) It looks that the variable 'account' is still not used in the 'add'
> method
> > 2) In the glance.conf.sample it's specified:
> >
> > +# Address where the Swift authentication service lives
> > +# The auth address should be in the form:
> > +# <DOMAIN>[:<PORT>]/<VERSION>/<ACCOUNT>
> > +swift_store_auth_address = 127.0.0.1:8080/v1.0/glance-account
> >
> > so one would understand that swift_store_account is 'glance-account', but
> then the code specifies:
> >
> > +DEFAULT_SWIFT_ACCOUNT = 'glance'
>
> I meant to remove this. The account should be specified in the
> swift_store_auth_address option, and nothing else.
>
> > 3) swift_store_account is a missing option in the glance.conf.sample
>
> I removed this option.
>
> > 4) swift_store_create_container_on_put = False (I'd rather see True as a
> default option)
>
> I didn't feel comfortable setting it to True by default since that
> would change data in Swift (add the container), but I could go either
> way I suppose... this is an easy fix obviously.
>
> > All of these add up to the confusion :)
> >
> > I would like to see glance.conf.sample with a set of sensible defaults so
> that one who does not have a great deal of experience with glance/swift can
> get up and running without troubles. The above mentioned points make me feel
> we are not quite there yet :)
>
> So would I. :) Unfortunately, its not exactly simple to setup Swift
> and get everything working without, frankly, knowing what you are
> doing and knowing how Swift authentication works (some "auth service"
> with a URL like http://localhost:8080/v1.0/account/ authenticates a
> user/key combination for an account and returns a storage management
> URL that subsequent requests use as their base request URL...
>

fair enough!

> Anyway, I've tried my best to make it sensible to configure *if* you
> know how to setup Swift, but I was tentative to try anything more
> complex.
>
> -jay

I think you did great, as I was able to get this up and running fairly quickly. Apart the niggley bit of

> > +DEFAULT_SWIFT_ACCOUNT = 'glance'

lying around in the code, this branch has both thumbs up from my side

review: Approve
Revision history for this message
Rick Harris (rconradharris) wrote :

Agree with Armando, we should probably remove this bit of vestigial code:

257 + account = options.get('swift_store_account',
258 + DEFAULT_SWIFT_ACCOUNT)

170 +DEFAULT_SWIFT_ACCOUNT = 'glance'

review: Needs Fixing
Revision history for this message
Jay Pipes (jaypipes) wrote :

Vestigial account stuff gone! :) Please set to approved if that's the final fix...

Revision history for this message
Rick Harris (rconradharris) wrote :

lgtm

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Download full text (40.9 KiB)

The attempt to merge lp:~jaypipes/glance/bug713126 into lp:glance failed. Below is the output from the failed tests.

running test
running egg_info
creating glance.egg-info
writing glance.egg-info/PKG-INFO
writing top-level names to glance.egg-info/top_level.txt
writing dependency_links to glance.egg-info/dependency_links.txt
writing manifest file 'glance.egg-info/SOURCES.txt'
reading manifest file 'glance.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
warning: no files found matching 'LICENSE'
warning: no files found matching 'ChangeLog'
warning: no files found matching 'tests/test_data.py'
writing manifest file 'glance.egg-info/SOURCES.txt'
running build_ext

Tests raises BadRequest for invalid store header ... ERROR
Tests to add a basic image in the file store ... ERROR
Tests creates a queued image for no body and no loc header ... ok
test_bad_container_format (tests.unit.test_api.TestGlanceAPI) ... ok
test_bad_disk_format (tests.unit.test_api.TestGlanceAPI) ... ok
test_delete_image (tests.unit.test_api.TestGlanceAPI) ... ERROR
test_delete_non_exists_image (tests.unit.test_api.TestGlanceAPI) ... ok
Test for HEAD /images/<ID> ... ok
test_show_image_basic (tests.unit.test_api.TestGlanceAPI) ... ERROR
test_show_non_exists_image (tests.unit.test_api.TestGlanceAPI) ... ok
Tests that the /images POST registry API creates the image ... ok
Tests proper exception is raised if a bad disk_format is set ... ok
Tests proper exception is raised if a bad disk_format is set ... ok
Tests proper exception is raised if a bad status is set ... ok
Tests that exception raised for bad matching disk and container ... ok
Tests that the /images DELETE registry API deletes the image ... ok
Tests proper exception is raised if attempt to delete non-existing ... ok
Tests that the /images/detail registry API returns ... ok
Tests that the /images registry API returns list of ... ok
Tests that the root registry API returns "index", ... ok
Tests that the /images PUT registry API updates the image ... ok
Tests proper exception is raised if attempt to update non-existing ... ok
Tests that exception raised trying to set a bad container_format ... ok
Tests that exception raised trying to set a bad disk_format ... ok
Tests that exception raised trying to set a bad status ... ok
Tests that exception raised for bad matching disk and container ... ok
Test ClientConnectionError raised ... ok
Tests proper exception is raised if image with ID already exists ... ok
Tests that we can add image metadata and returns the new id ... ok
Tests a bad status is set to a proper one by server ... ok
Tests BadRequest raised when supplying bad store name in meta ... ERROR
Tests can add image by passing image data as file ... ERROR
Tests can add image by passing image data as string ... ERROR
Tests add image by passing image data as string w/ no size attr ... ERROR
Tests that we can add image metadata with properties ... ok
Tests client returns image as queued ... ok
Tests that image metadata is deleted properly ... ERROR
Tests cannot delete non-existing image ... ok
Test a simple file backend retrieval works as expected ... ERROR
Tests that the detailed info about public images retu...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/source/installing.rst'
2--- doc/source/installing.rst 2011-02-04 23:59:52 +0000
3+++ doc/source/installing.rst 2011-03-07 20:15:11 +0000
4@@ -73,7 +73,7 @@
5
6 1. Install Bazaar and build dependencies::
7
8- $> sudo apt-get install bzr python-eventlet python-routes python-greenlet
9+ $> sudo apt-get install bzr python-eventlet python-routes python-greenlet swift
10 $> sudo apt-get install python-argparse python-sqlalchemy python-wsgiref python-pastedeploy
11
12 .. note::
13
14=== modified file 'etc/glance.conf.sample'
15--- etc/glance.conf.sample 2011-02-11 00:12:51 +0000
16+++ etc/glance.conf.sample 2011-03-07 20:15:11 +0000
17@@ -8,10 +8,6 @@
18 [app:glance-api]
19 paste.app_factory = glance.server:app_factory
20
21-# Directory that the Filesystem backend store
22-# writes image data to
23-filesystem_store_datadir=/var/lib/glance/images/
24-
25 # Which backend store should Glance use by default is not specified
26 # in a request to add a new image to Glance? Default: 'file'
27 # Available choices are 'file', 'swift', and 's3'
28@@ -29,6 +25,33 @@
29 # Port the registry server is listening on
30 registry_port = 9191
31
32+# ============ Filesystem Store Options ========================
33+
34+# Directory that the Filesystem backend store
35+# writes image data to
36+filesystem_store_datadir=/var/lib/glance/images/
37+
38+# ============ Swift Store Options =============================
39+
40+# Address where the Swift authentication service lives
41+# The auth address should be in the form:
42+# <DOMAIN>[:<PORT>]/<VERSION>/<ACCOUNT>
43+swift_store_auth_address = 127.0.0.1:8080/v1.0/glance-account
44+
45+# User to authenticate against the Swift authentication service
46+swift_store_user = jdoe
47+
48+# Auth key for the user authenticating against the
49+# Swift authentication service
50+swift_store_key = a86850deb2742ec3cb41518e26aa2d89
51+
52+# Container within the account that the account should use
53+# for storing images in Swift
54+swift_store_container = glance
55+
56+# Do we create the container if it does not exist?
57+swift_store_create_container_on_put = False
58+
59 [app:glance-registry]
60 paste.app_factory = glance.registry.server:app_factory
61
62
63=== modified file 'glance/server.py'
64--- glance/server.py 2011-03-05 16:04:32 +0000
65+++ glance/server.py 2011-03-07 20:15:11 +0000
66@@ -154,7 +154,8 @@
67
68 def image_iterator():
69 chunks = get_from_backend(image['location'],
70- expected_size=image['size'])
71+ expected_size=image['size'],
72+ options=self.options)
73
74 for chunk in chunks:
75 yield chunk
76
77=== modified file 'glance/store/filesystem.py'
78--- glance/store/filesystem.py 2011-01-28 21:54:34 +0000
79+++ glance/store/filesystem.py 2011-03-07 20:15:11 +0000
80@@ -19,12 +19,15 @@
81 A simple filesystem-backed store
82 """
83
84+import logging
85 import os
86 import urlparse
87
88 from glance.common import exception
89 import glance.store
90
91+logger = logging.getLogger('glance.store.filesystem')
92+
93
94 class ChunkedFile(object):
95
96@@ -60,8 +63,7 @@
97
98 class FilesystemBackend(glance.store.Backend):
99 @classmethod
100- def get(cls, parsed_uri, opener=lambda p: open(p, "rb"),
101- expected_size=None):
102+ def get(cls, parsed_uri, expected_size=None, options=None):
103 """ Filesystem-based backend
104
105 file:///path/to/file.tar.gz.0
106@@ -71,6 +73,8 @@
107 if not os.path.exists(filepath):
108 raise exception.NotFound("Image file %s not found" % filepath)
109 else:
110+ logger.debug("Found image at %s. Returning in ChunkedFile.",
111+ filepath)
112 return ChunkedFile(filepath)
113
114 @classmethod
115@@ -87,6 +91,7 @@
116 fn = parsed_uri.path
117 if os.path.exists(fn):
118 try:
119+ logger.debug("Deleting image at %s", fn)
120 os.unlink(fn)
121 except OSError:
122 raise exception.NotAuthorized("You cannot delete file %s" % fn)
123@@ -112,6 +117,8 @@
124 datadir = options['filesystem_store_datadir']
125
126 if not os.path.exists(datadir):
127+ logger.info("Directory to write image files does not exist "
128+ "(%s). Creating.", datadir)
129 os.makedirs(datadir)
130
131 filepath = os.path.join(datadir, str(id))
132@@ -129,6 +136,8 @@
133 bytes_written += len(buf)
134 f.write(buf)
135
136+ logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s"
137+ % locals())
138 return ('file://%s' % filepath, bytes_written)
139
140 @classmethod
141
142=== modified file 'glance/store/swift.py'
143--- glance/store/swift.py 2011-01-27 04:19:13 +0000
144+++ glance/store/swift.py 2011-03-07 20:15:11 +0000
145@@ -1,6 +1,6 @@
146 # vim: tabstop=4 shiftwidth=4 softtabstop=4
147
148-# Copyright 2010 OpenStack, LLC
149+# Copyright 2010-2011 OpenStack, LLC
150 # All Rights Reserved.
151 #
152 # Licensed under the Apache License, Version 2.0 (the "License"); you may
153@@ -15,107 +15,264 @@
154 # License for the specific language governing permissions and limitations
155 # under the License.
156
157+"""Storage backend for SWIFT"""
158+
159 from __future__ import absolute_import
160+
161+import httplib
162+import logging
163+
164+from swift.common.client import Connection, ClientException
165+
166+from glance.common import config
167+from glance.common import exception
168 import glance.store
169
170+DEFAULT_SWIFT_CONTAINER = 'glance'
171+
172+logger = logging.getLogger('glance.store.swift')
173+
174
175 class SwiftBackend(glance.store.Backend):
176 """
177 An implementation of the swift backend adapter.
178 """
179- EXAMPLE_URL = "swift://user:password@auth_url/container/file.gz.0"
180+ EXAMPLE_URL = "swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<FILE>"
181
182 CHUNKSIZE = 65536
183
184 @classmethod
185- def get(cls, parsed_uri, expected_size, conn_class=None):
186+ def get(cls, parsed_uri, expected_size=None, options=None):
187 """
188 Takes a parsed_uri in the format of:
189 swift://user:password@auth_url/container/file.gz.0, connects to the
190 swift instance at auth_url and downloads the file. Returns the
191 generator resp_body provided by get_object.
192 """
193- (user, key, authurl, container, obj) = \
194- cls._parse_swift_tokens(parsed_uri)
195+ (user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri)
196
197 # TODO(sirp): snet=False for now, however, if the instance of
198 # swift we're talking to is within our same region, we should set
199 # snet=True
200- connection_class = get_connection_class(conn_class)
201-
202- swift_conn = conn_class(
203+ swift_conn = Connection(
204 authurl=authurl, user=user, key=key, snet=False)
205
206- (resp_headers, resp_body) = swift_conn.get_object(
207- container=container, obj=obj, resp_chunk_size=cls.CHUNKSIZE)
208+ try:
209+ (resp_headers, resp_body) = swift_conn.get_object(
210+ container=container, obj=obj, resp_chunk_size=cls.CHUNKSIZE)
211+ except ClientException, e:
212+ if e.http_status == httplib.NOT_FOUND:
213+ location = format_swift_location(user, key, authurl,
214+ container, obj)
215+ raise exception.NotFound("Swift could not find image at "
216+ "location %(location)s" % locals())
217
218- obj_size = int(resp_headers['content-length'])
219- if obj_size != expected_size:
220- raise glance.store.BackendException(
221- "Expected %s byte file, Swift has %s bytes" %
222- (expected_size, obj_size))
223+ if expected_size:
224+ obj_size = int(resp_headers['content-length'])
225+ if obj_size != expected_size:
226+ raise glance.store.BackendException(
227+ "Expected %s byte file, Swift has %s bytes" %
228+ (expected_size, obj_size))
229
230 return resp_body
231
232 @classmethod
233- def delete(cls, parsed_uri, conn_class=None):
234+ def add(cls, id, data, options):
235+ """
236+ Stores image data to Swift and returns a location that the image was
237+ written to.
238+
239+ Swift writes the image data using the scheme:
240+ ``swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<ID>`
241+ where:
242+ <USER> = ``swift_store_user``
243+ <KEY> = ``swift_store_key``
244+ <AUTH_ADDRESS> = ``swift_store_auth_address``
245+ <CONTAINER> = ``swift_store_container``
246+ <ID> = The id of the image being added
247+
248+ :param id: The opaque image identifier
249+ :param data: The image data to write, as a file-like object
250+ :param options: Conf mapping
251+
252+ :retval Tuple with (location, size)
253+ The location that was written,
254+ and the size in bytes of the data written
255+ """
256+ container = options.get('swift_store_container',
257+ DEFAULT_SWIFT_CONTAINER)
258+ auth_address = options.get('swift_store_auth_address')
259+ user = options.get('swift_store_user')
260+ key = options.get('swift_store_key')
261+
262+ # TODO(jaypipes): This needs to be checked every time
263+ # because of the decision to make glance.store.Backend's
264+ # interface all @classmethods. This is inefficient. Backend
265+ # should be a stateful object with options parsed once in
266+ # a constructor.
267+ if not auth_address:
268+ logger.error(msg)
269+ msg = ("Could not find swift_store_auth_address in configuration "
270+ "options.")
271+ raise glance.store.BackendException(msg)
272+ else:
273+ full_auth_address = auth_address
274+ if not full_auth_address.startswith('http'):
275+ full_auth_address = 'https://' + full_auth_address
276+
277+ if not user:
278+ logger.error(msg)
279+ msg = ("Could not find swift_store_user in configuration "
280+ "options.")
281+ raise glance.store.BackendException(msg)
282+
283+ if not key:
284+ logger.error(msg)
285+ msg = ("Could not find swift_store_key in configuration "
286+ "options.")
287+ raise glance.store.BackendException(msg)
288+
289+ swift_conn = Connection(authurl=full_auth_address, user=user,
290+ key=key, snet=False)
291+
292+ logger.debug("Adding image object to Swift using "
293+ "(auth_address=%(auth_address)s, user=%(user)s, "
294+ "key=%(key)s)" % locals())
295+
296+ create_container_if_missing(container, swift_conn, options)
297+
298+ obj_name = str(id)
299+ location = format_swift_location(user, key, auth_address,
300+ container, obj_name)
301+ try:
302+ obj_etag = swift_conn.put_object(container, obj_name, data)
303+
304+ # NOTE: We return the user and key here! Have to because
305+ # location is used by the API server to return the actual
306+ # image data. We *really* should consider NOT returning
307+ # the location attribute from GET /images/<ID> and
308+ # GET /images/details
309+
310+ # We do a HEAD on the newly-added image to determine the size
311+ # of the image. A bit slow, but better than taking the word
312+ # of the user adding the image with size attribute in the metadata
313+ resp_headers = swift_conn.head_object(container, obj_name)
314+ size = 0
315+ # header keys are lowercased by Swift
316+ if 'content-length' in resp_headers:
317+ size = int(resp_headers['content-length'])
318+ return (location, size)
319+ except ClientException, e:
320+ if e.http_status == httplib.CONFLICT:
321+ raise exception.Duplicate("Swift already has an image at "
322+ "location %(location)s" % locals())
323+ msg = ("Failed to add object to Swift.\n"
324+ "Got error from Swift: %(e)s" % locals())
325+ raise glance.store.BackendException(msg)
326+
327+ @classmethod
328+ def delete(cls, parsed_uri):
329 """
330 Deletes the swift object(s) at the parsed_uri location
331 """
332- (user, key, authurl, container, obj) = \
333- cls._parse_swift_tokens(parsed_uri)
334+ (user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri)
335
336 # TODO(sirp): snet=False for now, however, if the instance of
337 # swift we're talking to is within our same region, we should set
338 # snet=True
339- connection_class = get_connection_class(conn_class)
340-
341- swift_conn = conn_class(
342+ swift_conn = Connection(
343 authurl=authurl, user=user, key=key, snet=False)
344
345- (resp_headers, resp_body) = swift_conn.delete_object(
346- container=container, obj=obj)
347-
348- # TODO(jaypipes): What to return here? After reading the docs
349- # at swift.common.client, I'm not sure what to check for...
350-
351- @classmethod
352- def _parse_swift_tokens(cls, parsed_uri):
353- """
354- Parsing the swift uri is three phases:
355- 1) urlparse to split the tokens
356- 2) use RE to split on @ and /
357- 3) reassemble authurl
358- """
359- path = parsed_uri.path.lstrip('//')
360- netloc = parsed_uri.netloc
361-
362- try:
363- try:
364- creds, netloc = netloc.split('@')
365- path = '/'.join([netloc, path])
366- except ValueError:
367- # Python 2.6.1 compat
368- # see lp659445 and Python issue7904
369- creds, path = path.split('@')
370-
371- user, key = creds.split(':')
372- path_parts = path.split('/')
373- obj = path_parts.pop()
374- container = path_parts.pop()
375- except (ValueError, IndexError):
376- raise glance.store.BackendException(
377- "Expected four values to unpack in: swift:%s. "
378- "Should have received something like: %s."
379- % (parsed_uri.path, cls.EXAMPLE_URL))
380-
381- authurl = "https://%s" % '/'.join(path_parts)
382-
383- return user, key, authurl, container, obj
384-
385-
386-def get_connection_class(conn_class):
387- if not conn_class:
388- import swift.common.client
389- conn_class = swift.common.client.Connection
390- return conn_class
391+ try:
392+ swift_conn.delete_object(container, obj)
393+ except ClientException, e:
394+ if e.http_status == httplib.NOT_FOUND:
395+ location = format_swift_location(user, key, authurl,
396+ container, obj)
397+ raise exception.NotFound("Swift could not find image at "
398+ "location %(location)s" % locals())
399+ else:
400+ raise
401+
402+
403+def parse_swift_tokens(parsed_uri):
404+ """
405+ Return the various tokens used by Swift.
406+
407+ :param parsed_uri: The pieces of a URI returned by urlparse
408+ :retval A tuple of (user, key, auth_address, container, obj_name)
409+ """
410+ path = parsed_uri.path.lstrip('//')
411+ netloc = parsed_uri.netloc
412+
413+ try:
414+ try:
415+ creds, netloc = netloc.split('@')
416+ path = '/'.join([netloc, path])
417+ except ValueError:
418+ # Python 2.6.1 compat
419+ # see lp659445 and Python issue7904
420+ creds, path = path.split('@')
421+
422+ user, key = creds.split(':')
423+ path_parts = path.split('/')
424+ obj = path_parts.pop()
425+ container = path_parts.pop()
426+ except (ValueError, IndexError):
427+ raise glance.store.BackendException(
428+ "Expected four values to unpack in: swift:%s. "
429+ "Should have received something like: %s."
430+ % (parsed_uri.path, SwiftBackend.EXAMPLE_URL))
431+
432+ authurl = "https://%s" % '/'.join(path_parts)
433+
434+ return user, key, authurl, container, obj
435+
436+
437+def format_swift_location(user, key, auth_address, container, obj_name):
438+ """
439+ Returns the swift URI in the format:
440+ swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<OBJNAME>
441+
442+ :param user: The swift user to authenticate with
443+ :param key: The auth key for the authenticating user
444+ :param auth_address: The base URL for the authentication service
445+ :param container: The name of the container
446+ :param obj_name: The name of the object
447+ """
448+ return "swift://%(user)s:%(key)s@%(auth_address)s/"\
449+ "%(container)s/%(obj_name)s" % locals()
450+
451+
452+def create_container_if_missing(container, swift_conn, options):
453+ """
454+ Creates a missing container in Swift if the
455+ ``swift_store_create_container_on_put`` option is set.
456+
457+ :param container: Name of container to create
458+ :param swift_conn: Connection to Swift
459+ :param options: Option mapping
460+ """
461+ try:
462+ swift_conn.head_container(container)
463+ except ClientException, e:
464+ if e.http_status == httplib.NOT_FOUND:
465+ add_container = config.get_option(options,
466+ 'swift_store_create_container_on_put',
467+ type='bool', default=False)
468+ if add_container:
469+ try:
470+ swift_conn.put_container(container)
471+ except ClientException, e:
472+ msg = ("Failed to add container to Swift.\n"
473+ "Got error from Swift: %(e)s" % locals())
474+ raise glance.store.BackendException(msg)
475+ else:
476+ msg = ("The container %(container)s does not exist in "
477+ "Swift. Please set the "
478+ "swift_store_create_container_on_put option"
479+ "to add container to Swift automatically."
480+ % locals())
481+ raise glance.store.BackendException(msg)
482+ else:
483+ raise
484
485=== modified file 'tests/stubs.py'
486--- tests/stubs.py 2011-02-25 14:55:26 +0000
487+++ tests/stubs.py 2011-03-07 20:15:11 +0000
488@@ -33,7 +33,6 @@
489 import glance.store
490 import glance.store.filesystem
491 import glance.store.http
492-import glance.store.swift
493 import glance.registry.db.api
494
495
496@@ -109,7 +108,7 @@
497 def stub_out_s3_backend(stubs):
498 """ Stubs out the S3 Backend with fake data and calls.
499
500- The stubbed swift backend provides back an iterator over
501+ The stubbed s3 backend provides back an iterator over
502 the data ""
503
504 :param stubs: Set of stubout stubs
505@@ -139,78 +138,9 @@
506 yield cls.DATA[i:i + cls.CHUNK_SIZE]
507 return chunk_it()
508
509- fake_swift_backend = FakeS3Backend()
510+ fake_s3_backend = FakeS3Backend()
511 stubs.Set(glance.store.s3.S3Backend, 'get',
512- fake_swift_backend.get)
513-
514-
515-def stub_out_swift_backend(stubs):
516- """Stubs out the Swift Glance backend with fake data
517- and calls.
518-
519- The stubbed swift backend provides back an iterator over
520- the data "I am a teapot, short and stout\n"
521-
522- :param stubs: Set of stubout stubs
523-
524- """
525- class FakeSwiftAuth(object):
526- pass
527-
528- class FakeSwiftConnection(object):
529- pass
530-
531- class FakeSwiftBackend(object):
532-
533- CHUNK_SIZE = 2
534- DATA = 'I am a teapot, short and stout\n'
535-
536- @classmethod
537- def get(cls, parsed_uri, expected_size, conn_class=None):
538- SwiftBackend = glance.store.swift.SwiftBackend
539-
540- # raise BackendException if URI is bad.
541- (user, key, authurl, container, obj) = \
542- SwiftBackend._parse_swift_tokens(parsed_uri)
543-
544- def chunk_it():
545- for i in xrange(0, len(cls.DATA), cls.CHUNK_SIZE):
546- yield cls.DATA[i:i + cls.CHUNK_SIZE]
547-
548- return chunk_it()
549-
550- fake_swift_backend = FakeSwiftBackend()
551- stubs.Set(glance.store.swift.SwiftBackend, 'get',
552- fake_swift_backend.get)
553-
554-
555-def stub_out_registry(stubs):
556- """Stubs out the Registry registry with fake data returns.
557-
558- The stubbed Registry always returns the following fixture::
559-
560- {'files': [
561- {'location': 'file:///chunk0', 'size': 12345},
562- {'location': 'file:///chunk1', 'size': 1235}
563- ]}
564-
565- :param stubs: Set of stubout stubs
566-
567- """
568- class FakeRegistry(object):
569-
570- DATA = \
571- {'files': [
572- {'location': 'file:///chunk0', 'size': 12345},
573- {'location': 'file:///chunk1', 'size': 1235}]}
574-
575- @classmethod
576- def lookup(cls, _parsed_uri):
577- return cls.DATA
578-
579- fake_registry_registry = FakeRegistry()
580- stubs.Set(glance.store.registries.Registry, 'lookup',
581- fake_registry_registry.lookup)
582+ fake_s3_backend.get)
583
584
585 def stub_out_registry_and_store_server(stubs):
586
587=== removed file 'tests/unit/swiftfakehttp.py'
588--- tests/unit/swiftfakehttp.py 2011-01-27 04:19:13 +0000
589+++ tests/unit/swiftfakehttp.py 1970-01-01 00:00:00 +0000
590@@ -1,294 +0,0 @@
591-# vim: tabstop=4 shiftwidth=4 softtabstop=4
592-
593-# Copyright 2010 OpenStack, LLC
594-# All Rights Reserved.
595-#
596-# Licensed under the Apache License, Version 2.0 (the "License"); you may
597-# not use this file except in compliance with the License. You may obtain
598-# a copy of the License at
599-#
600-# http://www.apache.org/licenses/LICENSE-2.0
601-#
602-# Unless required by applicable law or agreed to in writing, software
603-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
604-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
605-# License for the specific language governing permissions and limitations
606-# under the License.
607-
608-"""
609-fakehttp/socket implementation
610-
611-- TrackerSocket: an object which masquerades as a socket and responds to
612- requests in a manner consistent with a *very* stupid CloudFS tracker.
613-
614-- CustomHTTPConnection: an object which subclasses httplib.HTTPConnection
615- in order to replace it's socket with a TrackerSocket instance.
616-
617-The unittests each have setup methods which create freerange connection
618-instances that have had their HTTPConnection instances replaced by
619-intances of CustomHTTPConnection.
620-"""
621-
622-from httplib import HTTPConnection as connbase
623-import StringIO
624-
625-
626-class FakeSocket(object):
627- def __init__(self):
628- self._rbuffer = StringIO.StringIO()
629- self._wbuffer = StringIO.StringIO()
630-
631- def close(self):
632- pass
633-
634- def send(self, data, flags=0):
635- self._rbuffer.write(data)
636- sendall = send
637-
638- def recv(self, len=1024, flags=0):
639- return self._wbuffer(len)
640-
641- def connect(self):
642- pass
643-
644- def makefile(self, mode, flags):
645- return self._wbuffer
646-
647-
648-class TrackerSocket(FakeSocket):
649- def write(self, data):
650- self._wbuffer.write(data)
651-
652- def read(self, length=-1):
653- return self._rbuffer.read(length)
654-
655- def _create_GET_account_content(self, path, args):
656- if 'format' in args and args['format'] == 'json':
657- containers = []
658- containers.append('[\n')
659- containers.append('{"name":"container1","count":2,"bytes":78},\n')
660- containers.append('{"name":"container2","count":1,"bytes":39},\n')
661- containers.append('{"name":"container3","count":3,"bytes":117}\n')
662- containers.append(']\n')
663- elif 'format' in args and args['format'] == 'xml':
664- containers = []
665- containers.append('<?xml version="1.0" encoding="UTF-8"?>\n')
666- containers.append('<account name="FakeAccount">\n')
667- containers.append('<container><name>container1</name>'
668- '<count>2</count>'
669- '<bytes>78</bytes></container>\n')
670- containers.append('<container><name>container2</name>'
671- '<count>1</count>'
672- '<bytes>39</bytes></container>\n')
673- containers.append('<container><name>container3</name>'
674- '<count>3</count>'
675- '<bytes>117</bytes></container>\n')
676- containers.append('</account>\n')
677- else:
678- containers = ['container%s\n' % i for i in range(1, 4)]
679- return ''.join(containers)
680-
681- def _create_GET_container_content(self, path, args):
682- left = 0
683- right = 9
684- if 'offset' in args:
685- left = int(args['offset'])
686- if 'limit' in args:
687- right = left + int(args['limit'])
688-
689- if 'format' in args and args['format'] == 'json':
690- objects = []
691- objects.append('{"name":"object1",'
692- '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
693- '"bytes":14,'
694- '"content_type":"application\/octet-stream",'
695- '"last_modified":"2007-03-04 20:32:17"}')
696- objects.append('{"name":"object2",'
697- '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
698- '"bytes":64,'
699- '"content_type":"application\/octet-stream",'
700- '"last_modified":"2007-03-04 20:32:17"}')
701- objects.append('{"name":"object3",'
702- '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
703- '"bytes":14,'
704- '"content_type":"application\/octet-stream",'
705- '"last_modified":"2007-03-04 20:32:17"}')
706- objects.append('{"name":"object4",'
707- '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
708- '"bytes":64,'
709- '"content_type":"application\/octet-stream",'
710- '"last_modified":"2007-03-04 20:32:17"}')
711- objects.append('{"name":"object5",'
712- '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
713- '"bytes":14,'
714- '"content_type":"application\/octet-stream",'
715- '"last_modified":"2007-03-04 20:32:17"}')
716- objects.append('{"name":"object6",'
717- '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
718- '"bytes":64,'
719- '"content_type":"application\/octet-stream",'
720- '"last_modified":"2007-03-04 20:32:17"}')
721- objects.append('{"name":"object7",'
722- '"hash":"4281c348eaf83e70ddce0e07221c3d28",'
723- '"bytes":14,'
724- '"content_type":"application\/octet-stream",'
725- '"last_modified":"2007-03-04 20:32:17"}')
726- objects.append('{"name":"object8",'
727- '"hash":"b039efe731ad111bc1b0ef221c3849d0",'
728- '"bytes":64,'
729- '"content_type":"application\/octet-stream",'
730- '"last_modified":"2007-03-04 20:32:17"}')
731- output = '[\n%s\n]\n' % (',\n'.join(objects[left:right]))
732- elif 'format' in args and args['format'] == 'xml':
733- objects = []
734- objects.append('<object><name>object1</name>'
735- '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
736- '<bytes>14</bytes>'
737- '<content_type>application/octet-stream</content_type>'
738- '<last_modified>2007-03-04 20:32:17</last_modified>'
739- '</object>\n')
740- objects.append('<object><name>object2</name>'
741- '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
742- '<bytes>64</bytes>'
743- '<content_type>application/octet-stream</content_type>'
744- '<last_modified>2007-03-04 20:32:17</last_modified>'
745- '</object>\n')
746- objects.append('<object><name>object3</name>'
747- '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
748- '<bytes>14</bytes>'
749- '<content_type>application/octet-stream</content_type>'
750- '<last_modified>2007-03-04 20:32:17</last_modified>'
751- '</object>\n')
752- objects.append('<object><name>object4</name>'
753- '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
754- '<bytes>64</bytes>'
755- '<content_type>application/octet-stream</content_type>'
756- '<last_modified>2007-03-04 20:32:17</last_modified>'
757- '</object>\n')
758- objects.append('<object><name>object5</name>'
759- '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
760- '<bytes>14</bytes>'
761- '<content_type>application/octet-stream</content_type>'
762- '<last_modified>2007-03-04 20:32:17</last_modified>'
763- '</object>\n')
764- objects.append('<object><name>object6</name>'
765- '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
766- '<bytes>64</bytes>'
767- '<content_type>application/octet-stream</content_type>'
768- '<last_modified>2007-03-04 20:32:17</last_modified>'
769- '</object>\n')
770- objects.append('<object><name>object7</name>'
771- '<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
772- '<bytes>14</bytes>'
773- '<content_type>application/octet-stream</content_type>'
774- '<last_modified>2007-03-04 20:32:17</last_modified>'
775- '</object>\n')
776- objects.append('<object><name>object8</name>'
777- '<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
778- '<bytes>64</bytes>'
779- '<content_type>application/octet-stream</content_type>'
780- '<last_modified>2007-03-04 20:32:17</last_modified>'
781- '</object>\n')
782- objects = objects[left:right]
783- objects.insert(0, '<?xml version="1.0" encoding="UTF-8"?>\n')
784- objects.insert(1, '<container name="test_container_1"\n')
785- objects.append('</container>\n')
786- output = ''.join(objects)
787- else:
788- objects = ['object%s\n' % i for i in range(1, 9)]
789- objects = objects[left:right]
790- output = ''.join(objects)
791-
792- # prefix/path don't make much sense given our test data
793- if 'prefix' in args or 'path' in args:
794- pass
795- return output
796-
797- def render_GET(self, path, args):
798- # Special path that returns 404 Not Found
799- if (len(path) == 4) and (path[3] == 'bogus'):
800- self.write('HTTP/1.1 404 Not Found\n')
801- self.write('Content-Type: text/plain\n')
802- self.write('Content-Length: 0\n')
803- self.write('Connection: close\n\n')
804- return
805-
806- self.write('HTTP/1.1 200 Ok\n')
807- self.write('Content-Type: text/plain\n')
808- if len(path) == 2:
809- content = self._create_GET_account_content(path, args)
810- elif len(path) == 3:
811- content = self._create_GET_container_content(path, args)
812- # Object
813- elif len(path) == 4:
814- content = 'I am a teapot, short and stout\n'
815- self.write('Content-Length: %d\n' % len(content))
816- self.write('Connection: close\n\n')
817- self.write(content)
818-
819- def render_HEAD(self, path, args):
820- # Account
821- if len(path) == 2:
822- self.write('HTTP/1.1 204 No Content\n')
823- self.write('Content-Type: text/plain\n')
824- self.write('Connection: close\n')
825- self.write('X-Account-Container-Count: 3\n')
826- self.write('X-Account-Bytes-Used: 234\n\n')
827- else:
828- self.write('HTTP/1.1 200 Ok\n')
829- self.write('Content-Type: text/plain\n')
830- self.write('ETag: d5c7f3babf6c602a8da902fb301a9f27\n')
831- self.write('Content-Length: 21\n')
832- self.write('Connection: close\n\n')
833-
834- def render_POST(self, path, args):
835- self.write('HTTP/1.1 202 Ok\n')
836- self.write('Connection: close\n\n')
837-
838- def render_PUT(self, path, args):
839- self.write('HTTP/1.1 200 Ok\n')
840- self.write('Content-Type: text/plain\n')
841- self.write('Connection: close\n\n')
842- render_DELETE = render_PUT
843-
844- def render(self, method, uri):
845- if '?' in uri:
846- parts = uri.split('?')
847- query = parts[1].strip('&').split('&')
848- args = dict([tuple(i.split('=', 1)) for i in query])
849- path = parts[0].strip('/').split('/')
850- else:
851- args = {}
852- path = uri.strip('/').split('/')
853-
854- if hasattr(self, 'render_%s' % method):
855- getattr(self, 'render_%s' % method)(path, args)
856- else:
857- self.write('HTTP/1.1 406 Not Acceptable\n')
858- self.write('Content-Type: text/plain\n')
859- self.write('Connection: close\n')
860-
861- def makefile(self, mode, flags):
862- self._rbuffer.seek(0)
863- lines = self.read().splitlines()
864- (method, uri, version) = lines[0].split()
865-
866- self.render(method, uri)
867-
868- self._wbuffer.seek(0)
869- return self._wbuffer
870-
871-
872-class CustomHTTPConnection(connbase):
873- def connect(self):
874- self.sock = TrackerSocket()
875-
876-
877-if __name__ == '__main__':
878- conn = CustomHTTPConnection('localhost', 8000)
879- conn.request('HEAD', '/v1/account/container/object')
880- response = conn.getresponse()
881- print "Status:", response.status, response.reason
882- for (key, value) in response.getheaders():
883- print "%s: %s" % (key, value)
884- print response.read()
885
886=== added file 'tests/unit/test_filesystem_store.py'
887--- tests/unit/test_filesystem_store.py 1970-01-01 00:00:00 +0000
888+++ tests/unit/test_filesystem_store.py 2011-03-07 20:15:11 +0000
889@@ -0,0 +1,135 @@
890+# vim: tabstop=4 shiftwidth=4 softtabstop=4
891+
892+# Copyright 2011 OpenStack, LLC
893+# All Rights Reserved.
894+#
895+# Licensed under the Apache License, Version 2.0 (the "License"); you may
896+# not use this file except in compliance with the License. You may obtain
897+# a copy of the License at
898+#
899+# http://www.apache.org/licenses/LICENSE-2.0
900+#
901+# Unless required by applicable law or agreed to in writing, software
902+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
903+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
904+# License for the specific language governing permissions and limitations
905+# under the License.
906+
907+"""Tests the filesystem backend store"""
908+
909+import StringIO
910+import unittest
911+import urlparse
912+
913+import stubout
914+
915+from glance.common import exception
916+from glance.store.filesystem import FilesystemBackend, ChunkedFile
917+from tests import stubs
918+
919+
920+class TestFilesystemBackend(unittest.TestCase):
921+
922+ def setUp(self):
923+ """Establish a clean test environment"""
924+ self.stubs = stubout.StubOutForTesting()
925+ stubs.stub_out_filesystem_backend()
926+ self.orig_chunksize = ChunkedFile.CHUNKSIZE
927+ ChunkedFile.CHUNKSIZE = 10
928+
929+ def tearDown(self):
930+ """Clear the test environment"""
931+ stubs.clean_out_fake_filesystem_backend()
932+ self.stubs.UnsetAll()
933+ ChunkedFile.CHUNKSIZE = self.orig_chunksize
934+
935+ def test_get(self):
936+ """Test a "normal" retrieval of an image in chunks"""
937+ url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2")
938+ image_file = FilesystemBackend.get(url_pieces)
939+
940+ expected_data = "chunk00000remainder"
941+ expected_num_chunks = 2
942+ data = ""
943+ num_chunks = 0
944+
945+ for chunk in image_file:
946+ num_chunks += 1
947+ data += chunk
948+ self.assertEqual(expected_data, data)
949+ self.assertEqual(expected_num_chunks, num_chunks)
950+
951+ def test_get_non_existing(self):
952+ """
953+ Test that trying to retrieve a file that doesn't exist
954+ raises an error
955+ """
956+ url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing")
957+ self.assertRaises(exception.NotFound,
958+ FilesystemBackend.get,
959+ url_pieces)
960+
961+ def test_add(self):
962+ """Test that we can add an image via the filesystem backend"""
963+ ChunkedFile.CHUNKSIZE = 1024
964+ expected_image_id = 42
965+ expected_file_size = 1024 * 5 # 5K
966+ expected_file_contents = "*" * expected_file_size
967+ expected_location = "file://%s/%s" % (stubs.FAKE_FILESYSTEM_ROOTDIR,
968+ expected_image_id)
969+ image_file = StringIO.StringIO(expected_file_contents)
970+ options = {'verbose': True,
971+ 'debug': True,
972+ 'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
973+
974+ location, size = FilesystemBackend.add(42, image_file, options)
975+
976+ self.assertEquals(expected_location, location)
977+ self.assertEquals(expected_file_size, size)
978+
979+ url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42")
980+ new_image_file = FilesystemBackend.get(url_pieces)
981+ new_image_contents = ""
982+ new_image_file_size = 0
983+
984+ for chunk in new_image_file:
985+ new_image_file_size += len(chunk)
986+ new_image_contents += chunk
987+
988+ self.assertEquals(expected_file_contents, new_image_contents)
989+ self.assertEquals(expected_file_size, new_image_file_size)
990+
991+ def test_add_already_existing(self):
992+ """
993+ Tests that adding an image with an existing identifier
994+ raises an appropriate exception
995+ """
996+ image_file = StringIO.StringIO("nevergonnamakeit")
997+ options = {'verbose': True,
998+ 'debug': True,
999+ 'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
1000+ self.assertRaises(exception.Duplicate,
1001+ FilesystemBackend.add,
1002+ 2, image_file, options)
1003+
1004+ def test_delete(self):
1005+ """
1006+ Test we can delete an existing image in the filesystem store
1007+ """
1008+ url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2")
1009+
1010+ FilesystemBackend.delete(url_pieces)
1011+
1012+ self.assertRaises(exception.NotFound,
1013+ FilesystemBackend.get,
1014+ url_pieces)
1015+
1016+ def test_delete_non_existing(self):
1017+ """
1018+ Test that trying to delete a file that doesn't exist
1019+ raises an error
1020+ """
1021+ url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing")
1022+ self.assertRaises(exception.NotFound,
1023+ FilesystemBackend.delete,
1024+ url_pieces)
1025
1026=== modified file 'tests/unit/test_stores.py'
1027--- tests/unit/test_stores.py 2011-01-26 20:47:01 +0000
1028+++ tests/unit/test_stores.py 2011-03-07 20:15:11 +0000
1029@@ -22,7 +22,6 @@
1030 import urlparse
1031
1032 from glance.store.s3 import S3Backend
1033-from glance.store.swift import SwiftBackend
1034 from glance.store import Backend, BackendException, get_from_backend
1035 from tests import stubs
1036
1037@@ -40,27 +39,6 @@
1038 self.stubs.UnsetAll()
1039
1040
1041-class TestFilesystemBackend(TestBackend):
1042-
1043- def setUp(self):
1044- """Establish a clean test environment"""
1045- stubs.stub_out_filesystem_backend()
1046-
1047- def tearDown(self):
1048- """Clear the test environment"""
1049- stubs.clean_out_fake_filesystem_backend()
1050-
1051- def test_get(self):
1052-
1053- fetcher = get_from_backend("file:///tmp/glance-tests/2",
1054- expected_size=19)
1055-
1056- data = ""
1057- for chunk in fetcher:
1058- data += chunk
1059- self.assertEqual(data, "chunk00000remainder")
1060-
1061-
1062 class TestHTTPBackend(TestBackend):
1063
1064 def setUp(self):
1065@@ -104,45 +82,3 @@
1066
1067 chunks = [c for c in fetcher]
1068 self.assertEqual(chunks, expected_returns)
1069-
1070-
1071-class TestSwiftBackend(TestBackend):
1072- def setUp(self):
1073- super(TestSwiftBackend, self).setUp()
1074- stubs.stub_out_swift_backend(self.stubs)
1075-
1076- def test_get(self):
1077-
1078- swift_uri = "swift://user:pass@localhost/container1/file.tar.gz"
1079- expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s',
1080- 'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n']
1081-
1082- fetcher = get_from_backend(swift_uri,
1083- expected_size=21,
1084- conn_class=SwiftBackend)
1085-
1086- chunks = [c for c in fetcher]
1087-
1088- self.assertEqual(chunks, expected_returns)
1089-
1090- def test_get_bad_uri(self):
1091-
1092- swift_url = "swift://localhost/container1/file.tar.gz"
1093-
1094- self.assertRaises(BackendException, get_from_backend,
1095- swift_url, expected_size=21)
1096-
1097- def test_url_parsing(self):
1098-
1099- swift_uri = "swift://user:pass@localhost/v1.0/container1/file.tar.gz"
1100-
1101- parsed_uri = urlparse.urlparse(swift_uri)
1102-
1103- (user, key, authurl, container, obj) = \
1104- SwiftBackend._parse_swift_tokens(parsed_uri)
1105-
1106- self.assertEqual(user, 'user')
1107- self.assertEqual(key, 'pass')
1108- self.assertEqual(authurl, 'https://localhost/v1.0')
1109- self.assertEqual(container, 'container1')
1110- self.assertEqual(obj, 'file.tar.gz')
1111
1112=== added file 'tests/unit/test_swift_store.py'
1113--- tests/unit/test_swift_store.py 1970-01-01 00:00:00 +0000
1114+++ tests/unit/test_swift_store.py 2011-03-07 20:15:11 +0000
1115@@ -0,0 +1,300 @@
1116+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1117+
1118+# Copyright 2011 OpenStack, LLC
1119+# All Rights Reserved.
1120+#
1121+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1122+# not use this swift except in compliance with the License. You may obtain
1123+# a copy of the License at
1124+#
1125+# http://www.apache.org/licenses/LICENSE-2.0
1126+#
1127+# Unless required by applicable law or agreed to in writing, software
1128+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1129+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1130+# License for the specific language governing permissions and limitations
1131+# under the License.
1132+
1133+"""Tests the Swift backend store"""
1134+
1135+import StringIO
1136+import hashlib
1137+import httplib
1138+import sys
1139+import unittest
1140+import urlparse
1141+
1142+import stubout
1143+import swift.common.client
1144+
1145+from glance.common import exception
1146+from glance.store import BackendException
1147+from glance.store.swift import SwiftBackend, format_swift_location
1148+
1149+FIVE_KB = (5 * 1024)
1150+SWIFT_OPTIONS = {'verbose': True,
1151+ 'debug': True,
1152+ 'swift_store_user': 'user',
1153+ 'swift_store_key': 'key',
1154+ 'swift_store_auth_address': 'localhost:8080',
1155+ 'swift_store_container': 'glance'}
1156+
1157+
1158+# We stub out as little as possible to ensure that the code paths
1159+# between glance.store.swift and swift.common.client are tested
1160+# thoroughly
1161+def stub_out_swift_common_client(stubs):
1162+
1163+ fixture_containers = ['glance']
1164+ fixture_headers = {'glance/2':
1165+ {'content-length': FIVE_KB,
1166+ 'etag': 'c2e5db72bd7fd153f53ede5da5a06de3'}}
1167+ fixture_objects = {'glance/2':
1168+ StringIO.StringIO("*" * FIVE_KB)}
1169+
1170+ def fake_head_container(url, token, container, **kwargs):
1171+ if container not in fixture_containers:
1172+ msg = "No container %s found" % container
1173+ raise swift.common.client.ClientException(msg,
1174+ http_status=httplib.NOT_FOUND)
1175+
1176+ def fake_put_container(url, token, container, **kwargs):
1177+ fixture_containers.append(container)
1178+
1179+ def fake_put_object(url, token, container, name, contents, **kwargs):
1180+ # PUT returns the ETag header for the newly-added object
1181+ fixture_key = "%s/%s" % (container, name)
1182+ if not fixture_key in fixture_headers.keys():
1183+ if hasattr(contents, 'read'):
1184+ fixture_object = StringIO.StringIO()
1185+ chunk = contents.read(SwiftBackend.CHUNKSIZE)
1186+ while chunk:
1187+ fixture_object.write(chunk)
1188+ chunk = contents.read(SwiftBackend.CHUNKSIZE)
1189+ else:
1190+ fixture_object = StringIO.StringIO(contents)
1191+ fixture_objects[fixture_key] = fixture_object
1192+ fixture_headers[fixture_key] = {
1193+ 'content-length': fixture_object.len,
1194+ 'etag': hashlib.md5(fixture_object.read()).hexdigest()}
1195+ return fixture_headers[fixture_key]['etag']
1196+ else:
1197+ msg = ("Object PUT failed - Object with key %s already exists"
1198+ % fixture_key)
1199+ raise swift.common.client.ClientException(msg,
1200+ http_status=httplib.CONFLICT)
1201+
1202+ def fake_get_object(url, token, container, name, **kwargs):
1203+ # GET returns the tuple (list of headers, file object)
1204+ try:
1205+ fixture_key = "%s/%s" % (container, name)
1206+ return fixture_headers[fixture_key], fixture_objects[fixture_key]
1207+ except KeyError:
1208+ msg = "Object GET failed"
1209+ raise swift.common.client.ClientException(msg,
1210+ http_status=httplib.NOT_FOUND)
1211+
1212+ def fake_head_object(url, token, container, name, **kwargs):
1213+ # HEAD returns the list of headers for an object
1214+ try:
1215+ fixture_key = "%s/%s" % (container, name)
1216+ return fixture_headers[fixture_key]
1217+ except KeyError:
1218+ msg = "Object HEAD failed - Object does not exist"
1219+ raise swift.common.client.ClientException(msg,
1220+ http_status=httplib.NOT_FOUND)
1221+
1222+ def fake_delete_object(url, token, container, name, **kwargs):
1223+ # DELETE returns nothing
1224+ fixture_key = "%s/%s" % (container, name)
1225+ if fixture_key not in fixture_headers.keys():
1226+ msg = "Object DELETE failed - Object does not exist"
1227+ raise swift.common.client.ClientException(msg,
1228+ http_status=httplib.NOT_FOUND)
1229+ else:
1230+ del fixture_headers[fixture_key]
1231+ del fixture_objects[fixture_key]
1232+
1233+ def fake_http_connection(*args, **kwargs):
1234+ return None
1235+
1236+ def fake_get_auth(*args, **kwargs):
1237+ return None, None
1238+
1239+ stubs.Set(swift.common.client,
1240+ 'head_container', fake_head_container)
1241+ stubs.Set(swift.common.client,
1242+ 'put_container', fake_put_container)
1243+ stubs.Set(swift.common.client,
1244+ 'put_object', fake_put_object)
1245+ stubs.Set(swift.common.client,
1246+ 'delete_object', fake_delete_object)
1247+ stubs.Set(swift.common.client,
1248+ 'head_object', fake_head_object)
1249+ stubs.Set(swift.common.client,
1250+ 'get_object', fake_get_object)
1251+ stubs.Set(swift.common.client,
1252+ 'get_auth', fake_get_auth)
1253+ stubs.Set(swift.common.client,
1254+ 'http_connection', fake_http_connection)
1255+
1256+
1257+class TestSwiftBackend(unittest.TestCase):
1258+
1259+ def setUp(self):
1260+ """Establish a clean test environment"""
1261+ self.stubs = stubout.StubOutForTesting()
1262+ stub_out_swift_common_client(self.stubs)
1263+
1264+ def tearDown(self):
1265+ """Clear the test environment"""
1266+ self.stubs.UnsetAll()
1267+
1268+ def test_get(self):
1269+ """Test a "normal" retrieval of an image in chunks"""
1270+ url_pieces = urlparse.urlparse(
1271+ "swift://user:key@auth_address/glance/2")
1272+ image_swift = SwiftBackend.get(url_pieces)
1273+
1274+ expected_data = "*" * FIVE_KB
1275+ data = ""
1276+
1277+ for chunk in image_swift:
1278+ data += chunk
1279+ self.assertEqual(expected_data, data)
1280+
1281+ def test_get_mismatched_expected_size(self):
1282+ """
1283+ Test retrieval of an image with wrong expected_size param
1284+ raises an exception
1285+ """
1286+ url_pieces = urlparse.urlparse(
1287+ "swift://user:key@auth_address/glance/2")
1288+ self.assertRaises(BackendException,
1289+ SwiftBackend.get,
1290+ url_pieces,
1291+ {'expected_size': 42})
1292+
1293+ def test_get_non_existing(self):
1294+ """
1295+ Test that trying to retrieve a swift that doesn't exist
1296+ raises an error
1297+ """
1298+ url_pieces = urlparse.urlparse(
1299+ "swift://user:key@auth_address/noexist")
1300+ self.assertRaises(exception.NotFound,
1301+ SwiftBackend.get,
1302+ url_pieces)
1303+
1304+ def test_add(self):
1305+ """Test that we can add an image via the swift backend"""
1306+ expected_image_id = 42
1307+ expected_swift_size = 1024 * 5 # 5K
1308+ expected_swift_contents = "*" * expected_swift_size
1309+ expected_location = format_swift_location(
1310+ SWIFT_OPTIONS['swift_store_user'],
1311+ SWIFT_OPTIONS['swift_store_key'],
1312+ SWIFT_OPTIONS['swift_store_auth_address'],
1313+ SWIFT_OPTIONS['swift_store_container'],
1314+ expected_image_id)
1315+ image_swift = StringIO.StringIO(expected_swift_contents)
1316+
1317+ location, size = SwiftBackend.add(42, image_swift, SWIFT_OPTIONS)
1318+
1319+ self.assertEquals(expected_location, location)
1320+ self.assertEquals(expected_swift_size, size)
1321+
1322+ url_pieces = urlparse.urlparse(expected_location)
1323+ new_image_swift = SwiftBackend.get(url_pieces)
1324+ new_image_contents = new_image_swift.getvalue()
1325+ new_image_swift_size = new_image_swift.len
1326+
1327+ self.assertEquals(expected_swift_contents, new_image_contents)
1328+ self.assertEquals(expected_swift_size, new_image_swift_size)
1329+
1330+ def test_add_no_container_no_create(self):
1331+ """
1332+ Tests that adding an image with a non-existing container
1333+ raises an appropriate exception
1334+ """
1335+ options = SWIFT_OPTIONS.copy()
1336+ options['swift_store_create_container_on_put'] = 'False'
1337+ options['swift_store_container'] = 'noexist'
1338+ image_swift = StringIO.StringIO("nevergonnamakeit")
1339+
1340+ # We check the exception text to ensure the container
1341+ # missing text is found in it, otherwise, we would have
1342+ # simply used self.assertRaises here
1343+ exception_caught = False
1344+ try:
1345+ SwiftBackend.add(3, image_swift, options)
1346+ except BackendException, e:
1347+ exception_caught = True
1348+ self.assertTrue("container noexist does not exist "
1349+ "in Swift" in str(e))
1350+ self.assertTrue(exception_caught)
1351+
1352+ def test_add_no_container_and_create(self):
1353+ """
1354+ Tests that adding an image with a non-existing container
1355+ creates the container automatically if flag is set
1356+ """
1357+ options = SWIFT_OPTIONS.copy()
1358+ options['swift_store_create_container_on_put'] = 'True'
1359+ options['swift_store_container'] = 'noexist'
1360+ expected_image_id = 42
1361+ expected_swift_size = 1024 * 5 # 5K
1362+ expected_swift_contents = "*" * expected_swift_size
1363+ expected_location = format_swift_location(
1364+ options['swift_store_user'],
1365+ options['swift_store_key'],
1366+ options['swift_store_auth_address'],
1367+ options['swift_store_container'],
1368+ expected_image_id)
1369+ image_swift = StringIO.StringIO(expected_swift_contents)
1370+
1371+ location, size = SwiftBackend.add(42, image_swift, options)
1372+
1373+ self.assertEquals(expected_location, location)
1374+ self.assertEquals(expected_swift_size, size)
1375+
1376+ url_pieces = urlparse.urlparse(expected_location)
1377+ new_image_swift = SwiftBackend.get(url_pieces)
1378+ new_image_contents = new_image_swift.getvalue()
1379+ new_image_swift_size = new_image_swift.len
1380+
1381+ self.assertEquals(expected_swift_contents, new_image_contents)
1382+ self.assertEquals(expected_swift_size, new_image_swift_size)
1383+
1384+ def test_add_already_existing(self):
1385+ """
1386+ Tests that adding an image with an existing identifier
1387+ raises an appropriate exception
1388+ """
1389+ image_swift = StringIO.StringIO("nevergonnamakeit")
1390+ self.assertRaises(exception.Duplicate,
1391+ SwiftBackend.add,
1392+ 2, image_swift, SWIFT_OPTIONS)
1393+
1394+ def test_delete(self):
1395+ """
1396+ Test we can delete an existing image in the swift store
1397+ """
1398+ url_pieces = urlparse.urlparse(
1399+ "swift://user:key@auth_address/glance/2")
1400+
1401+ SwiftBackend.delete(url_pieces)
1402+
1403+ self.assertRaises(exception.NotFound,
1404+ SwiftBackend.get,
1405+ url_pieces)
1406+
1407+ def test_delete_non_existing(self):
1408+ """
1409+ Test that trying to delete a swift that doesn't exist
1410+ raises an error
1411+ """
1412+ url_pieces = urlparse.urlparse("swift://user:key@auth_address/noexist")
1413+ self.assertRaises(exception.NotFound,
1414+ SwiftBackend.delete,
1415+ url_pieces)
1416
1417=== modified file 'tools/pip-requires'
1418--- tools/pip-requires 2011-02-05 00:05:23 +0000
1419+++ tools/pip-requires 2011-03-07 20:15:11 +0000
1420@@ -12,5 +12,6 @@
1421 sphinx
1422 argparse
1423 mox==0.5.0
1424+swift
1425 -f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz
1426 sqlalchemy-migrate>=0.6

Subscribers

People subscribed via source and target branches