Merge lp:~cbehrens/nova/servers-search into lp:~hudson-openstack/nova/trunk

Proposed by Chris Behrens
Status: Merged
Approved by: Josh Kearney
Approved revision: 1305
Merged at revision: 1410
Proposed branch: lp:~cbehrens/nova/servers-search
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 1621 lines (+1110/-135)
12 files modified
nova/api/ec2/cloud.py (+30/-12)
nova/api/openstack/common.py (+33/-0)
nova/api/openstack/servers.py (+80/-17)
nova/api/openstack/views/servers.py (+1/-15)
nova/compute/api.py (+57/-31)
nova/db/api.py (+16/-10)
nova/db/sqlalchemy/api.py (+149/-44)
nova/db/sqlalchemy/models.py (+1/-0)
nova/tests/api/openstack/fakes.py (+6/-2)
nova/tests/api/openstack/test_servers.py (+275/-2)
nova/tests/test_compute.py (+457/-1)
nova/tests/test_metadata.py (+5/-1)
To merge this branch: bzr merge lp:~cbehrens/nova/servers-search
Reviewer Review Type Date Requested Status
Brian Waldon (community) Approve
Rick Harris (community) Approve
Brian Lamar (community) Abstain
Jay Pipes (community) Needs Fixing
Review via email: mp+67765@code.launchpad.net

Description of the change

This adds the servers search capabilities defined in the OS API v1.1 spec.. and more for admins.

For users, flavor=, image=, status=, and name= can be specified. name= supports regular expression matching.
Most other options are ignored. (things outside of the spec like 'recurse_zones' and 'reservation_id' still work, also)

If admin_api is enabled and context is an admin: along with the above, one can specify ip= and ip6= which will do regular expression matching. Also, any other 'Instance' column name can be specified, so you can do regexp matching there as well. Unknown Instance columns are ignored.

Also fixes up fixed_ip=, making a 404 returned vs a 500 error... and handling this properly with zone recursion as well.

To post a comment you must log in.
Revision history for this message
Brian Lamar (blamar) wrote :

This seems like a pretty inefficient way to do filtering if all we're looking for is wildcard matching. Why return everything when SQLAlchemy should successfully provide filtering across multiple backends? Can this be done using something like...

query.filter(Instance.name.like('%test%'))

...rather than returning each row and doing a regex?

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

Hi Chris!

Few little comments...

190 + # See if a valid search option was passed in.
191 + # Ignore unknown search options for possible forward compatability.
192 + # Raise an exception if more than 1 search option is specified
193 + found_opt = None
194 + for opt in exclusive_opts:
195 + v = search_opts.get(opt, None)
196 + if v:
197 + if found_opt is None:
198 + found_opt = opt
199 + else:
200 + LOG.error(_("More than 1 mutually exclusive "
201 + "search option specified (%(found_opt)s and "
202 + "%(opt)s were both specified") % locals())
203 + raise exception.InvalidInput(reason=_(
204 + "More than 1 mutually exclusive "
205 + "search option specified (%(found_opt)s and "
206 + "%(opt)s were both specified") % locals())

Might be more succinctly written as:

found_opt = None
found_opts = [opt for opt in exclusive_opts
              if search_opts.get(opt, None)]
if len(found_opts) > 1:
    found_opt_str = ", ".join(found_opts)
    msg = _("More than 1 mutually exclusive "
            "search option specified: %(found_opt_str)s")
            % locals())
    logger.error(msg)
    raise exception.InvalidInput(reason=msg)

if found_opts:
    found_opt = found_opts[0]

I recognize (and agree with your decision) not to do regexp matching via the database. Not only is it not portable, it's not any more efficient to do that at the database level (still requires a scan of all pre-restricted rows anyway...).

However this method signature:

+def instance_get_all_by_name_regexp(context, ipv6_regexp):

Looked strange, perhaps a copy/paste error? Should ipv6_regexp be name_regexp?

Cheers!
jay

review: Needs Fixing
Revision history for this message
Brian Lamar (blamar) wrote :

> I recognize (and agree with your decision) not to do regexp matching via the database. Not only is
> it not portable, it's not any more efficient to do that at the database level (still requires a
> scan of all pre-restricted rows anyway...).

Regular expressions are more expensive than LIKE matches (which in their own right, are pretty expensive). Do we really want operators doing complex regexs? At that point we should be putting our data into a purpose-built search indexing solution like Lucene/Solr/ElasticSearch/Sphinx because that's what they're good at.

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

> > I recognize (and agree with your decision) not to do regexp matching via the
> database. Not only is
> > it not portable, it's not any more efficient to do that at the database
> level (still requires a
> > scan of all pre-restricted rows anyway...).
>
> Regular expressions are more expensive than LIKE matches (which in their own
> right, are pretty expensive).

Actually, this is incorrect. LIKE '%something%' and column REGEXP 'someregexp' will produce identical query execution plans. The complexity of the REGEXP determines whether or not a simple string match such as '%something%' would be computationally more expensive to execute per row than a compiled regexp match.

> Do we really want operators doing complex
> regexs? At that point we should be putting our data into a purpose-built
> search indexing solution like Lucene/Solr/ElasticSearch/Sphinx because that's
> what they're good at.

Lucene/Solar/ElasticSearch/Sphinx are fulltext indexing technologies. What's happening here is looking for a particular pattern in a short string. The solution presented here is flexible enough to query for various IP(v6) and name patterns without having to set up a separate fulltext indexing server for this kind of thing, which I think would be overboard.

I understand your concern about the regexp inefficiency. Just saying that it's not that much less efficient than doing a REGEXP or LIKE '%something%' expression in SQL. The same loop and match process is occurring in Python code versus C code. The problem is that not all DBs support the REGEXP operator...

Just my two cents,
-jay

Revision history for this message
Chris Behrens (cbehrens) wrote :

Brian:

Yeah, I'm aware of everything in your first comment. I started out using 'like', but I ditched it for a few reasons.

1) Some of the things I'm matching against are not stored in the database. IPv6 addresses and the instance 'name', in particular. So, I'd have to build search functions in python that can parse '%..%'. That.. or I'd end up supporting regular expressions for some queries and the sql 'like' format for others.
2) Related to #1 in a way, what if the backend ends up not being SQL at all?
3) 'like' does a full table scan anyway and regular expressions are a little more flexible.

That said, using 'like' is going to be more efficient because I think it'll result in less 'join's. But, really, there's way too many joins in all of the previously existing instance getting code anyway. I duplicated what was used previously, but I don't think all of them are necessary. Someone really needs to go through and do an audit on all of these DB calls and find a way to not have to join stuff that isn't going to be used by the caller. I'd rather that I not have to be done as a part of this merge... as that'll be a huge task. :)

Revision history for this message
Chris Behrens (cbehrens) wrote :

Jay: Ah, thanks! I'll get those fixed up.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Is it necessary to have a separate db api method for each query string parameter? This seems like it could cause unnecessary growth in that module.

This doesn't seem to match the latest v1.1 spec. Is there a reason for the divergence?

review: Needs Information
Revision history for this message
Chris Behrens (cbehrens) wrote :

> Is it necessary to have a separate db api method for each query string
> parameter? This seems like it could cause unnecessary growth in that module.
>
> This doesn't seem to match the latest v1.1 spec. Is there a reason for the
> divergence?

Will check the spec.

As far as the separate db api methods... just to clarify... did you mean in db/api or actually in compute/api? Or both? Mostly everything can use the search "by_column" method I added. That tries to generically support columns by doing a 'getattr' on the Instance class for the column value to match. But, searching by 'ip', 'ipv6', and 'name' had to be separate because:

1) 'ip' matches against the FixedIp and FloatingIP tables, instead..
2) 'ipv6' isn't stored in the database
3) 'name' isn't stored in the database either... it's a @property of the class and a getattr() doesn't return the value directly... it returns a Property instance IIRC.

Revision history for this message
Brian Lamar (blamar) wrote :

> > > I recognize (and agree with your decision) not to do regexp matching via
> the
> > database. Not only is
> > > it not portable, it's not any more efficient to do that at the database
> > level (still requires a
> > > scan of all pre-restricted rows anyway...).
> >
> > Regular expressions are more expensive than LIKE matches (which in their own
> > right, are pretty expensive).
>
> Actually, this is incorrect. LIKE '%something%' and column REGEXP 'someregexp'
> will produce identical query execution plans. The complexity of the REGEXP
> determines whether or not a simple string match such as '%something%' would be
> computationally more expensive to execute per row than a compiled regexp
> match.
>
> > Do we really want operators doing complex
> > regexs? At that point we should be putting our data into a purpose-built
> > search indexing solution like Lucene/Solr/ElasticSearch/Sphinx because
> that's
> > what they're good at.
>
> Lucene/Solar/ElasticSearch/Sphinx are fulltext indexing technologies. What's
> happening here is looking for a particular pattern in a short string. The
> solution presented here is flexible enough to query for various IP(v6) and
> name patterns without having to set up a separate fulltext indexing server for
> this kind of thing, which I think would be overboard.
>
> I understand your concern about the regexp inefficiency. Just saying that it's
> not that much less efficient than doing a REGEXP or LIKE '%something%'
> expression in SQL. The same loop and match process is occurring in Python code
> versus C code. The problem is that not all DBs support the REGEXP operator...
>
> Just my two cents,
> -jay

Wow, just gonna have to agree to disagree then... I'm really positive it's going to be tons more efficient to do simple wildcard matching vs. fully-fledged perl-compatible regular expressions. I'm all about not putting specialized code into projects and I see this as being search-specialized code. I'm going to bow out now because I don't want to further this discussion on a merge-prop (sorry Chris!!).

> 1) Some of the things I'm matching against are not stored in the database. IPv6 addresses and the
> instance 'name', in particular. So, I'd have to build search functions in python that can parse
> '%..%'. That.. or I'd end up supporting regular expressions for some queries and the sql 'like'
> format for others.

Yeah, I can't believe that v6 address aren't stored in the DB. :) Good point.

> 2) Related to #1 in a way, what if the backend ends up not being SQL at all?

Heh, good luck with this one, but the point I'm trying to make is compatible with this as well. I use ElasticSearch with some of my CouchDB projects and there are tons of indexers for all types of datastores.

> 3) 'like' does a full table scan anyway and regular expressions are a little more flexible.

Absolutely, I just feel that we should be leaving the database to do the data-storage, a search-indexer to do searching, and Python to tie everything together.

Going to abstain from this merge prop, my apologies for cluttering it with conversations that most assuredly should have happened at the Blueprint level.

review: Abstain
Revision history for this message
Chris Behrens (cbehrens) wrote :

blamar: No problem, I understand. And FWIW, it's not 'pcre'.. it's just the python 're' module.. only slightly better performance-wise, probably. :)

Revision history for this message
Chris Behrens (cbehrens) wrote :

Ok. Per discussion with bcwaldon, I'm going to make the "name" query string option map to searching by display name. The spec sys name=serverName, and I got clarification that serverName means what the user sees... which is the Instance.display_name.

The spec doesn't mention 'instance_name' (Instance.name property in nova), but admins will want that. So, I'll make "instance_name" option map to what "name" did, which was search by this instance name. instance_name will have admin_api and admin context checking.

I'll remove searching by 'server_name='. It's always NULL in my tables, anyway, and it's not clear what this is really supposed to be used for (I haven't looked in the code, honestly). It's not mentioned in the spec, and it doesn't seem to be an immediate need... so...

Revision history for this message
Chris Behrens (cbehrens) wrote :

After reviewing section 4.1.1 in the v1.1 API spec regarding 'list', I went ahead and added some of the other search options like 'image', 'flavor', and status... along with making the previous changes that were discussed. Ie:

name matches display_name
instance_name is admin API/admin context only
I also made ip and ip6 admin-only due to it not being in the spec.
Unit tests have been added for OS API

Revision history for this message
Brian Waldon (bcwaldon) wrote :

A branch recently went in that replaced all returns of fault-wrapped webob exceptions with raising of non-wrapped webob exceptions. Can you convert any of the "return faults.Fault(...)" to "raise ..."?

I think we may want to take the approach of ignoring unsupported query params rather than raising an exception (that's how Glance does it). We should be liberal in what we accept. A lot of the _get_items / _items code can get cleaned back up if we don't need to explicitly check filter params. Same goes for check_option_permissions.

Power states belong to the compute layer, and server statuses belong to the OSAPI. I think we should remove any OSAPI status-related code from the power_states module and place it in either api.openstack.common or api.openstack.servers. What do you think?

What is the 'fresh' query filter? I just started seeing that in novaclient requests.

Are there future plans to implement changes-since? I can see it is an accepted filter, just not respected.

In the DB API, why is there a _get_all_by_column_regexp and a _get_all_by_instance_name? Can't we use get_all_by_column_regexp to accomplish the name filter?

Can you change _get_all_by_column to _get_all_by_column_value? The former doesn't clearly say you are checking the value in a specific column when there is another function with column_regexp.

I don't see it necessary to list OSAPI-specific filters in the compute api. Can we fix that known_params?

I really want to be able to sort by multiple parameters, and I think most people using the API would expect to be able to do that. How hard would it be to support this?

Don't get me wrong here, I'm really excited to get this feature in. Having done this in Glance already, I know this isn't a small undertaking :)

review: Needs Fixing
Revision history for this message
Chris Behrens (cbehrens) wrote :
Download full text (5.2 KiB)

> A branch recently went in that replaced all returns of fault-wrapped webob
> exceptions with raising of non-wrapped webob exceptions. Can you convert any
> of the "return faults.Fault(...)" to "raise ..."?

Aha.. that explains something. :) Sure.

> I think we may want to take the approach of ignoring unsupported query params
> rather than raising an exception (that's how Glance does it). We should be
> liberal in what we accept. A lot of the _get_items / _items code can get
> cleaned back up if we don't need to explicitly check filter params. Same goes
> for check_option_permissions.

Ok. I was thinking about v1.1 and trying to 'guess' at what I should do here based on the possible response codes to the query. I agree with what you say, though. I'd rather allow and ignore search options we don't understand. That will clean things up slightly. I still need to check for permissions on certain search items that should be admin-api only, but I can cut the 2 lists of options down to 1.. (list of options that require admin)

>
> Power states belong to the compute layer, and server statuses belong to the
> OSAPI. I think we should remove any OSAPI status-related code from the
> power_states module and place it in either api.openstack.common or
> api.openstack.servers. What do you think?

I had the same debate. I was going to move it to api.openstack.common.. until I realized that compute.api needs to send the 'status' to search by to child zones. This means that compute.api would end up making a call into api.openstack.common to convert a power state to the status to search for.. and that didn't feel quite right. I think I can solve that differently. I can make the OS API pass the 'status' to search for through to compute.api (along with 'state' which is the translation of status to power state). compute.api will ignore 'status' and use 'state' but it'll pass 'status' on to novaclient to talk to zones.

That or I could leave it the way it is. :) Changing it probably makes sense so there's clear separation.

>
> What is the 'fresh' query filter? I just started seeing that in novaclient
> requests.

Honestly, I have no clue. :-/ But I know that novaclient sends it. I figured it was v1.0 equiv of 'changes-since' that's in v1.1.

>
> Are there future plans to implement changes-since? I can see it is an accepted
> filter, just not respected.

Yeah, good question. I'm already going beyond the scope of the story that was given to us for the sprint, and that one seemed a little more difficult. Wasn't sure if compute.api should filter that.. or let OS API filter on the return to the client. I thought maybe titan would have a better idea of how that should be done. ;)

>
> In the DB API, why is there a _get_all_by_column_regexp and a
> _get_all_by_instance_name? Can't we use get_all_by_column_regexp to accomplish
> the name filter?

Let me double check that. I saw a problem early on with this, because 'name' is a @property in the Instance class and I seemed to be getting back a property object with getattr. I bet I was doing testing with a class and not an instance of a class at the time, however. I bet this can be consolidated.

> ...

Read more...

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> > Power states belong to the compute layer, and server statuses belong to the
> > OSAPI. I think we should remove any OSAPI status-related code from the
> > power_states module and place it in either api.openstack.common or
> > api.openstack.servers. What do you think?
>
> I had the same debate. I was going to move it to api.openstack.common.. until
> I realized that compute.api needs to send the 'status' to search by to child
> zones. This means that compute.api would end up making a call into
> api.openstack.common to convert a power state to the status to search for..
> and that didn't feel quite right. I think I can solve that differently. I
> can make the OS API pass the 'status' to search for through to compute.api
> (along with 'state' which is the translation of status to power state).
> compute.api will ignore 'status' and use 'state' but it'll pass 'status' on to
> novaclient to talk to zones.
>
> That or I could leave it the way it is. :) Changing it probably makes sense
> so there's clear separation.

Yeah, I really don't want the compute api (the power states module) to depend on concepts defined in the openstack api.

> > Are there future plans to implement changes-since? I can see it is an
> accepted
> > filter, just not respected.
>
> Yeah, good question. I'm already going beyond the scope of the story that was
> given to us for the sprint, and that one seemed a little more difficult.
> Wasn't sure if compute.api should filter that.. or let OS API filter on the
> return to the client. I thought maybe titan would have a better idea of how
> that should be done. ;)

We got it :)

> > I don't see it necessary to list OSAPI-specific filters in the compute api.
> > Can we fix that known_params?
>
> Well, I need a table to map the search parameter to the function to call with
> its arguments. In the initial merge prop, I didn't have this as I used
> getattr(self, '_get_all_by_%s' % opt_name).. and called that function. The
> problem is that the generic 'column' ones need an extra argument. The
> original merge prop had a list of those search options that could be done
> generically. I could change it back... but it'd also require moving all of
> the _get_all_by* calls back outside of 'def get_all' so I could use
> getattr(self, ..) again.
>
> Hm.. did that make sense? We can chat on IRC to clarify if needed.

What bothers me is the inclusion of non-filter query params (like changes-since). If those would just go away I would be more okay with it.

> > I really want to be able to sort by multiple parameters, and I think most
> > people using the API would expect to be able to do that. How hard would it
> be
> > to support this?
>
> Yeah, I hear you. The story I was given says to throw an error if multiple
> are given, so that's the main reason why I didn't code support for it. :) My
> instinct is to say it's rather difficult... but when I really start to think
> about it, I'm not sure it's so bad. I can think about it some more while
> doing the other fixups.

I don't mean to toot my own horn here, but look at glance and see if that approach would work.

Revision history for this message
Chris Behrens (cbehrens) wrote :

Alright. This is almost a re-write since the original merge prop, now. I updated the description up at the top to better reflect what this ends up adding.

Fire away.. :)

Revision history for this message
Ed Leafe (ed-leafe) wrote :

Only got through the first 700 lines of the diff so far, but here's what I have:

Lines 134 -143 can be more efficiently written:

def power_states_from_status(status):
    """Map the server status string to a list of power states"""
    low_status = status.strip().lower()
    power_states = [val for key, val in _STATUS_MAP.iteritems()
      if key.lower() == low_status
      and val is not None]
    return power_states
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The method '_check_servers_options()' would be better named something like '_remove_invalid_options()' so that the fact that it modifies search_opts is more clearly expressed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Line 643: What is this supposed to be doing? Did you maybe mean to copy the 'filters' dict to another name?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Lines 660 -664 can be more efficiently written:

for filter_name in query_filters:
    query_prefix = _exact_match_filter(query_prefix, filter_name,
            filters.pop(filter_name))

Revision history for this message
Chris Behrens (cbehrens) wrote :

Ed:

Ok re: _check_servers_options().

643: I meant to make a copy of it to use going forward.. I just reassigned it to same variable name. I'm making a copy because I'm modifying it and I can't have it affect the caller.

660-664: Good call... makes sense.

Revision history for this message
Chris Behrens (cbehrens) wrote :

Updated. Added a comment around #643 to clarify the .copy().

Revision history for this message
Brian Waldon (bcwaldon) wrote :

This looks great, Mr. Behrens. I like the db-api implementation a lot more. I've got just a few comments:

I noticed you linked a lazy load bug to this. Can you explain how this branch addresses that?

164: Instead of using an instance variable, can you implement this with constant tuples that are passed into _remove_invalid_options as an 'allowed_search_options' argument?

217: I'm not crazy about this method name. How about just _get_servers?
215: This is only called once, so does it make sense to break it out into its own method? I think it all fits under _servers_from_request (or whatever its new name might be ;)

392: There's a pattern here that is repeated a few times. Maybe you can create a mapping dictionary for converting key names

419: This behavior will also happen through OSAPI, right? I'm not so sure we want to support special cases like this. It's a totally valid request to filter by fixed_ip and name where no results are returned.

480: This behavior seems odd. I'm thinking we should just return an empty list. What do you think?

Revision history for this message
Chris Behrens (cbehrens) wrote :

lazy load: The fix is actually described in the thread with the bug, but: OS API accesses Instance['virtual_interfaces']['network'], but instance_get_all() does not joinedload('virtual_interfaces.network'). When the sqlalchemy session becomes detached, OS API cannot do the query for 'network' when it's accessed. My new DB API routine joins it. I didn't add it to any other queries, because the OS API may be the only place where it's needed.

164: Sure

217: Ok.

215: I'd originally broken it out because I split the caller into the 1.0/1.1 Controllers, so it had 2 references. I'll merge it back in, since that's not the case anymore.

392: I didn't use a mapping table because the flipping needs to happen in a certain order (since
i'm renaming name -> display_name, but instance_name to name. Doing those in reverse order would clobber the 'name' if both exist in the query). But I know how I can solve that.

419: Yeah, I did this because I'm not sure 'fixed_ip' should be used via OS API. One should use 'ip' instead. If it's for EC2 only, fixed_ip this way is a lot more efficient query since it looks up via FixedIps table directly with no joins to find the entry. Decisions like these are difficult, as I'm not entirely sure what's most desirable to the community...but generally there is some sort of reason to the method of my madness. :) I'm cool with changing it.

480: I feel this way, too.. I was just trying to match the original behavior in single zone. I guess if EC2 should really return an error in this case, the error return should happen in the ec2 controller if it gets an empty list back.

Revision history for this message
Chris Behrens (cbehrens) wrote :

Ok, I think all of those issues are addressed.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Thanks for addressing those issues. Code looks good to me, now, but I'm seeing one test failure:

======================================================================
FAIL: test_get_servers_allows_status_v1_1 (nova.tests.api.openstack.test_servers.ServersTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/brian.waldon/valhalla/nova/servers-search/nova/tests/api/openstack/test_servers.py", line 1184, in test_get_servers_allows_status_v1_1
    self.assertEqual(res.status_int, 200)
AssertionError: 500 != 200
-------------------- >> begin captured logging << --------------------

...

(nova.api.openstack): TRACE: File "/Users/brian.waldon/valhalla/nova/servers-search/nova/api/openstack/servers.py", line 56, in index
(nova.api.openstack): TRACE: servers = self._get_servers(req, is_detail=False)
(nova.api.openstack): TRACE: File "/Users/brian.waldon/valhalla/nova/servers-search/nova/api/openstack/servers.py", line 126, in _get_servers
(nova.api.openstack): TRACE: context, search_opts=search_opts)
(nova.api.openstack): TRACE: File "/Users/brian.waldon/valhalla/nova/servers-search/nova/tests/api/openstack/test_servers.py", line 1174, in fake_get_all
(nova.api.openstack): TRACE: [power_state.RUNNING, power_state.BLOCKED])
(nova.api.openstack): TRACE: File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/unittest.py", line 350, in failUnlessEqual
(nova.api.openstack): TRACE: (msg or '%r != %r' % (first, second))
(nova.api.openstack): TRACE: AssertionError: [2, 1] != [1, 2]
(nova.api.openstack): TRACE:
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------

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

Tests pass (for me), and code looks good. Really nice work here Chris.

review: Approve
Revision history for this message
Chris Behrens (cbehrens) wrote :

bcwaldon: Issue is this:

[15:51:38] <comstud> 1173 self.assertEqual(search_opts['state'],
[15:51:39] <comstud> 1174 [power_state.RUNNING,
                     power_state.BLOCKED])

The list is in reverse order in your test and I'm assuming order here. I've just wrapped those lists with set() so it does a set comparison. Try now. :)

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Yep, all good now. Thank you, Dr. Behrens!

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :

Attempt to merge into lp:nova failed due to conflicts:

text conflict in nova/api/ec2/cloud.py
text conflict in nova/tests/test_metadata.py

Revision history for this message
Chris Behrens (cbehrens) wrote :

Merged trunk and resolved 2 conflicts. Need the approves again.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'nova/api/ec2/cloud.py'
--- nova/api/ec2/cloud.py 2011-08-09 12:47:47 +0000
+++ nova/api/ec2/cloud.py 2011-08-09 20:27:18 +0000
@@ -213,8 +213,9 @@
213213
214 def _get_mpi_data(self, context, project_id):214 def _get_mpi_data(self, context, project_id):
215 result = {}215 result = {}
216 search_opts = {'project_id': project_id}
216 for instance in self.compute_api.get_all(context,217 for instance in self.compute_api.get_all(context,
217 project_id=project_id):218 search_opts=search_opts):
218 if instance['fixed_ips']:219 if instance['fixed_ips']:
219 line = '%s slots=%d' % (instance['fixed_ips'][0]['address'],220 line = '%s slots=%d' % (instance['fixed_ips'][0]['address'],
220 instance['vcpus'])221 instance['vcpus'])
@@ -264,8 +265,13 @@
264265
265 def get_metadata(self, address):266 def get_metadata(self, address):
266 ctxt = context.get_admin_context()267 ctxt = context.get_admin_context()
267 instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address)268 search_opts = {'fixed_ip': address}
268 if instance_ref is None:269 try:
270 instance_ref = self.compute_api.get_all(ctxt,
271 search_opts=search_opts)
272 except exception.NotFound:
273 instance_ref = None
274 if not instance_ref:
269 return None275 return None
270276
271 # This ensures that all attributes of the instance277 # This ensures that all attributes of the instance
@@ -1086,11 +1092,16 @@
1086 return result1092 return result
10871093
1088 def describe_instances(self, context, **kwargs):1094 def describe_instances(self, context, **kwargs):
1089 return self._format_describe_instances(context, **kwargs)1095 # Optional DescribeInstances argument
1096 instance_id = kwargs.get('instance_id', None)
1097 return self._format_describe_instances(context,
1098 instance_id=instance_id)
10901099
1091 def describe_instances_v6(self, context, **kwargs):1100 def describe_instances_v6(self, context, **kwargs):
1092 kwargs['use_v6'] = True1101 # Optional DescribeInstancesV6 argument
1093 return self._format_describe_instances(context, **kwargs)1102 instance_id = kwargs.get('instance_id', None)
1103 return self._format_describe_instances(context,
1104 instance_id=instance_id, use_v6=True)
10941105
1095 def _format_describe_instances(self, context, **kwargs):1106 def _format_describe_instances(self, context, **kwargs):
1096 return {'reservationSet': self._format_instances(context, **kwargs)}1107 return {'reservationSet': self._format_instances(context, **kwargs)}
@@ -1152,7 +1163,8 @@
1152 result['groupSet'] = CloudController._convert_to_set(1163 result['groupSet'] = CloudController._convert_to_set(
1153 security_group_names, 'groupId')1164 security_group_names, 'groupId')
11541165
1155 def _format_instances(self, context, instance_id=None, **kwargs):1166 def _format_instances(self, context, instance_id=None, use_v6=False,
1167 **search_opts):
1156 # TODO(termie): this method is poorly named as its name does not imply1168 # TODO(termie): this method is poorly named as its name does not imply
1157 # that it will be making a variety of database calls1169 # that it will be making a variety of database calls
1158 # rather than simply formatting a bunch of instances that1170 # rather than simply formatting a bunch of instances that
@@ -1163,11 +1175,17 @@
1163 instances = []1175 instances = []
1164 for ec2_id in instance_id:1176 for ec2_id in instance_id:
1165 internal_id = ec2utils.ec2_id_to_id(ec2_id)1177 internal_id = ec2utils.ec2_id_to_id(ec2_id)
1166 instance = self.compute_api.get(context,1178 try:
1167 instance_id=internal_id)1179 instance = self.compute_api.get(context, internal_id)
1180 except exception.NotFound:
1181 continue
1168 instances.append(instance)1182 instances.append(instance)
1169 else:1183 else:
1170 instances = self.compute_api.get_all(context, **kwargs)1184 try:
1185 instances = self.compute_api.get_all(context,
1186 search_opts=search_opts)
1187 except exception.NotFound:
1188 instances = []
1171 for instance in instances:1189 for instance in instances:
1172 if not context.is_admin:1190 if not context.is_admin:
1173 if instance['image_ref'] == str(FLAGS.vpn_image_id):1191 if instance['image_ref'] == str(FLAGS.vpn_image_id):
@@ -1189,7 +1207,7 @@
1189 fixed_addr = fixed['address']1207 fixed_addr = fixed['address']
1190 if fixed['floating_ips']:1208 if fixed['floating_ips']:
1191 floating_addr = fixed['floating_ips'][0]['address']1209 floating_addr = fixed['floating_ips'][0]['address']
1192 if fixed['network'] and 'use_v6' in kwargs:1210 if fixed['network'] and use_v6:
1193 i['dnsNameV6'] = ipv6.to_global(1211 i['dnsNameV6'] = ipv6.to_global(
1194 fixed['network']['cidr_v6'],1212 fixed['network']['cidr_v6'],
1195 fixed['virtual_interface']['address'],1213 fixed['virtual_interface']['address'],
@@ -1326,7 +1344,7 @@
1326 'AvailabilityZone'),1344 'AvailabilityZone'),
1327 block_device_mapping=kwargs.get('block_device_mapping', {}))1345 block_device_mapping=kwargs.get('block_device_mapping', {}))
1328 return self._format_run_instances(context,1346 return self._format_run_instances(context,
1329 instances[0]['reservation_id'])1347 reservation_id=instances[0]['reservation_id'])
13301348
1331 def _do_instance(self, action, context, ec2_id):1349 def _do_instance(self, action, context, ec2_id):
1332 instance_id = ec2utils.ec2_id_to_id(ec2_id)1350 instance_id = ec2utils.ec2_id_to_id(ec2_id)
13331351
=== modified file 'nova/api/openstack/common.py'
--- nova/api/openstack/common.py 2011-08-09 16:33:13 +0000
+++ nova/api/openstack/common.py 2011-08-09 20:27:18 +0000
@@ -27,6 +27,7 @@
27from nova import log as logging27from nova import log as logging
28from nova import quota28from nova import quota
29from nova.api.openstack import wsgi29from nova.api.openstack import wsgi
30from nova.compute import power_state as compute_power_state
3031
3132
32LOG = logging.getLogger('nova.api.openstack.common')33LOG = logging.getLogger('nova.api.openstack.common')
@@ -37,6 +38,38 @@
37XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1'38XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
3839
3940
41_STATUS_MAP = {
42 None: 'BUILD',
43 compute_power_state.NOSTATE: 'BUILD',
44 compute_power_state.RUNNING: 'ACTIVE',
45 compute_power_state.BLOCKED: 'ACTIVE',
46 compute_power_state.SUSPENDED: 'SUSPENDED',
47 compute_power_state.PAUSED: 'PAUSED',
48 compute_power_state.SHUTDOWN: 'SHUTDOWN',
49 compute_power_state.SHUTOFF: 'SHUTOFF',
50 compute_power_state.CRASHED: 'ERROR',
51 compute_power_state.FAILED: 'ERROR',
52 compute_power_state.BUILDING: 'BUILD',
53}
54
55
56def status_from_power_state(power_state):
57 """Map the power state to the server status string"""
58 return _STATUS_MAP[power_state]
59
60
61def power_states_from_status(status):
62 """Map the server status string to a list of power states"""
63 power_states = []
64 for power_state, status_map in _STATUS_MAP.iteritems():
65 # Skip the 'None' state
66 if power_state is None:
67 continue
68 if status.lower() == status_map.lower():
69 power_states.append(power_state)
70 return power_states
71
72
40def get_pagination_params(request):73def get_pagination_params(request):
41 """Return marker, limit tuple from request.74 """Return marker, limit tuple from request.
4275
4376
=== modified file 'nova/api/openstack/servers.py'
--- nova/api/openstack/servers.py 2011-08-09 16:33:13 +0000
+++ nova/api/openstack/servers.py 2011-08-09 20:27:18 +0000
@@ -44,7 +44,7 @@
4444
4545
46class Controller(object):46class Controller(object):
47 """ The Server API controller for the OpenStack API """47 """ The Server API base controller class for the OpenStack API """
4848
49 def __init__(self):49 def __init__(self):
50 self.compute_api = compute.API()50 self.compute_api = compute.API()
@@ -53,17 +53,21 @@
53 def index(self, req):53 def index(self, req):
54 """ Returns a list of server names and ids for a given user """54 """ Returns a list of server names and ids for a given user """
55 try:55 try:
56 servers = self._items(req, is_detail=False)56 servers = self._get_servers(req, is_detail=False)
57 except exception.Invalid as err:57 except exception.Invalid as err:
58 return exc.HTTPBadRequest(explanation=str(err))58 return exc.HTTPBadRequest(explanation=str(err))
59 except exception.NotFound:
60 return exc.HTTPNotFound()
59 return servers61 return servers
6062
61 def detail(self, req):63 def detail(self, req):
62 """ Returns a list of server details for a given user """64 """ Returns a list of server details for a given user """
63 try:65 try:
64 servers = self._items(req, is_detail=True)66 servers = self._get_servers(req, is_detail=True)
65 except exception.Invalid as err:67 except exception.Invalid as err:
66 return exc.HTTPBadRequest(explanation=str(err))68 return exc.HTTPBadRequest(explanation=str(err))
69 except exception.NotFound as err:
70 return exc.HTTPNotFound()
67 return servers71 return servers
6872
69 def _build_view(self, req, instance, is_detail=False):73 def _build_view(self, req, instance, is_detail=False):
@@ -75,22 +79,55 @@
75 def _action_rebuild(self, info, request, instance_id):79 def _action_rebuild(self, info, request, instance_id):
76 raise NotImplementedError()80 raise NotImplementedError()
7781
78 def _items(self, req, is_detail):82 def _get_servers(self, req, is_detail):
79 """Returns a list of servers for a given user.83 """Returns a list of servers, taking into account any search
8084 options specified.
81 builder - the response model builder
82 """85 """
83 query_str = req.str_GET86
84 reservation_id = query_str.get('reservation_id')87 search_opts = {}
85 project_id = query_str.get('project_id')88 search_opts.update(req.str_GET)
86 fixed_ip = query_str.get('fixed_ip')89
87 recurse_zones = utils.bool_from_str(query_str.get('recurse_zones'))90 context = req.environ['nova.context']
91 remove_invalid_options(context, search_opts,
92 self._get_server_search_options())
93
94 # Convert recurse_zones into a boolean
95 search_opts['recurse_zones'] = utils.bool_from_str(
96 search_opts.get('recurse_zones', False))
97
98 # If search by 'status', we need to convert it to 'state'
99 # If the status is unknown, bail.
100 # Leave 'state' in search_opts so compute can pass it on to
101 # child zones..
102 if 'status' in search_opts:
103 status = search_opts['status']
104 search_opts['state'] = common.power_states_from_status(status)
105 if len(search_opts['state']) == 0:
106 reason = _('Invalid server status: %(status)s') % locals()
107 LOG.error(reason)
108 raise exception.InvalidInput(reason=reason)
109
110 # By default, compute's get_all() will return deleted instances.
111 # If an admin hasn't specified a 'deleted' search option, we need
112 # to filter out deleted instances by setting the filter ourselves.
113 # ... Unless 'changes-since' is specified, because 'changes-since'
114 # should return recently deleted images according to the API spec.
115
116 if 'deleted' not in search_opts:
117 # Admin hasn't specified deleted filter
118 if 'changes-since' not in search_opts:
119 # No 'changes-since', so we need to find non-deleted servers
120 search_opts['deleted'] = False
121 else:
122 # This is the default, but just in case..
123 search_opts['deleted'] = True
124
88 instance_list = self.compute_api.get_all(125 instance_list = self.compute_api.get_all(
89 req.environ['nova.context'],126 context, search_opts=search_opts)
90 reservation_id=reservation_id,127
91 project_id=project_id,128 # FIXME(comstud): 'changes-since' is not fully implemented. Where
92 fixed_ip=fixed_ip,129 # should this be filtered?
93 recurse_zones=recurse_zones)130
94 limited_list = self._limit_items(instance_list, req)131 limited_list = self._limit_items(instance_list, req)
95 servers = [self._build_view(req, inst, is_detail)['server']132 servers = [self._build_view(req, inst, is_detail)['server']
96 for inst in limited_list]133 for inst in limited_list]
@@ -506,6 +543,7 @@
506543
507544
508class ControllerV10(Controller):545class ControllerV10(Controller):
546 """v1.0 OpenStack API controller"""
509547
510 @scheduler_api.redirect_handler548 @scheduler_api.redirect_handler
511 def delete(self, req, id):549 def delete(self, req, id):
@@ -568,8 +606,13 @@
568 """ Determine the admin password for a server on creation """606 """ Determine the admin password for a server on creation """
569 return self.helper._get_server_admin_password_old_style(server)607 return self.helper._get_server_admin_password_old_style(server)
570608
609 def _get_server_search_options(self):
610 """Return server search options allowed by non-admin"""
611 return 'reservation_id', 'fixed_ip', 'name', 'recurse_zones'
612
571613
572class ControllerV11(Controller):614class ControllerV11(Controller):
615 """v1.1 OpenStack API controller"""
573616
574 @scheduler_api.redirect_handler617 @scheduler_api.redirect_handler
575 def delete(self, req, id):618 def delete(self, req, id):
@@ -742,6 +785,11 @@
742 """ Determine the admin password for a server on creation """785 """ Determine the admin password for a server on creation """
743 return self.helper._get_server_admin_password_new_style(server)786 return self.helper._get_server_admin_password_new_style(server)
744787
788 def _get_server_search_options(self):
789 """Return server search options allowed by non-admin"""
790 return ('reservation_id', 'name', 'recurse_zones',
791 'status', 'image', 'flavor', 'changes-since')
792
745793
746class HeadersSerializer(wsgi.ResponseHeadersSerializer):794class HeadersSerializer(wsgi.ResponseHeadersSerializer):
747795
@@ -920,3 +968,18 @@
920 deserializer = wsgi.RequestDeserializer(body_deserializers)968 deserializer = wsgi.RequestDeserializer(body_deserializers)
921969
922 return wsgi.Resource(controller, deserializer, serializer)970 return wsgi.Resource(controller, deserializer, serializer)
971
972
973def remove_invalid_options(context, search_options, allowed_search_options):
974 """Remove search options that are not valid for non-admin API/context"""
975 if FLAGS.allow_admin_api and context.is_admin:
976 # Allow all options
977 return
978 # Otherwise, strip out all unknown options
979 unknown_options = [opt for opt in search_options
980 if opt not in allowed_search_options]
981 unk_opt_str = ", ".join(unknown_options)
982 log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals()
983 LOG.debug(log_msg)
984 for opt in unknown_options:
985 search_options.pop(opt, None)
923986
=== modified file 'nova/api/openstack/views/servers.py'
--- nova/api/openstack/views/servers.py 2011-07-29 16:28:02 +0000
+++ nova/api/openstack/views/servers.py 2011-08-09 20:27:18 +0000
@@ -20,7 +20,6 @@
20import os20import os
2121
22from nova import exception22from nova import exception
23from nova.compute import power_state
24import nova.compute23import nova.compute
25import nova.context24import nova.context
26from nova.api.openstack import common25from nova.api.openstack import common
@@ -61,24 +60,11 @@
6160
62 def _build_detail(self, inst):61 def _build_detail(self, inst):
63 """Returns a detailed model of a server."""62 """Returns a detailed model of a server."""
64 power_mapping = {
65 None: 'BUILD',
66 power_state.NOSTATE: 'BUILD',
67 power_state.RUNNING: 'ACTIVE',
68 power_state.BLOCKED: 'ACTIVE',
69 power_state.SUSPENDED: 'SUSPENDED',
70 power_state.PAUSED: 'PAUSED',
71 power_state.SHUTDOWN: 'SHUTDOWN',
72 power_state.SHUTOFF: 'SHUTOFF',
73 power_state.CRASHED: 'ERROR',
74 power_state.FAILED: 'ERROR',
75 power_state.BUILDING: 'BUILD',
76 }
7763
78 inst_dict = {64 inst_dict = {
79 'id': inst['id'],65 'id': inst['id'],
80 'name': inst['display_name'],66 'name': inst['display_name'],
81 'status': power_mapping[inst.get('state')]}67 'status': common.status_from_power_state(inst.get('state'))}
8268
83 ctxt = nova.context.get_admin_context()69 ctxt = nova.context.get_admin_context()
84 compute_api = nova.compute.API()70 compute_api = nova.compute.API()
8571
=== modified file 'nova/compute/api.py'
--- nova/compute/api.py 2011-08-09 15:27:56 +0000
+++ nova/compute/api.py 2011-08-09 20:27:18 +0000
@@ -19,6 +19,7 @@
19"""Handles all requests relating to instances (guest vms)."""19"""Handles all requests relating to instances (guest vms)."""
2020
21import eventlet21import eventlet
22import novaclient
22import re23import re
23import time24import time
2425
@@ -712,59 +713,84 @@
712 """713 """
713 return self.get(context, instance_id)714 return self.get(context, instance_id)
714715
715 def get_all(self, context, project_id=None, reservation_id=None,716 def get_all(self, context, search_opts=None):
716 fixed_ip=None, recurse_zones=False):
717 """Get all instances filtered by one of the given parameters.717 """Get all instances filtered by one of the given parameters.
718718
719 If there is no filter and the context is an admin, it will retreive719 If there is no filter and the context is an admin, it will retreive
720 all instances in the system.720 all instances in the system.
721 """721 """
722722
723 if reservation_id is not None:723 if search_opts is None:
724 search_opts = {}
725
726 LOG.debug(_("Searching by: %s") % str(search_opts))
727
728 # Fixups for the DB call
729 filters = {}
730
731 def _remap_flavor_filter(flavor_id):
732 instance_type = self.db.instance_type_get_by_flavor_id(
733 context, flavor_id)
734 filters['instance_type_id'] = instance_type['id']
735
736 def _remap_fixed_ip_filter(fixed_ip):
737 # Turn fixed_ip into a regexp match. Since '.' matches
738 # any character, we need to use regexp escaping for it.
739 filters['ip'] = '^%s$' % fixed_ip.replace('.', '\\.')
740
741 # search_option to filter_name mapping.
742 filter_mapping = {
743 'image': 'image_ref',
744 'name': 'display_name',
745 'instance_name': 'name',
746 'recurse_zones': None,
747 'flavor': _remap_flavor_filter,
748 'fixed_ip': _remap_fixed_ip_filter}
749
750 # copy from search_opts, doing various remappings as necessary
751 for opt, value in search_opts.iteritems():
752 # Do remappings.
753 # Values not in the filter_mapping table are copied as-is.
754 # If remapping is None, option is not copied
755 # If the remapping is a string, it is the filter_name to use
756 try:
757 remap_object = filter_mapping[opt]
758 except KeyError:
759 filters[opt] = value
760 else:
761 if remap_object:
762 if isinstance(remap_object, basestring):
763 filters[remap_object] = value
764 else:
765 remap_object(value)
766
767 recurse_zones = search_opts.get('recurse_zones', False)
768 if 'reservation_id' in filters:
724 recurse_zones = True769 recurse_zones = True
725 instances = self.db.instance_get_all_by_reservation(
726 context, reservation_id)
727 elif fixed_ip is not None:
728 try:
729 instances = self.db.fixed_ip_get_instance(context, fixed_ip)
730 except exception.FloatingIpNotFound, e:
731 if not recurse_zones:
732 raise
733 instances = None
734 elif project_id or not context.is_admin:
735 if not context.project_id:
736 instances = self.db.instance_get_all_by_user(
737 context, context.user_id)
738 else:
739 if project_id is None:
740 project_id = context.project_id
741 instances = self.db.instance_get_all_by_project(
742 context, project_id)
743 else:
744 instances = self.db.instance_get_all(context)
745770
746 if instances is None:771 instances = self.db.instance_get_all_by_filters(context, filters)
747 instances = []
748 elif not isinstance(instances, list):
749 instances = [instances]
750772
751 if not recurse_zones:773 if not recurse_zones:
752 return instances774 return instances
753775
776 # Recurse zones. Need admin context for this. Send along
777 # the un-modified search options we received..
754 admin_context = context.elevated()778 admin_context = context.elevated()
755 children = scheduler_api.call_zone_method(admin_context,779 children = scheduler_api.call_zone_method(admin_context,
756 "list",780 "list",
781 errors_to_ignore=[novaclient.exceptions.NotFound],
757 novaclient_collection_name="servers",782 novaclient_collection_name="servers",
758 reservation_id=reservation_id,783 search_opts=search_opts)
759 project_id=project_id,
760 fixed_ip=fixed_ip,
761 recurse_zones=True)
762784
763 for zone, servers in children:785 for zone, servers in children:
786 # 'servers' can be None if a 404 was returned by a zone
787 if servers is None:
788 continue
764 for server in servers:789 for server in servers:
765 # Results are ready to send to user. No need to scrub.790 # Results are ready to send to user. No need to scrub.
766 server._info['_is_precooked'] = True791 server._info['_is_precooked'] = True
767 instances.append(server._info)792 instances.append(server._info)
793
768 return instances794 return instances
769795
770 def _cast_compute_message(self, method, context, instance_id, host=None,796 def _cast_compute_message(self, method, context, instance_id, host=None,
771797
=== modified file 'nova/db/api.py'
--- nova/db/api.py 2011-08-08 21:33:03 +0000
+++ nova/db/api.py 2011-08-09 20:27:18 +0000
@@ -387,15 +387,6 @@
387 return IMPL.fixed_ip_get_by_virtual_interface(context, vif_id)387 return IMPL.fixed_ip_get_by_virtual_interface(context, vif_id)
388388
389389
390def fixed_ip_get_instance(context, address):
391 """Get an instance for a fixed ip by address."""
392 return IMPL.fixed_ip_get_instance(context, address)
393
394
395def fixed_ip_get_instance_v6(context, address):
396 return IMPL.fixed_ip_get_instance_v6(context, address)
397
398
399def fixed_ip_get_network(context, address):390def fixed_ip_get_network(context, address):
400 """Get a network for a fixed ip by address."""391 """Get a network for a fixed ip by address."""
401 return IMPL.fixed_ip_get_network(context, address)392 return IMPL.fixed_ip_get_network(context, address)
@@ -500,6 +491,11 @@
500 return IMPL.instance_get_all(context)491 return IMPL.instance_get_all(context)
501492
502493
494def instance_get_all_by_filters(context, filters):
495 """Get all instances that match all filters."""
496 return IMPL.instance_get_all_by_filters(context, filters)
497
498
503def instance_get_active_by_window(context, begin, end=None):499def instance_get_active_by_window(context, begin, end=None):
504 """Get instances active during a certain time window."""500 """Get instances active during a certain time window."""
505 return IMPL.instance_get_active_by_window(context, begin, end)501 return IMPL.instance_get_active_by_window(context, begin, end)
@@ -521,10 +517,20 @@
521517
522518
523def instance_get_all_by_reservation(context, reservation_id):519def instance_get_all_by_reservation(context, reservation_id):
524 """Get all instance belonging to a reservation."""520 """Get all instances belonging to a reservation."""
525 return IMPL.instance_get_all_by_reservation(context, reservation_id)521 return IMPL.instance_get_all_by_reservation(context, reservation_id)
526522
527523
524def instance_get_by_fixed_ip(context, address):
525 """Get an instance for a fixed ip by address."""
526 return IMPL.instance_get_by_fixed_ip(context, address)
527
528
529def instance_get_by_fixed_ipv6(context, address):
530 """Get an instance for a fixed ip by IPv6 address."""
531 return IMPL.instance_get_by_fixed_ipv6(context, address)
532
533
528def instance_get_fixed_addresses(context, instance_id):534def instance_get_fixed_addresses(context, instance_id):
529 """Get the fixed ip address of an instance."""535 """Get the fixed ip address of an instance."""
530 return IMPL.instance_get_fixed_addresses(context, instance_id)536 return IMPL.instance_get_fixed_addresses(context, instance_id)
531537
=== modified file 'nova/db/sqlalchemy/api.py'
--- nova/db/sqlalchemy/api.py 2011-08-09 15:27:56 +0000
+++ nova/db/sqlalchemy/api.py 2011-08-09 20:27:18 +0000
@@ -18,6 +18,7 @@
18"""18"""
19Implementation of SQLAlchemy backend.19Implementation of SQLAlchemy backend.
20"""20"""
21import re
21import warnings22import warnings
2223
23from nova import block_device24from nova import block_device
@@ -829,28 +830,6 @@
829 return rv830 return rv
830831
831832
832@require_context
833def fixed_ip_get_instance(context, address):
834 fixed_ip_ref = fixed_ip_get_by_address(context, address)
835 return fixed_ip_ref.instance
836
837
838@require_context
839def fixed_ip_get_instance_v6(context, address):
840 session = get_session()
841
842 # convert IPv6 address to mac
843 mac = ipv6.to_mac(address)
844
845 # get virtual interface
846 vif_ref = virtual_interface_get_by_address(context, mac)
847
848 # look up instance based on instance_id from vif row
849 result = session.query(models.Instance).\
850 filter_by(id=vif_ref['instance_id'])
851 return result
852
853
854@require_admin_context833@require_admin_context
855def fixed_ip_get_network(context, address):834def fixed_ip_get_network(context, address):
856 fixed_ip_ref = fixed_ip_get_by_address(context, address)835 fixed_ip_ref = fixed_ip_get_by_address(context, address)
@@ -1169,6 +1148,114 @@
1169 all()1148 all()
11701149
11711150
1151@require_context
1152def instance_get_all_by_filters(context, filters):
1153 """Return instances that match all filters. Deleted instances
1154 will be returned by default, unless there's a filter that says
1155 otherwise"""
1156
1157 def _regexp_filter_by_ipv6(instance, filter_re):
1158 for interface in instance['virtual_interfaces']:
1159 fixed_ipv6 = interface.get('fixed_ipv6')
1160 if fixed_ipv6 and filter_re.match(fixed_ipv6):
1161 return True
1162 return False
1163
1164 def _regexp_filter_by_ip(instance, filter_re):
1165 for interface in instance['virtual_interfaces']:
1166 for fixed_ip in interface['fixed_ips']:
1167 if not fixed_ip or not fixed_ip['address']:
1168 continue
1169 if filter_re.match(fixed_ip['address']):
1170 return True
1171 for floating_ip in fixed_ip.get('floating_ips', []):
1172 if not floating_ip or not floating_ip['address']:
1173 continue
1174 if filter_re.match(floating_ip['address']):
1175 return True
1176 return False
1177
1178 def _regexp_filter_by_column(instance, filter_name, filter_re):
1179 try:
1180 v = getattr(instance, filter_name)
1181 except AttributeError:
1182 return True
1183 if v and filter_re.match(str(v)):
1184 return True
1185 return False
1186
1187 def _exact_match_filter(query, column, value):
1188 """Do exact match against a column. value to match can be a list
1189 so you can match any value in the list.
1190 """
1191 if isinstance(value, list):
1192 column_attr = getattr(models.Instance, column)
1193 return query.filter(column_attr.in_(value))
1194 else:
1195 filter_dict = {}
1196 filter_dict[column] = value
1197 return query.filter_by(**filter_dict)
1198
1199 session = get_session()
1200 query_prefix = session.query(models.Instance).\
1201 options(joinedload_all('fixed_ips.floating_ips')).\
1202 options(joinedload_all('virtual_interfaces.network')).\
1203 options(joinedload_all(
1204 'virtual_interfaces.fixed_ips.floating_ips')).\
1205 options(joinedload('security_groups')).\
1206 options(joinedload_all('fixed_ips.network')).\
1207 options(joinedload('metadata')).\
1208 options(joinedload('instance_type'))
1209
1210 # Make a copy of the filters dictionary to use going forward, as we'll
1211 # be modifying it and we shouldn't affect the caller's use of it.
1212 filters = filters.copy()
1213
1214 if not context.is_admin:
1215 # If we're not admin context, add appropriate filter..
1216 if context.project_id:
1217 filters['project_id'] = context.project_id
1218 else:
1219 filters['user_id'] = context.user_id
1220
1221 # Filters for exact matches that we can do along with the SQL query...
1222 # For other filters that don't match this, we will do regexp matching
1223 exact_match_filter_names = ['project_id', 'user_id', 'image_ref',
1224 'state', 'instance_type_id', 'deleted']
1225
1226 query_filters = [key for key in filters.iterkeys()
1227 if key in exact_match_filter_names]
1228
1229 for filter_name in query_filters:
1230 # Do the matching and remove the filter from the dictionary
1231 # so we don't try it again below..
1232 query_prefix = _exact_match_filter(query_prefix, filter_name,
1233 filters.pop(filter_name))
1234
1235 instances = query_prefix.all()
1236
1237 if not instances:
1238 return []
1239
1240 # Now filter on everything else for regexp matching..
1241 # For filters not in the list, we'll attempt to use the filter_name
1242 # as a column name in Instance..
1243 regexp_filter_funcs = {'ip6': _regexp_filter_by_ipv6,
1244 'ip': _regexp_filter_by_ip}
1245
1246 for filter_name in filters.iterkeys():
1247 filter_func = regexp_filter_funcs.get(filter_name, None)
1248 filter_re = re.compile(str(filters[filter_name]))
1249 if filter_func:
1250 filter_l = lambda instance: filter_func(instance, filter_re)
1251 else:
1252 filter_l = lambda instance: _regexp_filter_by_column(instance,
1253 filter_name, filter_re)
1254 instances = filter(filter_l, instances)
1255
1256 return instances
1257
1258
1172@require_admin_context1259@require_admin_context
1173def instance_get_active_by_window(context, begin, end=None):1260def instance_get_active_by_window(context, begin, end=None):
1174 """Return instances that were continuously active over the given window"""1261 """Return instances that were continuously active over the given window"""
@@ -1237,30 +1324,48 @@
1237@require_context1324@require_context
1238def instance_get_all_by_reservation(context, reservation_id):1325def instance_get_all_by_reservation(context, reservation_id):
1239 session = get_session()1326 session = get_session()
1327 query = session.query(models.Instance).\
1328 filter_by(reservation_id=reservation_id).\
1329 options(joinedload_all('fixed_ips.floating_ips')).\
1330 options(joinedload('virtual_interfaces')).\
1331 options(joinedload('security_groups')).\
1332 options(joinedload_all('fixed_ips.network')).\
1333 options(joinedload('metadata')).\
1334 options(joinedload('instance_type'))
12401335
1241 if is_admin_context(context):1336 if is_admin_context(context):
1242 return session.query(models.Instance).\1337 return query.\
1243 options(joinedload_all('fixed_ips.floating_ips')).\1338 filter_by(deleted=can_read_deleted(context)).\
1244 options(joinedload('virtual_interfaces')).\1339 all()
1245 options(joinedload('security_groups')).\
1246 options(joinedload_all('fixed_ips.network')).\
1247 options(joinedload('metadata')).\
1248 options(joinedload('instance_type')).\
1249 filter_by(reservation_id=reservation_id).\
1250 filter_by(deleted=can_read_deleted(context)).\
1251 all()
1252 elif is_user_context(context):1340 elif is_user_context(context):
1253 return session.query(models.Instance).\1341 return query.\
1254 options(joinedload_all('fixed_ips.floating_ips')).\1342 filter_by(project_id=context.project_id).\
1255 options(joinedload('virtual_interfaces')).\1343 filter_by(deleted=False).\
1256 options(joinedload('security_groups')).\1344 all()
1257 options(joinedload_all('fixed_ips.network')).\1345
1258 options(joinedload('metadata')).\1346
1259 options(joinedload('instance_type')).\1347@require_context
1260 filter_by(project_id=context.project_id).\1348def instance_get_by_fixed_ip(context, address):
1261 filter_by(reservation_id=reservation_id).\1349 """Return instance ref by exact match of FixedIP"""
1262 filter_by(deleted=False).\1350 fixed_ip_ref = fixed_ip_get_by_address(context, address)
1263 all()1351 return fixed_ip_ref.instance
1352
1353
1354@require_context
1355def instance_get_by_fixed_ipv6(context, address):
1356 """Return instance ref by exact match of IPv6"""
1357 session = get_session()
1358
1359 # convert IPv6 address to mac
1360 mac = ipv6.to_mac(address)
1361
1362 # get virtual interface
1363 vif_ref = virtual_interface_get_by_address(context, mac)
1364
1365 # look up instance based on instance_id from vif row
1366 result = session.query(models.Instance).\
1367 filter_by(id=vif_ref['instance_id'])
1368 return result
12641369
12651370
1266@require_admin_context1371@require_admin_context
@@ -1302,7 +1407,7 @@
1302 network_refs = network_get_all_by_instance(context, instance_id)1407 network_refs = network_get_all_by_instance(context, instance_id)
1303 # compile a list of cidr_v6 prefixes sorted by network id1408 # compile a list of cidr_v6 prefixes sorted by network id
1304 prefixes = [ref.cidr_v6 for ref in1409 prefixes = [ref.cidr_v6 for ref in
1305 sorted(network_refs, key=lambda ref: ref.id)]1410 sorted(network_refs, key=lambda ref: ref.id)]
1306 # get vifs associated with instance1411 # get vifs associated with instance
1307 vif_refs = virtual_interface_get_by_instance(context, instance_ref.id)1412 vif_refs = virtual_interface_get_by_instance(context, instance_ref.id)
1308 # compile list of the mac_addresses for vifs sorted by network id1413 # compile list of the mac_addresses for vifs sorted by network id
13091414
=== modified file 'nova/db/sqlalchemy/models.py'
--- nova/db/sqlalchemy/models.py 2011-08-03 11:31:10 +0000
+++ nova/db/sqlalchemy/models.py 2011-08-09 20:27:18 +0000
@@ -180,6 +180,7 @@
180 image_ref = Column(String(255))180 image_ref = Column(String(255))
181 kernel_id = Column(String(255))181 kernel_id = Column(String(255))
182 ramdisk_id = Column(String(255))182 ramdisk_id = Column(String(255))
183 server_name = Column(String(255))
183184
184# image_ref = Column(Integer, ForeignKey('images.id'), nullable=True)185# image_ref = Column(Integer, ForeignKey('images.id'), nullable=True)
185# kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True)186# kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True)
186187
=== modified file 'nova/tests/api/openstack/fakes.py'
--- nova/tests/api/openstack/fakes.py 2011-08-02 22:57:24 +0000
+++ nova/tests/api/openstack/fakes.py 2011-08-09 20:27:18 +0000
@@ -71,14 +71,18 @@
71 return self.application71 return self.application
7272
7373
74def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True):74def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True,
75 fake_auth_context=None):
75 if not inner_app10:76 if not inner_app10:
76 inner_app10 = openstack.APIRouterV10()77 inner_app10 = openstack.APIRouterV10()
77 if not inner_app11:78 if not inner_app11:
78 inner_app11 = openstack.APIRouterV11()79 inner_app11 = openstack.APIRouterV11()
7980
80 if fake_auth:81 if fake_auth:
81 ctxt = context.RequestContext('fake', 'fake')82 if fake_auth_context is not None:
83 ctxt = fake_auth_context
84 else:
85 ctxt = context.RequestContext('fake', 'fake')
82 api10 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,86 api10 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
83 limits.RateLimitingMiddleware(inner_app10)))87 limits.RateLimitingMiddleware(inner_app10)))
84 api11 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,88 api11 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
8589
=== modified file 'nova/tests/api/openstack/test_servers.py'
--- nova/tests/api/openstack/test_servers.py 2011-08-09 04:47:16 +0000
+++ nova/tests/api/openstack/test_servers.py 2011-08-09 20:27:18 +0000
@@ -236,7 +236,8 @@
236 fakes.stub_out_key_pair_funcs(self.stubs)236 fakes.stub_out_key_pair_funcs(self.stubs)
237 fakes.stub_out_image_service(self.stubs)237 fakes.stub_out_image_service(self.stubs)
238 self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid)238 self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid)
239 self.stubs.Set(nova.db.api, 'instance_get_all', return_servers)239 self.stubs.Set(nova.db.api, 'instance_get_all_by_filters',
240 return_servers)
240 self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)241 self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
241 self.stubs.Set(nova.db, 'instance_get_by_uuid',242 self.stubs.Set(nova.db, 'instance_get_by_uuid',
242 return_server_by_uuid)243 return_server_by_uuid)
@@ -1098,6 +1099,277 @@
1098 self.assertEqual(res.status_int, 400)1099 self.assertEqual(res.status_int, 400)
1099 self.assertTrue(res.body.find('marker param') > -1)1100 self.assertTrue(res.body.find('marker param') > -1)
11001101
1102 def test_get_servers_with_bad_option_v1_0(self):
1103 # 1.0 API ignores unknown options
1104 def fake_get_all(compute_self, context, search_opts=None):
1105 return [stub_instance(100)]
1106
1107 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1108
1109 req = webob.Request.blank('/v1.0/servers?unknownoption=whee')
1110 res = req.get_response(fakes.wsgi_app())
1111 self.assertEqual(res.status_int, 200)
1112 servers = json.loads(res.body)['servers']
1113 self.assertEqual(len(servers), 1)
1114 self.assertEqual(servers[0]['id'], 100)
1115
1116 def test_get_servers_with_bad_option_v1_1(self):
1117 # 1.1 API also ignores unknown options
1118 def fake_get_all(compute_self, context, search_opts=None):
1119 return [stub_instance(100)]
1120
1121 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1122
1123 req = webob.Request.blank('/v1.1/servers?unknownoption=whee')
1124 res = req.get_response(fakes.wsgi_app())
1125 self.assertEqual(res.status_int, 200)
1126 servers = json.loads(res.body)['servers']
1127 self.assertEqual(len(servers), 1)
1128 self.assertEqual(servers[0]['id'], 100)
1129
1130 def test_get_servers_allows_image_v1_1(self):
1131 def fake_get_all(compute_self, context, search_opts=None):
1132 self.assertNotEqual(search_opts, None)
1133 self.assertTrue('image' in search_opts)
1134 self.assertEqual(search_opts['image'], '12345')
1135 return [stub_instance(100)]
1136
1137 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1138 self.flags(allow_admin_api=False)
1139
1140 req = webob.Request.blank('/v1.1/servers?image=12345')
1141 res = req.get_response(fakes.wsgi_app())
1142 # The following assert will fail if either of the asserts in
1143 # fake_get_all() fail
1144 self.assertEqual(res.status_int, 200)
1145 servers = json.loads(res.body)['servers']
1146 self.assertEqual(len(servers), 1)
1147 self.assertEqual(servers[0]['id'], 100)
1148
1149 def test_get_servers_allows_flavor_v1_1(self):
1150 def fake_get_all(compute_self, context, search_opts=None):
1151 self.assertNotEqual(search_opts, None)
1152 self.assertTrue('flavor' in search_opts)
1153 # flavor is an integer ID
1154 self.assertEqual(search_opts['flavor'], '12345')
1155 return [stub_instance(100)]
1156
1157 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1158 self.flags(allow_admin_api=False)
1159
1160 req = webob.Request.blank('/v1.1/servers?flavor=12345')
1161 res = req.get_response(fakes.wsgi_app())
1162 # The following assert will fail if either of the asserts in
1163 # fake_get_all() fail
1164 self.assertEqual(res.status_int, 200)
1165 servers = json.loads(res.body)['servers']
1166 self.assertEqual(len(servers), 1)
1167 self.assertEqual(servers[0]['id'], 100)
1168
1169 def test_get_servers_allows_status_v1_1(self):
1170 def fake_get_all(compute_self, context, search_opts=None):
1171 self.assertNotEqual(search_opts, None)
1172 self.assertTrue('state' in search_opts)
1173 self.assertEqual(set(search_opts['state']),
1174 set([power_state.RUNNING, power_state.BLOCKED]))
1175 return [stub_instance(100)]
1176
1177 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1178 self.flags(allow_admin_api=False)
1179
1180 req = webob.Request.blank('/v1.1/servers?status=active')
1181 res = req.get_response(fakes.wsgi_app())
1182 # The following assert will fail if either of the asserts in
1183 # fake_get_all() fail
1184 self.assertEqual(res.status_int, 200)
1185 servers = json.loads(res.body)['servers']
1186 self.assertEqual(len(servers), 1)
1187 self.assertEqual(servers[0]['id'], 100)
1188
1189 def test_get_servers_invalid_status_v1_1(self):
1190 """Test getting servers by invalid status"""
1191
1192 self.flags(allow_admin_api=False)
1193
1194 req = webob.Request.blank('/v1.1/servers?status=running')
1195 res = req.get_response(fakes.wsgi_app())
1196 # The following assert will fail if either of the asserts in
1197 # fake_get_all() fail
1198 self.assertEqual(res.status_int, 400)
1199 self.assertTrue(res.body.find('Invalid server status') > -1)
1200
1201 def test_get_servers_allows_name_v1_1(self):
1202 def fake_get_all(compute_self, context, search_opts=None):
1203 self.assertNotEqual(search_opts, None)
1204 self.assertTrue('name' in search_opts)
1205 self.assertEqual(search_opts['name'], 'whee.*')
1206 return [stub_instance(100)]
1207
1208 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1209 self.flags(allow_admin_api=False)
1210
1211 req = webob.Request.blank('/v1.1/servers?name=whee.*')
1212 res = req.get_response(fakes.wsgi_app())
1213 # The following assert will fail if either of the asserts in
1214 # fake_get_all() fail
1215 self.assertEqual(res.status_int, 200)
1216 servers = json.loads(res.body)['servers']
1217 self.assertEqual(len(servers), 1)
1218 self.assertEqual(servers[0]['id'], 100)
1219
1220 def test_get_servers_unknown_or_admin_options1(self):
1221 """Test getting servers by admin-only or unknown options.
1222 This tests when admin_api is off. Make sure the admin and
1223 unknown options are stripped before they get to
1224 compute_api.get_all()
1225 """
1226
1227 self.flags(allow_admin_api=False)
1228
1229 def fake_get_all(compute_self, context, search_opts=None):
1230 self.assertNotEqual(search_opts, None)
1231 # Allowed by user
1232 self.assertTrue('name' in search_opts)
1233 self.assertTrue('status' in search_opts)
1234 # Allowed only by admins with admin API on
1235 self.assertFalse('ip' in search_opts)
1236 self.assertFalse('unknown_option' in search_opts)
1237 return [stub_instance(100)]
1238
1239 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1240
1241 query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
1242 req = webob.Request.blank('/v1.1/servers?%s' % query_str)
1243 # Request admin context
1244 context = nova.context.RequestContext('testuser', 'testproject',
1245 is_admin=True)
1246 res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
1247 # The following assert will fail if either of the asserts in
1248 # fake_get_all() fail
1249 self.assertEqual(res.status_int, 200)
1250 servers = json.loads(res.body)['servers']
1251 self.assertEqual(len(servers), 1)
1252 self.assertEqual(servers[0]['id'], 100)
1253
1254 def test_get_servers_unknown_or_admin_options2(self):
1255 """Test getting servers by admin-only or unknown options.
1256 This tests when admin_api is on, but context is a user.
1257 Make sure the admin and unknown options are stripped before
1258 they get to compute_api.get_all()
1259 """
1260
1261 self.flags(allow_admin_api=True)
1262
1263 def fake_get_all(compute_self, context, search_opts=None):
1264 self.assertNotEqual(search_opts, None)
1265 # Allowed by user
1266 self.assertTrue('name' in search_opts)
1267 self.assertTrue('status' in search_opts)
1268 # Allowed only by admins with admin API on
1269 self.assertFalse('ip' in search_opts)
1270 self.assertFalse('unknown_option' in search_opts)
1271 return [stub_instance(100)]
1272
1273 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1274
1275 query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
1276 req = webob.Request.blank('/v1.1/servers?%s' % query_str)
1277 # Request admin context
1278 context = nova.context.RequestContext('testuser', 'testproject',
1279 is_admin=False)
1280 res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
1281 # The following assert will fail if either of the asserts in
1282 # fake_get_all() fail
1283 self.assertEqual(res.status_int, 200)
1284 servers = json.loads(res.body)['servers']
1285 self.assertEqual(len(servers), 1)
1286 self.assertEqual(servers[0]['id'], 100)
1287
1288 def test_get_servers_unknown_or_admin_options3(self):
1289 """Test getting servers by admin-only or unknown options.
1290 This tests when admin_api is on and context is admin.
1291 All options should be passed through to compute_api.get_all()
1292 """
1293
1294 self.flags(allow_admin_api=True)
1295
1296 def fake_get_all(compute_self, context, search_opts=None):
1297 self.assertNotEqual(search_opts, None)
1298 # Allowed by user
1299 self.assertTrue('name' in search_opts)
1300 self.assertTrue('status' in search_opts)
1301 # Allowed only by admins with admin API on
1302 self.assertTrue('ip' in search_opts)
1303 self.assertTrue('unknown_option' in search_opts)
1304 return [stub_instance(100)]
1305
1306 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1307
1308 query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
1309 req = webob.Request.blank('/v1.1/servers?%s' % query_str)
1310 # Request admin context
1311 context = nova.context.RequestContext('testuser', 'testproject',
1312 is_admin=True)
1313 res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
1314 # The following assert will fail if either of the asserts in
1315 # fake_get_all() fail
1316 self.assertEqual(res.status_int, 200)
1317 servers = json.loads(res.body)['servers']
1318 self.assertEqual(len(servers), 1)
1319 self.assertEqual(servers[0]['id'], 100)
1320
1321 def test_get_servers_admin_allows_ip_v1_1(self):
1322 """Test getting servers by ip with admin_api enabled and
1323 admin context
1324 """
1325 self.flags(allow_admin_api=True)
1326
1327 def fake_get_all(compute_self, context, search_opts=None):
1328 self.assertNotEqual(search_opts, None)
1329 self.assertTrue('ip' in search_opts)
1330 self.assertEqual(search_opts['ip'], '10\..*')
1331 return [stub_instance(100)]
1332
1333 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1334
1335 req = webob.Request.blank('/v1.1/servers?ip=10\..*')
1336 # Request admin context
1337 context = nova.context.RequestContext('testuser', 'testproject',
1338 is_admin=True)
1339 res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
1340 # The following assert will fail if either of the asserts in
1341 # fake_get_all() fail
1342 self.assertEqual(res.status_int, 200)
1343 servers = json.loads(res.body)['servers']
1344 self.assertEqual(len(servers), 1)
1345 self.assertEqual(servers[0]['id'], 100)
1346
1347 def test_get_servers_admin_allows_ip6_v1_1(self):
1348 """Test getting servers by ip6 with admin_api enabled and
1349 admin context
1350 """
1351 self.flags(allow_admin_api=True)
1352
1353 def fake_get_all(compute_self, context, search_opts=None):
1354 self.assertNotEqual(search_opts, None)
1355 self.assertTrue('ip6' in search_opts)
1356 self.assertEqual(search_opts['ip6'], 'ffff.*')
1357 return [stub_instance(100)]
1358
1359 self.stubs.Set(nova.compute.API, 'get_all', fake_get_all)
1360
1361 req = webob.Request.blank('/v1.1/servers?ip6=ffff.*')
1362 # Request admin context
1363 context = nova.context.RequestContext('testuser', 'testproject',
1364 is_admin=True)
1365 res = req.get_response(fakes.wsgi_app(fake_auth_context=context))
1366 # The following assert will fail if either of the asserts in
1367 # fake_get_all() fail
1368 self.assertEqual(res.status_int, 200)
1369 servers = json.loads(res.body)['servers']
1370 self.assertEqual(len(servers), 1)
1371 self.assertEqual(servers[0]['id'], 100)
1372
1101 def _setup_for_create_instance(self):1373 def _setup_for_create_instance(self):
1102 """Shared implementation for tests below that create instance"""1374 """Shared implementation for tests below that create instance"""
1103 def instance_create(context, inst):1375 def instance_create(context, inst):
@@ -1665,6 +1937,7 @@
1665 def test_get_all_server_details_v1_0(self):1937 def test_get_all_server_details_v1_0(self):
1666 req = webob.Request.blank('/v1.0/servers/detail')1938 req = webob.Request.blank('/v1.0/servers/detail')
1667 res = req.get_response(fakes.wsgi_app())1939 res = req.get_response(fakes.wsgi_app())
1940 self.assertEqual(res.status_int, 200)
1668 res_dict = json.loads(res.body)1941 res_dict = json.loads(res.body)
16691942
1670 for i, s in enumerate(res_dict['servers']):1943 for i, s in enumerate(res_dict['servers']):
@@ -1720,7 +1993,7 @@
1720 return [stub_instance(i, 'fake', 'fake', None, None, i % 2)1993 return [stub_instance(i, 'fake', 'fake', None, None, i % 2)
1721 for i in xrange(5)]1994 for i in xrange(5)]
17221995
1723 self.stubs.Set(nova.db.api, 'instance_get_all_by_project',1996 self.stubs.Set(nova.db.api, 'instance_get_all_by_filters',
1724 return_servers_with_host)1997 return_servers_with_host)
17251998
1726 req = webob.Request.blank('/v1.0/servers/detail')1999 req = webob.Request.blank('/v1.0/servers/detail')
17272000
=== modified file 'nova/tests/test_compute.py'
--- nova/tests/test_compute.py 2011-08-05 05:06:22 +0000
+++ nova/tests/test_compute.py 2011-08-09 20:27:18 +0000
@@ -26,6 +26,7 @@
26from nova import context26from nova import context
27from nova import db27from nova import db
28from nova.db.sqlalchemy import models28from nova.db.sqlalchemy import models
29from nova.db.sqlalchemy import api as sqlalchemy_api
29from nova import exception30from nova import exception
30from nova import flags31from nova import flags
31import nova.image.fake32import nova.image.fake
@@ -73,8 +74,11 @@
7374
74 self.stubs.Set(nova.image.fake._FakeImageService, 'show', fake_show)75 self.stubs.Set(nova.image.fake._FakeImageService, 'show', fake_show)
7576
76 def _create_instance(self, params={}):77 def _create_instance(self, params=None):
77 """Create a test instance"""78 """Create a test instance"""
79
80 if params is None:
81 params = {}
78 inst = {}82 inst = {}
79 inst['image_ref'] = 183 inst['image_ref'] = 1
80 inst['reservation_id'] = 'r-fakeres'84 inst['reservation_id'] = 'r-fakeres'
@@ -864,6 +868,458 @@
864 self.assertEqual(len(instances), 1)868 self.assertEqual(len(instances), 1)
865 self.assertEqual(power_state.SHUTOFF, instances[0]['state'])869 self.assertEqual(power_state.SHUTOFF, instances[0]['state'])
866870
871 def test_get_all_by_name_regexp(self):
872 """Test searching instances by name (display_name)"""
873 c = context.get_admin_context()
874 instance_id1 = self._create_instance({'display_name': 'woot'})
875 instance_id2 = self._create_instance({
876 'display_name': 'woo',
877 'id': 20})
878 instance_id3 = self._create_instance({
879 'display_name': 'not-woot',
880 'id': 30})
881
882 instances = self.compute_api.get_all(c,
883 search_opts={'name': 'woo.*'})
884 self.assertEqual(len(instances), 2)
885 instance_ids = [instance.id for instance in instances]
886 self.assertTrue(instance_id1 in instance_ids)
887 self.assertTrue(instance_id2 in instance_ids)
888
889 instances = self.compute_api.get_all(c,
890 search_opts={'name': 'woot.*'})
891 instance_ids = [instance.id for instance in instances]
892 self.assertEqual(len(instances), 1)
893 self.assertTrue(instance_id1 in instance_ids)
894
895 instances = self.compute_api.get_all(c,
896 search_opts={'name': '.*oot.*'})
897 self.assertEqual(len(instances), 2)
898 instance_ids = [instance.id for instance in instances]
899 self.assertTrue(instance_id1 in instance_ids)
900 self.assertTrue(instance_id3 in instance_ids)
901
902 instances = self.compute_api.get_all(c,
903 search_opts={'name': 'n.*'})
904 self.assertEqual(len(instances), 1)
905 instance_ids = [instance.id for instance in instances]
906 self.assertTrue(instance_id3 in instance_ids)
907
908 instances = self.compute_api.get_all(c,
909 search_opts={'name': 'noth.*'})
910 self.assertEqual(len(instances), 0)
911
912 db.instance_destroy(c, instance_id1)
913 db.instance_destroy(c, instance_id2)
914 db.instance_destroy(c, instance_id3)
915
916 def test_get_all_by_instance_name_regexp(self):
917 """Test searching instances by name"""
918 self.flags(instance_name_template='instance-%d')
919
920 c = context.get_admin_context()
921 instance_id1 = self._create_instance()
922 instance_id2 = self._create_instance({'id': 2})
923 instance_id3 = self._create_instance({'id': 10})
924
925 instances = self.compute_api.get_all(c,
926 search_opts={'instance_name': 'instance.*'})
927 self.assertEqual(len(instances), 3)
928
929 instances = self.compute_api.get_all(c,
930 search_opts={'instance_name': '.*\-\d$'})
931 self.assertEqual(len(instances), 2)
932 instance_ids = [instance.id for instance in instances]
933 self.assertTrue(instance_id1 in instance_ids)
934 self.assertTrue(instance_id2 in instance_ids)
935
936 instances = self.compute_api.get_all(c,
937 search_opts={'instance_name': 'i.*2'})
938 self.assertEqual(len(instances), 1)
939 self.assertEqual(instances[0].id, instance_id2)
940
941 db.instance_destroy(c, instance_id1)
942 db.instance_destroy(c, instance_id2)
943 db.instance_destroy(c, instance_id3)
944
945 def test_get_by_fixed_ip(self):
946 """Test getting 1 instance by Fixed IP"""
947 c = context.get_admin_context()
948 instance_id1 = self._create_instance()
949 instance_id2 = self._create_instance({'id': 20})
950 instance_id3 = self._create_instance({'id': 30})
951
952 vif_ref1 = db.virtual_interface_create(c,
953 {'address': '12:34:56:78:90:12',
954 'instance_id': instance_id1,
955 'network_id': 1})
956 vif_ref2 = db.virtual_interface_create(c,
957 {'address': '90:12:34:56:78:90',
958 'instance_id': instance_id2,
959 'network_id': 1})
960
961 db.fixed_ip_create(c,
962 {'address': '1.1.1.1',
963 'instance_id': instance_id1,
964 'virtual_interface_id': vif_ref1['id']})
965 db.fixed_ip_create(c,
966 {'address': '1.1.2.1',
967 'instance_id': instance_id2,
968 'virtual_interface_id': vif_ref2['id']})
969
970 # regex not allowed
971 instances = self.compute_api.get_all(c,
972 search_opts={'fixed_ip': '.*'})
973 self.assertEqual(len(instances), 0)
974
975 instances = self.compute_api.get_all(c,
976 search_opts={'fixed_ip': '1.1.3.1'})
977 self.assertEqual(len(instances), 0)
978
979 instances = self.compute_api.get_all(c,
980 search_opts={'fixed_ip': '1.1.1.1'})
981 self.assertEqual(len(instances), 1)
982 self.assertEqual(instances[0].id, instance_id1)
983
984 instances = self.compute_api.get_all(c,
985 search_opts={'fixed_ip': '1.1.2.1'})
986 self.assertEqual(len(instances), 1)
987 self.assertEqual(instances[0].id, instance_id2)
988
989 db.virtual_interface_delete(c, vif_ref1['id'])
990 db.virtual_interface_delete(c, vif_ref2['id'])
991 db.instance_destroy(c, instance_id1)
992 db.instance_destroy(c, instance_id2)
993
994 def test_get_all_by_ip_regexp(self):
995 """Test searching by Floating and Fixed IP"""
996 c = context.get_admin_context()
997 instance_id1 = self._create_instance({'display_name': 'woot'})
998 instance_id2 = self._create_instance({
999 'display_name': 'woo',
1000 'id': 20})
1001 instance_id3 = self._create_instance({
1002 'display_name': 'not-woot',
1003 'id': 30})
1004
1005 vif_ref1 = db.virtual_interface_create(c,
1006 {'address': '12:34:56:78:90:12',
1007 'instance_id': instance_id1,
1008 'network_id': 1})
1009 vif_ref2 = db.virtual_interface_create(c,
1010 {'address': '90:12:34:56:78:90',
1011 'instance_id': instance_id2,
1012 'network_id': 1})
1013 vif_ref3 = db.virtual_interface_create(c,
1014 {'address': '34:56:78:90:12:34',
1015 'instance_id': instance_id3,
1016 'network_id': 1})
1017
1018 db.fixed_ip_create(c,
1019 {'address': '1.1.1.1',
1020 'instance_id': instance_id1,
1021 'virtual_interface_id': vif_ref1['id']})
1022 db.fixed_ip_create(c,
1023 {'address': '1.1.2.1',
1024 'instance_id': instance_id2,
1025 'virtual_interface_id': vif_ref2['id']})
1026 fix_addr = db.fixed_ip_create(c,
1027 {'address': '1.1.3.1',
1028 'instance_id': instance_id3,
1029 'virtual_interface_id': vif_ref3['id']})
1030 fix_ref = db.fixed_ip_get_by_address(c, fix_addr)
1031 flo_ref = db.floating_ip_create(c,
1032 {'address': '10.0.0.2',
1033 'fixed_ip_id': fix_ref['id']})
1034
1035 # ends up matching 2nd octet here.. so all 3 match
1036 instances = self.compute_api.get_all(c,
1037 search_opts={'ip': '.*\.1'})
1038 self.assertEqual(len(instances), 3)
1039
1040 instances = self.compute_api.get_all(c,
1041 search_opts={'ip': '1.*'})
1042 self.assertEqual(len(instances), 3)
1043
1044 instances = self.compute_api.get_all(c,
1045 search_opts={'ip': '.*\.1.\d+$'})
1046 self.assertEqual(len(instances), 1)
1047 instance_ids = [instance.id for instance in instances]
1048 self.assertTrue(instance_id1 in instance_ids)
1049
1050 instances = self.compute_api.get_all(c,
1051 search_opts={'ip': '.*\.2.+'})
1052 self.assertEqual(len(instances), 1)
1053 self.assertEqual(instances[0].id, instance_id2)
1054
1055 instances = self.compute_api.get_all(c,
1056 search_opts={'ip': '10.*'})
1057 self.assertEqual(len(instances), 1)
1058 self.assertEqual(instances[0].id, instance_id3)
1059
1060 db.virtual_interface_delete(c, vif_ref1['id'])
1061 db.virtual_interface_delete(c, vif_ref2['id'])
1062 db.virtual_interface_delete(c, vif_ref3['id'])
1063 db.floating_ip_destroy(c, '10.0.0.2')
1064 db.instance_destroy(c, instance_id1)
1065 db.instance_destroy(c, instance_id2)
1066 db.instance_destroy(c, instance_id3)
1067
1068 def test_get_all_by_ipv6_regexp(self):
1069 """Test searching by IPv6 address"""
1070
1071 c = context.get_admin_context()
1072 instance_id1 = self._create_instance({'display_name': 'woot'})
1073 instance_id2 = self._create_instance({
1074 'display_name': 'woo',
1075 'id': 20})
1076 instance_id3 = self._create_instance({
1077 'display_name': 'not-woot',
1078 'id': 30})
1079
1080 vif_ref1 = db.virtual_interface_create(c,
1081 {'address': '12:34:56:78:90:12',
1082 'instance_id': instance_id1,
1083 'network_id': 1})
1084 vif_ref2 = db.virtual_interface_create(c,
1085 {'address': '90:12:34:56:78:90',
1086 'instance_id': instance_id2,
1087 'network_id': 1})
1088 vif_ref3 = db.virtual_interface_create(c,
1089 {'address': '34:56:78:90:12:34',
1090 'instance_id': instance_id3,
1091 'network_id': 1})
1092
1093 # This will create IPv6 addresses of:
1094 # 1: fd00::1034:56ff:fe78:9012
1095 # 20: fd00::9212:34ff:fe56:7890
1096 # 30: fd00::3656:78ff:fe90:1234
1097
1098 instances = self.compute_api.get_all(c,
1099 search_opts={'ip6': '.*1034.*'})
1100 self.assertEqual(len(instances), 1)
1101 self.assertEqual(instances[0].id, instance_id1)
1102
1103 instances = self.compute_api.get_all(c,
1104 search_opts={'ip6': '^fd00.*'})
1105 self.assertEqual(len(instances), 3)
1106 instance_ids = [instance.id for instance in instances]
1107 self.assertTrue(instance_id1 in instance_ids)
1108 self.assertTrue(instance_id2 in instance_ids)
1109 self.assertTrue(instance_id3 in instance_ids)
1110
1111 instances = self.compute_api.get_all(c,
1112 search_opts={'ip6': '^.*12.*34.*'})
1113 self.assertEqual(len(instances), 2)
1114 instance_ids = [instance.id for instance in instances]
1115 self.assertTrue(instance_id2 in instance_ids)
1116 self.assertTrue(instance_id3 in instance_ids)
1117
1118 db.virtual_interface_delete(c, vif_ref1['id'])
1119 db.virtual_interface_delete(c, vif_ref2['id'])
1120 db.virtual_interface_delete(c, vif_ref3['id'])
1121 db.instance_destroy(c, instance_id1)
1122 db.instance_destroy(c, instance_id2)
1123 db.instance_destroy(c, instance_id3)
1124
1125 def test_get_all_by_multiple_options_at_once(self):
1126 """Test searching by multiple options at once"""
1127 c = context.get_admin_context()
1128 instance_id1 = self._create_instance({'display_name': 'woot'})
1129 instance_id2 = self._create_instance({
1130 'display_name': 'woo',
1131 'id': 20})
1132 instance_id3 = self._create_instance({
1133 'display_name': 'not-woot',
1134 'id': 30})
1135
1136 vif_ref1 = db.virtual_interface_create(c,
1137 {'address': '12:34:56:78:90:12',
1138 'instance_id': instance_id1,
1139 'network_id': 1})
1140 vif_ref2 = db.virtual_interface_create(c,
1141 {'address': '90:12:34:56:78:90',
1142 'instance_id': instance_id2,
1143 'network_id': 1})
1144 vif_ref3 = db.virtual_interface_create(c,
1145 {'address': '34:56:78:90:12:34',
1146 'instance_id': instance_id3,
1147 'network_id': 1})
1148
1149 db.fixed_ip_create(c,
1150 {'address': '1.1.1.1',
1151 'instance_id': instance_id1,
1152 'virtual_interface_id': vif_ref1['id']})
1153 db.fixed_ip_create(c,
1154 {'address': '1.1.2.1',
1155 'instance_id': instance_id2,
1156 'virtual_interface_id': vif_ref2['id']})
1157 fix_addr = db.fixed_ip_create(c,
1158 {'address': '1.1.3.1',
1159 'instance_id': instance_id3,
1160 'virtual_interface_id': vif_ref3['id']})
1161 fix_ref = db.fixed_ip_get_by_address(c, fix_addr)
1162 flo_ref = db.floating_ip_create(c,
1163 {'address': '10.0.0.2',
1164 'fixed_ip_id': fix_ref['id']})
1165
1166 # ip ends up matching 2nd octet here.. so all 3 match ip
1167 # but 'name' only matches one
1168 instances = self.compute_api.get_all(c,
1169 search_opts={'ip': '.*\.1', 'name': 'not.*'})
1170 self.assertEqual(len(instances), 1)
1171 self.assertEqual(instances[0].id, instance_id3)
1172
1173 # ip ends up matching any ip with a '2' in it.. so instance
1174 # 2 and 3.. but name should only match #2
1175 # but 'name' only matches one
1176 instances = self.compute_api.get_all(c,
1177 search_opts={'ip': '.*2', 'name': '^woo.*'})
1178 self.assertEqual(len(instances), 1)
1179 self.assertEqual(instances[0].id, instance_id2)
1180
1181 # same as above but no match on name (name matches instance_id1
1182 # but the ip query doesn't
1183 instances = self.compute_api.get_all(c,
1184 search_opts={'ip': '.*2.*', 'name': '^woot.*'})
1185 self.assertEqual(len(instances), 0)
1186
1187 # ip matches all 3... ipv6 matches #2+#3...name matches #3
1188 instances = self.compute_api.get_all(c,
1189 search_opts={'ip': '.*\.1',
1190 'name': 'not.*',
1191 'ip6': '^.*12.*34.*'})
1192 self.assertEqual(len(instances), 1)
1193 self.assertEqual(instances[0].id, instance_id3)
1194
1195 db.virtual_interface_delete(c, vif_ref1['id'])
1196 db.virtual_interface_delete(c, vif_ref2['id'])
1197 db.virtual_interface_delete(c, vif_ref3['id'])
1198 db.floating_ip_destroy(c, '10.0.0.2')
1199 db.instance_destroy(c, instance_id1)
1200 db.instance_destroy(c, instance_id2)
1201 db.instance_destroy(c, instance_id3)
1202
1203 def test_get_all_by_image(self):
1204 """Test searching instances by image"""
1205
1206 c = context.get_admin_context()
1207 instance_id1 = self._create_instance({'image_ref': '1234'})
1208 instance_id2 = self._create_instance({
1209 'id': 2,
1210 'image_ref': '4567'})
1211 instance_id3 = self._create_instance({
1212 'id': 10,
1213 'image_ref': '4567'})
1214
1215 instances = self.compute_api.get_all(c,
1216 search_opts={'image': '123'})
1217 self.assertEqual(len(instances), 0)
1218
1219 instances = self.compute_api.get_all(c,
1220 search_opts={'image': '1234'})
1221 self.assertEqual(len(instances), 1)
1222 self.assertEqual(instances[0].id, instance_id1)
1223
1224 instances = self.compute_api.get_all(c,
1225 search_opts={'image': '4567'})
1226 self.assertEqual(len(instances), 2)
1227 instance_ids = [instance.id for instance in instances]
1228 self.assertTrue(instance_id2 in instance_ids)
1229 self.assertTrue(instance_id3 in instance_ids)
1230
1231 # Test passing a list as search arg
1232 instances = self.compute_api.get_all(c,
1233 search_opts={'image': ['1234', '4567']})
1234 self.assertEqual(len(instances), 3)
1235
1236 db.instance_destroy(c, instance_id1)
1237 db.instance_destroy(c, instance_id2)
1238 db.instance_destroy(c, instance_id3)
1239
1240 def test_get_all_by_flavor(self):
1241 """Test searching instances by image"""
1242
1243 c = context.get_admin_context()
1244 instance_id1 = self._create_instance({'instance_type_id': 1})
1245 instance_id2 = self._create_instance({
1246 'id': 2,
1247 'instance_type_id': 2})
1248 instance_id3 = self._create_instance({
1249 'id': 10,
1250 'instance_type_id': 2})
1251
1252 # NOTE(comstud): Migrations set up the instance_types table
1253 # for us. Therefore, we assume the following is true for
1254 # these tests:
1255 # instance_type_id 1 == flavor 3
1256 # instance_type_id 2 == flavor 1
1257 # instance_type_id 3 == flavor 4
1258 # instance_type_id 4 == flavor 5
1259 # instance_type_id 5 == flavor 2
1260
1261 instances = self.compute_api.get_all(c,
1262 search_opts={'flavor': 5})
1263 self.assertEqual(len(instances), 0)
1264
1265 self.assertRaises(exception.FlavorNotFound,
1266 self.compute_api.get_all,
1267 c, search_opts={'flavor': 99})
1268
1269 instances = self.compute_api.get_all(c,
1270 search_opts={'flavor': 3})
1271 self.assertEqual(len(instances), 1)
1272 self.assertEqual(instances[0].id, instance_id1)
1273
1274 instances = self.compute_api.get_all(c,
1275 search_opts={'flavor': 1})
1276 self.assertEqual(len(instances), 2)
1277 instance_ids = [instance.id for instance in instances]
1278 self.assertTrue(instance_id2 in instance_ids)
1279 self.assertTrue(instance_id3 in instance_ids)
1280
1281 db.instance_destroy(c, instance_id1)
1282 db.instance_destroy(c, instance_id2)
1283 db.instance_destroy(c, instance_id3)
1284
1285 def test_get_all_by_state(self):
1286 """Test searching instances by state"""
1287
1288 c = context.get_admin_context()
1289 instance_id1 = self._create_instance({'state': power_state.SHUTDOWN})
1290 instance_id2 = self._create_instance({
1291 'id': 2,
1292 'state': power_state.RUNNING})
1293 instance_id3 = self._create_instance({
1294 'id': 10,
1295 'state': power_state.RUNNING})
1296
1297 instances = self.compute_api.get_all(c,
1298 search_opts={'state': power_state.SUSPENDED})
1299 self.assertEqual(len(instances), 0)
1300
1301 instances = self.compute_api.get_all(c,
1302 search_opts={'state': power_state.SHUTDOWN})
1303 self.assertEqual(len(instances), 1)
1304 self.assertEqual(instances[0].id, instance_id1)
1305
1306 instances = self.compute_api.get_all(c,
1307 search_opts={'state': power_state.RUNNING})
1308 self.assertEqual(len(instances), 2)
1309 instance_ids = [instance.id for instance in instances]
1310 self.assertTrue(instance_id2 in instance_ids)
1311 self.assertTrue(instance_id3 in instance_ids)
1312
1313 # Test passing a list as search arg
1314 instances = self.compute_api.get_all(c,
1315 search_opts={'state': [power_state.SHUTDOWN,
1316 power_state.RUNNING]})
1317 self.assertEqual(len(instances), 3)
1318
1319 db.instance_destroy(c, instance_id1)
1320 db.instance_destroy(c, instance_id2)
1321 db.instance_destroy(c, instance_id3)
1322
867 @staticmethod1323 @staticmethod
868 def _parse_db_block_device_mapping(bdm_ref):1324 def _parse_db_block_device_mapping(bdm_ref):
869 attr_list = ('delete_on_termination', 'device_name', 'no_device',1325 attr_list = ('delete_on_termination', 'device_name', 'no_device',
8701326
=== modified file 'nova/tests/test_metadata.py'
--- nova/tests/test_metadata.py 2011-07-23 07:57:04 +0000
+++ nova/tests/test_metadata.py 2011-08-09 20:27:18 +0000
@@ -43,17 +43,21 @@
43 'reservation_id': 'r-xxxxxxxx',43 'reservation_id': 'r-xxxxxxxx',
44 'user_data': '',44 'user_data': '',
45 'image_ref': 7,45 'image_ref': 7,
46 'fixed_ips': [],
46 'root_device_name': '/dev/sda1',47 'root_device_name': '/dev/sda1',
47 'hostname': 'test'})48 'hostname': 'test'})
4849
49 def instance_get(*args, **kwargs):50 def instance_get(*args, **kwargs):
50 return self.instance51 return self.instance
5152
53 def instance_get_list(*args, **kwargs):
54 return [self.instance]
55
52 def floating_get(*args, **kwargs):56 def floating_get(*args, **kwargs):
53 return '99.99.99.99'57 return '99.99.99.99'
5458
55 self.stubs.Set(api, 'instance_get', instance_get)59 self.stubs.Set(api, 'instance_get', instance_get)
56 self.stubs.Set(api, 'fixed_ip_get_instance', instance_get)60 self.stubs.Set(api, 'instance_get_all_by_filters', instance_get_list)
57 self.stubs.Set(api, 'instance_get_floating_address', floating_get)61 self.stubs.Set(api, 'instance_get_floating_address', floating_get)
58 self.app = metadatarequesthandler.MetadataRequestHandler()62 self.app = metadatarequesthandler.MetadataRequestHandler()
5963