Merge lp:~jaypipes/glance/bug713126 into lp:~glance-coresec/glance/cactus-trunk
- bug713126
- Merge into cactus-trunk
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 | ||||
Related bugs: |
|
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 |
Commit message
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.
Wayne A. Walls (wayne-walls) wrote : | # |
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_
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 :)
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_
> 'glance' and DEFAULT_
> :)
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
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_
When I use ST (the tool that comes with swift) without it, I get the following error:
__main_
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.
Traceback (most recent call last):
File "/usr/lib/
result = self.applicatio
File "build/
return resp(environ, start_response)
File "build/
response = self.app(environ, start_response)
File "build/
return resp(environ, start_response)
File "build/
resp = self.call_func(req, *args, **self.kwargs)
File "build/
return self.func(req, *args, **kwargs)
File "/usr/lib/
result = method(**arg_dict)
File "/usr/lib/
self.
File "/usr/lib/
raise e
ImportError: No module named common.client
I have swift installed in my site-packages, and I can import swift.common.
272 + connection_class = get_connection_
Thanks,
Armando
Armando Migliaccio (armando-migliaccio) wrote : | # |
I'd like to see this in trunk asap :)
Rick Harris (rconradharris) wrote : | # |
Excellent work Jay. I got it working with my CloudFiles account with some small-fixes:
> 237 + account = options.
> 238 + DEFAULT_
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.
> 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.
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.
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_
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.
> Traceback (most recent call last):
> File "/usr/lib/
> handle_one_response
> result = self.applicatio
> File "build/
> return resp(environ, start_response)
> File "build/
> __call__
> response = self.app(environ, start_response)
> File "build/
> return resp(environ, start_response)
> File "build/
> resp = self.call_func(req, *args, **self.kwargs)
> File "build/
> return self.func(req, *args, **kwargs)
> File "/usr/lib/
> __call__
> result = method(**arg_dict)
> File "/usr/lib/
> create
> self._upload_
> File "/usr/lib/
> _upload_
> raise e
> ImportError: No module named common.client
>
> I have swift installed in my site-packages, and I can import
> swift.common.
> occurs every time I try to upload. As the problem is here:
>
> 272 + connection_class = get_connection_
It's because there is a module glance.store.swift and a module swift.common.
Thanks!
jay
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 :)
Jay Pipes (jaypipes) wrote : | # |
> Excellent work Jay. I got it working with my CloudFiles account with some
> small-fixes:
>
> > 237 + account = options.
> > 238 + DEFAULT_
>
> 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/
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.
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.
> 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/
I could solve the problem using nose's skip stuff, too, I think?
> > 281 + obj_etag = swift_conn.
>
> 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...
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.
> 285 + except Exception, e:
> 286 + if e.http_status == httplib.NOT_FOUND:
> 287 + add_container = config.
> 288 + 'swift_
> 289 + type='bool', default=False)
> 290 + if add_container:
> 291 + try:
> 292 + swift_conn.
> 293 + except Exception, e:
> 294 + msg = ("Failed to add container to Swift.\n"
> 295 + "Got error from Swift: %(e)s" % locals())
> 296 + raise glance.
> 297 + else:
> 298 + msg = ("The container %(container)s does not exist in "
> 299 + "Swift. Please set the "
> 300 + "swift_
> 301 + "to add container to Swift automatically."
> 302 + % locals())
> 303 + raise glance.
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_
> 245 + user = options.
...
> 287 + add_container = config.
> 288 + 'swift_
> 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-
> 263 + if not user:
> 264 + logger.error(msg)
> 265 + msg = ("Could not find swift_store_user in configuration "
> 266 + "options.")
> 267 + raise glance.
There is some repeated code similar to this. Wonder if we'd benefit from a
`_get_required_
and raises if not.
> 157 +DEFAULT_
> 158 +DEFAULT_
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.
Jay Pipes (jaypipes) wrote : | # |
On Tue, Mar 1, 2011 at 2:20 AM, Rick Harris <email address hidden> wrote:
>> 245 Â + Â Â Â Â user = options.
> ...
>> 287 Â + Â Â Â Â Â Â Â Â add_container = config.
>> 288 Â + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â 'swift_
>> 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-
Sorry, I'm lost. The above is the glance.
function you added in a recent patch that I specifically argued
against using... I'm only following the convention you set in
/glance/
-jay
Armando Migliaccio (armando-migliaccio) wrote : | # |
Does line:
279 + logger.
280 + "(auth_
281 + "key=%(key)s)")
need % locals() ?
Armando Migliaccio (armando-migliaccio) wrote : | # |
Rick,
did you specify the version for swift_store_
swift_store_
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:/
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
Rick Harris (rconradharris) wrote : | # |
Jay:
> I'm only following the convention you set in
/glance/
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_
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:/
Or, we can use a separate config for each part:
swift_store_port = 80
swift_store_host = auth.api.
swift_store_
I lean towards the separate-config approach since it seems a little simpler.
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.
2011-03-01 10:28:03,242 INFO nova.virt.xenapi [-] Task [Async.
2011-03-01 10:28:03,452 INFO nova.virt.
2011-03-01 10:28:03,452 INFO nova.virt.
2011-03-01 10:28:03,502 DEBUG nova.virt.
2011-03-01 10:28:03,506 INFO nova.virt.
2011-03-01 10:28:03,506 INFO nova.virt.
##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:/
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_
# 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_XXXXXXXXXX
# 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_
# Do we create the container if it does not exist?
swift_store_
Any ideas? What else are you folks running into?
Cheers!
Wayne A. Walls (wayne-walls) wrote : | # |
Oh, and I'm using swauth (bind to 8080) for swift, not devauth.
thanks!
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.
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.
> instance-00000009 created OpaqueRef:
> 2011-03-01 10:28:03,242 INFO nova.virt.xenapi [-] Task
> [Async.
> status: success <value>null</value>
> 2011-03-01 10:28:03,452 INFO nova.virt.
> xenserver vm state -> |Running|
> 2011-03-01 10:28:03,452 INFO nova.virt.
> power_state -> |1|
> 2011-03-01 10:28:03,502 DEBUG nova.virt.
> instance-00000009: booted from (pid=964) _wait_for_boot
> /root/openstack
> 2011-03-01 10:28:03,506 INFO nova.virt.
> xenserver vm state -> |Running|
> 2011-03-01 10:28:03,506 INFO nova.virt.
> 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:/
> 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_
>
> # 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_XXXXXXXXXX
>
> # 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_
>
> # Do we create the container if it does not exist?
> swift_store_
>
>
>
> Any ideas? What else are you folks running into?
>
> Cheers!
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
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...
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/
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://
Others say this is:
http://
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
Jay Pipes (jaypipes) wrote : | # |
> Does line:
>
> 279 + logger.
> 280 + "(auth_
> 281 + "key=%(key)s)")
>
> need % locals() ?
Yes it does :) Good catch!
Jay Pipes (jaypipes) wrote : | # |
Added fixes from reviews. Please do re-review. Thanks :)
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.
> 380 + 'swift_
> 381 + type='bool', default=False)
> 382 + if add_container:
> 383 + try:
> 384 + swift_conn.
> 385 + except ClientException, e:
> 386 + msg = ("Failed to add container to Swift.\n"
> 387 + "Got error from Swift: %(e)s" % locals())
> 388 + raise glance.
> 389 + else:
> 390 + msg = ("The container %(container)s does not exist in "
> 391 + "Swift. Please set the "
> 392 + "swift_
> 393 + "to add container to Swift automatically."
> 394 + % locals())
> 395 + raise glance.
In the case where e.http_status != HTTP_FOUND, we probably want to re-raise
the current exception.
Jay Pipes (jaypipes) wrote : | # |
Yep, all good catches. Fixing now.
Devin Carlen (devcamcar) wrote : | # |
210 + location = "swift:
211 + container, obj)
305 + location = "swift:
306 + "%(container)
319 + location = "swift:
320 + container, id)
350 + location = "swift:
351 + container, obj)
Can you add a helper method to make this cleaner?
Jay Pipes (jaypipes) wrote : | # |
Devin and Rick, pushed fixes from your reviews. Please re-check :) Thanks again!
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/
> 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://
>
> Others say this is:
>
> http://
>
> 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>
+swift_
so one would understand that swift_store_account is 'glance-account', but then the code specifies:
+DEFAULT_
3) swift_store_account is a missing option in the glance.conf.sample
4) swift_store_
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
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>
> +swift_
>
> so one would understand that swift_store_account is 'glance-account', but then the code specifies:
>
> +DEFAULT_
I meant to remove this. The account should be specified in the
swift_store_
> 3) swift_store_account is a missing option in the glance.conf.sample
I removed this option.
> 4) swift_store_
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://
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
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>
> > +swift_
> >
> > so one would understand that swift_store_account is 'glance-account', but
> then the code specifies:
> >
> > +DEFAULT_
>
> I meant to remove this. The account should be specified in the
> swift_store_
>
> > 3) swift_store_account is a missing option in the glance.conf.sample
>
> I removed this option.
>
> > 4) swift_store_
> 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://
> 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_
lying around in the code, this branch has both thumbs up from my side
Rick Harris (rconradharris) wrote : | # |
Agree with Armando, we should probably remove this bit of vestigial code:
257 + account = options.
258 + DEFAULT_
170 +DEFAULT_
Jay Pipes (jaypipes) wrote : | # |
Vestigial account stuff gone! :) Please set to approved if that's the final fix...
OpenStack Infra (hudson-openstack) wrote : | # |
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.
writing top-level names to glance.
writing dependency_links to glance.
writing manifest file 'glance.
reading manifest file 'glance.
reading manifest template 'MANIFEST.in'
warning: no files found matching 'LICENSE'
warning: no files found matching 'ChangeLog'
warning: no files found matching 'tests/
writing manifest file 'glance.
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_
test_bad_
test_delete_image (tests.
test_delete_
Test for HEAD /images/<ID> ... ok
test_show_
test_show_
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 ClientConnectio
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
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 |
Checking out this patch right now.
cheers!