Merge lp:~vladimir.p/nova/admin-vm into lp:~hudson-openstack/nova/trunk

Proposed by Vladimir Popovski
Status: Work in progress
Proposed branch: lp:~vladimir.p/nova/admin-vm
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 2176 lines (+1311/-96)
20 files modified
Authors (+1/-0)
bin/nova-manage (+120/-11)
nova/api/ec2/admin.py (+16/-1)
nova/api/ec2/cloud.py (+134/-18)
nova/api/openstack/create_instance_helper.py (+15/-1)
nova/api/openstack/servers.py (+9/-1)
nova/cloudpipe/pipelib.py (+1/-0)
nova/compute/api.py (+95/-47)
nova/compute/instance_categories.py (+196/-0)
nova/compute/manager.py (+3/-1)
nova/db/api.py (+58/-2)
nova/db/sqlalchemy/api.py (+160/-10)
nova/db/sqlalchemy/migrate_repo/versions/029_add_instance_categories.py (+152/-0)
nova/db/sqlalchemy/migration.py (+1/-1)
nova/db/sqlalchemy/models.py (+19/-1)
nova/exception.py (+17/-0)
nova/flags.py (+10/-0)
nova/tests/api/openstack/test_servers.py (+4/-0)
nova/tests/test_instance_categories.py (+295/-0)
nova/virt/libvirt/firewall.py (+5/-2)
To merge this branch: bzr merge lp:~vladimir.p/nova/admin-vm
Reviewer Review Type Date Requested Status
Dan Prince (community) Needs Fixing
Brian Lamar (community) Needs Fixing
Devin Carlen (community) Needs Fixing
Josh Kearney (community) Needs Fixing
Brian Waldon (community) Needs Information
Review via email: mp+63771@code.launchpad.net

This proposal supersedes a proposal from 2011-06-07.

Description of the change

First cut of Admin-VM changes:

- added instance categories. By default, all instances have 'regular' category assigned
- each category has default image_id
- new param "category" in ec2/openstack APIs
- filter admin instances/images/volumes

To post a comment you must log in.
Revision history for this message
Devin Carlen (devcamcar) wrote : Posted in a previous version of this proposal

Hi Vladimir,

You have a number of merge conflicts that need to be resolved.

review: Needs Fixing
Revision history for this message
Vladimir Popovski (vladimir.p) wrote : Posted in a previous version of this proposal

Hi Devin,

Thanks. Working on merging trunk back to our code. Will be ready soon.

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

Since this is a relatively major feature, I would like to see the blueprint expanded to describe the implementation. In particular, I want to hear why adding a 'category' to an instance is the right path to take. Why not use the metadata container?

This could also be considered an admin-only feature that doesn't belong in the end-user apis. I'm not crazy about seeing undocumented changes to the OpenStack API.

I'm pretty excited about this feature, I just want to make sure we are doing it right :)

review: Needs Information
Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

Hi Brian,

Please take a look at short design description we put into Blueprint.

Thanks,
-Vladimir

-----Original Message-----
From: <email address hidden> [mailto:<email address hidden>] On Behalf Of
Brian Waldon
Sent: Wednesday, June 08, 2011 12:58 PM
To: <email address hidden>
Subject: Re: [Merge] lp:~vladimir.p/nova/admin-vm into lp:nova

Review: Needs Information
Since this is a relatively major feature, I would like to see the blueprint
expanded to describe the implementation. In particular, I want to hear why
adding a 'category' to an instance is the right path to take. Why not use
the metadata container?

This could also be considered an admin-only feature that doesn't belong in
the end-user apis. I'm not crazy about seeing undocumented changes to the
OpenStack API.

I'm pretty excited about this feature, I just want to make sure we are doing
it right :)
--
https://code.launchpad.net/~vladimir.p/nova/admin-vm/+merge/63771
You are the owner of lp:~vladimir.p/nova/admin-vm.

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

Sorry it took me so long to get back to you Vladimir! Hopefully we can get the ball rolling again...

First off, there are some merge conflicts that needs to be resolved.

I'm still not quite sold on the design here. I'd like some other nova-core members to take a look at the blueprint/diff and share their thoughts.

Like I said before, I don't think we should make changes to the core OSAPI. It's looking like you will have to implement the api code as an extension unless you can convince somebody to add it to the admin api. I'd be more than happy to talk to you about this off the MP if you need some guidance here.

Revision history for this message
Josh Kearney (jk0) wrote :

Looks like you still have a few merge conflicts to handle.

review: Needs Fixing
Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

Merged with latest nova revision (#1187)

lp:~vladimir.p/nova/admin-vm updated
1112. By Vladimir Popovski

Pep8 normalization

1113. By Vladimir Popovski

changed unit_test properties (otherwise was skipped)

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

Hi Vladimir, I agree with Brian that you need to implement the apis as extensions.

review: Needs Fixing
Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

ok, I'm fine with API extensions, but how about the general direction/idea to have instance categories (wanted to use name type, but it is already taken for describing config types/flavors)?

If having instance categories will not be approved by community there is no point in continuing in this direction.

Any feedback on categories?

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

> ok, I'm fine with API extensions, but how about the general direction/idea to
> have instance categories (wanted to use name type, but it is already taken for
> describing config types/flavors)?
>
> If having instance categories will not be approved by community there is no
> point in continuing in this direction.
>
> Any feedback on categories?

Personally, I feel this could be solved through the use of system-level metadata on an instance. We don't have that capability right now, but I think it would be a good fit for this. It would allow a category to be set on an instance when it needed to be, and totally ignore it when it doesn't matter. What do you think?

Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

I suppose there are 3 general aspects for categories:
- APIs for setting categories per instance (and everything related to APIs)
- having a separate table where different category-wise settings might be stored
- distinguishing if instance belongs to category or not (where to store this field)

Having a separate table for categories helps to organize things, support different types (categories) of instances and establish better relations between tables. Potentially, we could link categories table to instance_types and set up special configurations/flavors that are applied to particular type of admin instances. In this approach such table has a metadata field, but it may have sense to put it outside in separate table a-la instance_metadata.

It is definitely possible to store this category field in the instance_metadata table as a key/value pair and we were thinking about it at the beginning. But it complicates SQL queries and you can't build normal SQL relations between key/value & categories table.
It will require first to retrieve a value and only after that to perform an additional query to retrieve properties of this category ...
Another use-case - filtering of non-regular instances. Now for each DescribeInstances call we will need to look at meta-data key/value pairs and find out if it should be hidden or not.

But I guess the main question about categories - if we think that it might be one of basic instance parameters that will benefit every installation/customer. Because in general, the instance_metadata table could hold almost all fields from instances table and it will be enough to have only instance_id field there :-)

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

Here's how understand the problem you are trying to solve with this merge prop: We need the ability for a deployer to create an instance that a tenant cannot access. This instance needs to be able to access the tenant's network. Please help me understand if this isn't 100% correct.

My first thoughts when solving the problem (as I defined it) are to allow an admin to change the network associated with an instance. Therefore, any instance he creates can be plugged into any project network. Do you think this is a viable solution?

I just want to make sure there isn't a simpler solution to this problem. This merge prop adds a lot of code to solve what seems to be a rather simple problem.

Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

Actually, we are trying to do something bigger than that - we would like to
be able to spawn special instances with their special properties.
Different types of instances may perform different tasks. For example,
"virtual storage controllers / arrays" providing HA block storage to
project's tenants. Another example might be a dedicated billing or IO
statistics server per project.

If instance will spawn multiple projects ... probably, but t will require
changes on networking level and we decided to wait for completion of
multi-nic implementation for that.
In general, tenants should not be able to access those instances in the same
manner as they access regular instances, but this is a slightly different
issue. They might be able to access such instances using REST APIs for
retrieving some data.

Does it make sense?

Revision history for this message
Vish Ishaya (vishvananda) wrote :

Brian: I think if we have plugin networking + admin managed metadata we have the components that we need. I'm a fan of putting the metadata into a generalized extra_data table. Other than that a standard way of defining an admin vm would be very valuable. It seems like a "category" definition could be defined by a tenant, networks to plug into (and whether to plug into one or more users network), base image, flavor, injected user data, and some small amount of metadata. Then you could have a command that would launch an image based on a category definition.

The special scheduler would have enough information based on the metadata to make any necessary decisions.

Revision history for this message
Vish Ishaya (vishvananda) wrote :

> In general, tenants should not be able to access those instances in the same
> manner as they access regular instances, but this is a slightly different
> issue.

This can be responsible by running the instances as a different tenant

> They might be able to access such instances using REST APIs for
> retrieving some data.

A simple command for retrieving data could get data from the flavor or metadata to return to the user.

A complicated command would require a guest agent. As long as you can specify a specific (administration) network to plug into then your REST API should be able to communicate to the guest agent to run commands.

So a complicated category def would say: run the instance as this tenant, using this image id (which has a guest agent), this special metadata, with flavor x, plugged into the admin network and the network for tenant x. Alternatively, in flatdhcp you could specify to allow access from certain tenant security groups in place of arbitrary networks.

Vish

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

=== modified file 'nova/virt/libvirt/connection.py' (properties changed: -x to +x)
=== modified file 'nova/virt/libvirt/firewall.py' (properties changed: -x to +x)

Should these files have been made executable?

Also it seems you may have added an import to connection.py which is unused.

Lines 107-109, 132-134, 150-151, 171-172, 197-198, and potentially others needs docstring/sphinx argument standardization:

"""One line summary.

Optional extra information which might be multiple lines.

:param param_one: This is a description of param one.
:param param_two: This is a description of param two.
:returns: This is a description of the return value.

"""

OR

"""This is a description of this method."""

review: Needs Fixing
Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

Thanks.

Re: properties changed from -x to +x - we missed it. Seems like samba on one of dev servers was incorrectly configured. will fix it.

Re: probably a leftover from the merge with one of latest nova versions (when connection was removed). Will fix it

Re: comments standardization - fine. will fix it.

lp:~vladimir.p/nova/admin-vm updated
1114. By Vladimir Popovski

merged with 1221

1115. By Vladimir Popovski

merged with 1222

1116. By Vladimir Popovski

replaced my fix for nova-api with community's

Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

- fixed all conflicts related to recent merges
- comments in nova-manage are complied with other comments in this exec module

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

Would you mind calling this something other than 'category'? To me, a category is a grouping of similar things. I think what we are going for here is more of a 'class': a predefined configuration of an instance. Does that seem reasonable?

Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

I suppose the best term for it is "type", but it is taken by
flavor/instance_type which actually refers to instance configuration type.

-----Original Message-----
From: <email address hidden> [mailto:<email address hidden>] On Behalf Of
Brian Waldon
Sent: Wednesday, June 29, 2011 7:17 AM
To: <email address hidden>
Subject: Re: [Merge] lp:~vladimir.p/nova/admin-vm into lp:nova

Would you mind calling this something other than 'category'? To me, a
category is a grouping of similar things. I think what we are going for here
is more of a 'class': a predefined configuration of an instance. Does that
seem reasonable?
--
https://code.launchpad.net/~vladimir.p/nova/admin-vm/+merge/63771
You are the owner of lp:~vladimir.p/nova/admin-vm.

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

The term 'class' may not be the best option, either, as it is a well-defined keyword in python.

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

What about 'InstancePlan' or 'InstanceConfig?

Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

Definitely not InstanceConfig :-)

I suppose the main purpose is to distinguish between regular compute
instances and administrative instances (instances created for other needs):
statistics/billing VMs, some special DBs, instances for hosting Virtual
Storage arrays/controllers, even VPN instances, etc. Most likely such
instances will have different configuration description / templates, but it
is probably a different question.

I'm open to any suggestions, but for me InstanceType or InstanceCategory are
winners... meanwhile :-)

Revision history for this message
Dan Prince (dan-prince) wrote :

Hi Vladimir,

A couple more merge conflicts for this one:

Text conflict in bin/nova-manage
Text conflict in nova/api/openstack/create_instance_helper.py
Text conflict in nova/compute/api.py
3 conflicts encountered.

review: Needs Fixing
Revision history for this message
Vladimir Popovski (vladimir.p) wrote :

I'm missing it. The Launchpad doesn't show any conflict.

Anyway, I suppose there might be conflicts related to added new parameters
or changed prints, but how about general feedback about direction?

Thanks.

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

> I'm missing it. The Launchpad doesn't show any conflict.

Launchpad isn't great at showing conflicts that develop after a merge prop is submitted. On your local machine, try to merge trunk into your branch. That will uncover what Dan pointed out.

Unmerged revisions

1116. By Vladimir Popovski

replaced my fix for nova-api with community's

1115. By Vladimir Popovski

merged with 1222

1114. By Vladimir Popovski

merged with 1221

1113. By Vladimir Popovski

changed unit_test properties (otherwise was skipped)

1112. By Vladimir Popovski

Pep8 normalization

1111. By Vladimir Popovski

Admin-VM: merged with 1187

1110. By Vladimir Popovski

Admin-VM: merged with 1186

1109. By Vladimir Popovski

pep8 code normalization

1108. By Vladimir Popovski

Admin-VM: removed nova.conf (accidentally added)

1107. By Vladimir Popovski

Admin-VM: merged with nova trunk-1159

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Authors'
2--- Authors 2011-06-28 00:23:19 +0000
3+++ Authors 2011-06-28 22:54:37 +0000
4@@ -88,6 +88,7 @@
5 Vasiliy Shlykov <vash@vasiliyshlykov.org>
6 Vishvananda Ishaya <vishvananda@gmail.com>
7 Vivek Y S <vivek.ys@gmail.com>
8+Vladimir Popovski <vladimir@zadarastorage.com>
9 William Wolf <throughnothing@gmail.com>
10 Yoshiaki Tamura <yoshi@midokura.jp>
11 Youcef Laribi <Youcef.Laribi@eu.citrix.com>
12
13=== modified file 'bin/nova-manage'
14--- bin/nova-manage 2011-06-25 19:38:07 +0000
15+++ bin/nova-manage 2011-06-28 22:54:37 +0000
16@@ -87,6 +87,7 @@
17 from nova.auth import manager
18 from nova.cloudpipe import pipelib
19 from nova.compute import instance_types
20+from nova.compute import instance_categories
21 from nova.db import migration
22
23 FLAGS = flags.FLAGS
24@@ -140,7 +141,9 @@
25 ipport = "%s:%s" % (project.vpn_ip, project.vpn_port)
26 print "%-20s\t" % ipport,
27 ctxt = context.get_admin_context()
28- vpn = db.instance_get_project_vpn(ctxt, project.id)
29+ cat_id = instance_categories.get_instance_cat_id_by_name(
30+ FLAGS.instance_cat_vpn)
31+ vpn = db.instance_get_project_admin(ctxt, project.id, cat_id)
32 if vpn:
33 address = None
34 state = 'down'
35@@ -611,14 +614,14 @@
36 class VmCommands(object):
37 """Class for mangaging VM instances."""
38
39- def list(self, host=None):
40+ def list(self, host=None, cat_id=None):
41 """Show a list of all instances
42
43 :param host: show all instance on specified host.
44 :param instance: show specificed instance.
45 """
46 print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
47- " %-10s %-10s %-10s %-5s" % (
48+ " %-10s %-10s %-10s %-5s %-8s" % (
49 _('instance'),
50 _('node'),
51 _('type'),
52@@ -630,20 +633,23 @@
53 _('project'),
54 _('user'),
55 _('zone'),
56- _('index'))
57+ _('index'),
58+ _('category'))
59
60- if host is None:
61- instances = db.instance_get_all(context.get_admin_context())
62+ ctxt = context.get_admin_context()
63+ if cat_id is not None:
64+ instances = db.instance_get_all_by_cat(ctxt, cat_id)
65+ elif host is None:
66+ instances = db.instance_get_all(ctxt)
67 else:
68- instances = db.instance_get_all_by_host(
69- context.get_admin_context(), host)
70+ instances = db.instance_get_all_by_host(ctxt, host)
71
72 for instance in instances:
73 print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
74- " %-10s %-10s %-10s %-5d" % (
75+ " %-10s %-10s %-10s %-5d %-8s" % (
76 instance['hostname'],
77 instance['host'],
78- instance['instance_type'],
79+ instance['instance_type']['name'],
80 instance['state_description'],
81 instance['launched_at'],
82 instance['image_id'],
83@@ -652,7 +658,8 @@
84 instance['project_id'],
85 instance['user_id'],
86 instance['availability_zone'],
87- instance['launch_index'])
88+ instance['launch_index'],
89+ instance['instance_cat']['name'])
90
91 def live_migration(self, ec2_id, dest):
92 """Migrates a running instance to a new machine.
93@@ -937,6 +944,98 @@
94 self._print_instance_types(name, inst_types)
95
96
97+class InstanceCatCommands(object):
98+ """Class for managing instance categories."""
99+
100+ def _print_instance_categories(self, name, val):
101+ deleted = ('', ', inactive')[val["deleted"] == 1]
102+ print ("%-16s: id: %2d, Desc: %-30s, image_id %-10s, Specs: %s%s") % \
103+ (name, val["id"], val["description"], val["image_id"],
104+ val["specs"], deleted)
105+
106+ def create(self, name, description, image_id=None, specs=None):
107+ """Creates instance category
108+ arguments: name description [image_id] [specs]
109+ """
110+ try:
111+ instance_categories.create(name, description, image_id, specs)
112+ except exception.InvalidInputException:
113+ print "Must supply valid parameters to create instance_category"
114+ print e
115+ sys.exit(1)
116+ except exception.ApiError, e:
117+ print "\n\n"
118+ print "\n%s" % e
119+ print "Please ensure category name is unique."
120+ print "To complete remove a category, use the --purge flag:"
121+ print "\n # nova-manage instance_category delete <name> --purge"
122+ print "\nCurrently defined category names:"
123+ self.list("--all")
124+ sys.exit(2)
125+ except:
126+ print "Unknown error"
127+ sys.exit(3)
128+ else:
129+ print "Category %s created" % name
130+
131+ def update(self, name, description=None, image_id=None, specs=None):
132+ """Updates instance category
133+ arguments: name [description] [image_id] [specs]
134+ """
135+ try:
136+ instance_categories.update(name, description, image_id, specs)
137+ except exception.ApiError:
138+ print "Valid instance category name is required"
139+ sys.exit(1)
140+ except exception.DBError, e:
141+ print "DB Error: %s" % e
142+ sys.exit(2)
143+ except:
144+ print "Unknown error"
145+ sys.exit(3)
146+ else:
147+ print "Category %s updated" % (name)
148+
149+ def delete(self, name, purge=None):
150+ """Marks instance category as deleted
151+ arguments: name"""
152+ try:
153+ if purge == "--purge":
154+ instance_categories.purge(name)
155+ verb = "purged"
156+ else:
157+ instance_categories.destroy(name)
158+ verb = "deleted"
159+ except exception.ApiError:
160+ print "Valid instance category name is required"
161+ sys.exit(1)
162+ except exception.DBError, e:
163+ print "DB Error: %s" % e
164+ sys.exit(2)
165+ except:
166+ sys.exit(3)
167+ else:
168+ print "Category %s %s" % (name, verb)
169+
170+ def list(self, name=None):
171+ """Lists all active or specific instance categories
172+ arguments: [name]"""
173+ try:
174+ if name is None:
175+ inst_cats = instance_categories.get_all_categories()
176+ elif name == "--all":
177+ inst_cats = instance_categories.get_all_categories(True)
178+ else:
179+ inst_cats = instance_categories.get_instance_cat_by_name(name)
180+ except exception.DBError, e:
181+ _db_error(e)
182+ if isinstance(inst_cats.values()[0], dict):
183+ for k, v in inst_cats.iteritems():
184+ self._print_instance_categories(k, v)
185+ else:
186+ self._print_instance_categories(name, inst_cats)
187+
188+
189 class ImageCommands(object):
190 """Methods for dealing with a cloud in an odd state"""
191
192@@ -1075,6 +1174,15 @@
193 self._convert_images(other_images)
194 self._convert_images(machine_images)
195
196+ def show_admin_images(self, category=None):
197+ """Show registered admin images [per category]
198+ arguments: [category]"""
199+
200+ if category == "--all":
201+ category = None
202+ image_names = instance_categories.get_admin_image_names(category)
203+ print image_names
204+
205
206 class AgentBuildCommands(object):
207 """Class for managing agent builds."""
208@@ -1158,6 +1266,7 @@
209 ('fixed', FixedIpCommands),
210 ('flavor', InstanceTypeCommands),
211 ('floating', FloatingIpCommands),
212+ ('instance_category', InstanceCatCommands),
213 ('instance_type', InstanceTypeCommands),
214 ('image', ImageCommands),
215 ('network', NetworkCommands),
216
217=== modified file 'nova/api/ec2/admin.py'
218--- nova/api/ec2/admin.py 2011-06-25 19:38:07 +0000
219+++ nova/api/ec2/admin.py 2011-06-28 22:54:37 +0000
220@@ -33,6 +33,7 @@
221 from nova import utils
222 from nova.api.ec2 import ec2utils
223 from nova.auth import manager
224+from nova.compute import instance_categories
225
226
227 FLAGS = flags.FLAGS
228@@ -91,6 +92,13 @@
229 'flavor_id': inst['flavorid']}
230
231
232+def instance_cat_dict(inst):
233+ return {'name': inst['name'],
234+ 'description': inst['description'],
235+ 'image_id': inst['image_id'],
236+ 'specs': inst['specs']}
237+
238+
239 def vpn_dict(project, vpn_instance):
240 rv = {'project_id': project.id,
241 'public_ip': project.vpn_ip,
242@@ -129,6 +137,11 @@
243 return {'instanceTypeSet': [instance_dict(v) for v in
244 db.instance_type_get_all(context).values()]}
245
246+ def describe_instance_categories(self, context, **_kwargs):
247+ """Returns all active instance categories data"""
248+ return {'instanceCatSet': [instance_cat_dict(v) for v in
249+ db.instance_cat_get_all(context).values()]}
250+
251 def describe_user(self, _context, name, **_kwargs):
252 """Returns user data, including access and secret keys."""
253 return user_dict(manager.AuthManager().get_user(name))
254@@ -272,7 +285,9 @@
255 def _vpn_for(self, context, project_id):
256 """Get the VPN instance for a project ID."""
257 for instance in db.instance_get_all_by_project(context, project_id):
258- if (instance['image_id'] == str(FLAGS.vpn_image_id)
259+ if (instance_categories.is_admin_instance(
260+ instance,
261+ FLAGS.instance_cat_vpn) \
262 and not instance['state_description'] in
263 ['shutting_down', 'shutdown']):
264 return instance
265
266=== modified file 'nova/api/ec2/cloud.py'
267--- nova/api/ec2/cloud.py 2011-06-25 19:38:07 +0000
268+++ nova/api/ec2/cloud.py 2011-06-28 22:54:37 +0000
269@@ -44,6 +44,7 @@
270 from nova import volume
271 from nova.api.ec2 import ec2utils
272 from nova.compute import instance_types
273+from nova.compute import instance_categories
274 from nova.image import s3
275
276
277@@ -620,6 +621,40 @@
278 return self.compute_api.get_vnc_console(context,
279 instance_id=instance_id)
280
281+ def _filter_volumes(self, context, volumes, category):
282+ result = []
283+ for volume in volumes:
284+ add_image = False
285+ if volume.get('instance', None):
286+ instance_id = volume['instance']['id']
287+ instance = db.instance_get(context, instance_id)
288+ is_admin_inst = \
289+ instance_categories.is_admin_instance(instance, category)
290+ else:
291+ is_admin_inst = False
292+
293+ if category is None:
294+ if context.is_admin \
295+ or not is_admin_inst:
296+ add_image = True
297+ else:
298+ if is_admin_inst:
299+ add_image = True
300+
301+ if add_image:
302+ LOG.debug(("filter_volumes: +++ add volume %d, "
303+ "is_admin_inst %s, cat %s, is_admin=%s"),
304+ volume['id'], str(is_admin_inst), category,
305+ str(context.is_admin))
306+ result.append(volume)
307+ else:
308+ LOG.debug(("filter_volumes: --- skip volume %d, "
309+ "is_admin_inst %s, cat %s, is_admin=%s"),
310+ volume['id'], str(is_admin_inst), category,
311+ str(context.is_admin))
312+
313+ return result
314+
315 def describe_volumes(self, context, volume_id=None, **kwargs):
316 if volume_id:
317 volumes = []
318@@ -629,7 +664,13 @@
319 volumes.append(volume)
320 else:
321 volumes = self.volume_api.get_all(context)
322- volumes = [self._format_volume(context, v) for v in volumes]
323+
324+ category = self._get_category_name(**kwargs)
325+ filtered_volumes = self._filter_volumes(context,
326+ volumes,
327+ category=category)
328+
329+ volumes = [self._format_volume(context, v) for v in filtered_volumes]
330 return {'volumeSet': volumes}
331
332 def _format_volume(self, context, volume):
333@@ -748,6 +789,16 @@
334 lst = [lst]
335 return [{label: x} for x in lst]
336
337+ def _get_category_name(self, **kwargs):
338+ return kwargs.get('category', None)
339+
340+ def _get_category(self, **kwargs):
341+ cat_name = self._get_category_name(**kwargs)
342+ if cat_name is None:
343+ return instance_categories.get_default_instance_cat()
344+ else:
345+ return instance_categories.get_instance_cat_by_name(cat_name)
346+
347 def describe_instances(self, context, **kwargs):
348 return self._format_describe_instances(context, **kwargs)
349
350@@ -758,8 +809,10 @@
351 def _format_describe_instances(self, context, **kwargs):
352 return {'reservationSet': self._format_instances(context, **kwargs)}
353
354- def _format_run_instances(self, context, reservation_id):
355- i = self._format_instances(context, reservation_id=reservation_id)
356+ def _format_run_instances(self, context, reservation_id, category):
357+ i = self._format_instances(context,
358+ reservation_id=reservation_id,
359+ category=category)
360 assert len(i) == 1
361 return i[0]
362
363@@ -772,16 +825,18 @@
364 # NOTE(vish): instance_id is an optional list of ids to filter by
365 if instance_id:
366 instances = []
367+ instance_cat = self._get_category(**kwargs)
368 for ec2_id in instance_id:
369 internal_id = ec2utils.ec2_id_to_id(ec2_id)
370 instance = self.compute_api.get(context,
371- instance_id=internal_id)
372+ instance_id=internal_id,
373+ instance_cat=instance_cat)
374 instances.append(instance)
375 else:
376 instances = self.compute_api.get_all(context, **kwargs)
377 for instance in instances:
378 if not context.is_admin:
379- if instance['image_ref'] == str(FLAGS.vpn_image_id):
380+ if instance_categories.is_admin_instance(instance):
381 continue
382 i = {}
383 instance_id = instance['id']
384@@ -820,6 +875,10 @@
385 i['instanceType'] = instance['instance_type'].get('name')
386 else:
387 i['instanceType'] = None
388+ if instance['instance_cat']:
389+ i['category'] = instance['instance_cat'].get('name')
390+ else:
391+ i['category'] = None
392 i['launchTime'] = instance['created_at']
393 i['amiLaunchIndex'] = instance['launch_index']
394 i['displayName'] = instance['display_name']
395@@ -938,6 +997,12 @@
396 if image_state != 'available':
397 raise exception.ApiError(_('Image must be available'))
398
399+ category = self._get_category_name(**kwargs)
400+ instance_cat = self._get_category(**kwargs)
401+ if instance_categories.is_admin_category(instance_cat) \
402+ and not context.is_admin:
403+ raise exception.AdminRequired()
404+
405 instances = self.compute_api.create(context,
406 instance_type=instance_types.get_instance_type_by_name(
407 kwargs.get('instance_type', None)),
408@@ -953,53 +1018,67 @@
409 security_group=kwargs.get('security_group'),
410 availability_zone=kwargs.get('placement', {}).get(
411 'AvailabilityZone'),
412- block_device_mapping=kwargs.get('block_device_mapping', {}))
413+ block_device_mapping=kwargs.get('block_device_mapping', {}),
414+ instance_cat=instance_cat)
415 return self._format_run_instances(context,
416- instances[0]['reservation_id'])
417+ instances[0]['reservation_id'],
418+ category=category)
419
420- def _do_instance(self, action, context, ec2_id):
421+ def _do_instance(self, action, context, ec2_id, instance_cat=None):
422 instance_id = ec2utils.ec2_id_to_id(ec2_id)
423- action(context, instance_id=instance_id)
424+ action(context, instance_id=instance_id, instance_cat=instance_cat)
425
426- def _do_instances(self, action, context, instance_id):
427+ def _do_instances(self, action, context, instance_id, instance_cat=None):
428 for ec2_id in instance_id:
429- self._do_instance(action, context, ec2_id)
430+ self._do_instance(action, context, ec2_id, instance_cat)
431
432 def terminate_instances(self, context, instance_id, **kwargs):
433 """Terminate each instance in instance_id, which is a list of ec2 ids.
434 instance_id is a kwarg so its name cannot be modified."""
435 LOG.debug(_("Going to start terminating instances"))
436- self._do_instances(self.compute_api.delete, context, instance_id)
437+ instance_cat = self._get_category(**kwargs)
438+ self._do_instances(self.compute_api.delete, context, instance_id,
439+ instance_cat)
440 return True
441
442 def reboot_instances(self, context, instance_id, **kwargs):
443 """instance_id is a list of instance ids"""
444 LOG.audit(_("Reboot instance %r"), instance_id, context=context)
445- self._do_instances(self.compute_api.reboot, context, instance_id)
446+ instance_cat = self._get_category(**kwargs)
447+ self._do_instances(self.compute_api.reboot, context, instance_id,
448+ instance_cat)
449 return True
450
451 def stop_instances(self, context, instance_id, **kwargs):
452 """Stop each instances in instance_id.
453 Here instance_id is a list of instance ids"""
454 LOG.debug(_("Going to stop instances"))
455- self._do_instances(self.compute_api.stop, context, instance_id)
456+ instance_cat = self._get_category(**kwargs)
457+ self._do_instances(self.compute_api.stop, context, instance_id,
458+ instance_cat)
459 return True
460
461 def start_instances(self, context, instance_id, **kwargs):
462 """Start each instances in instance_id.
463 Here instance_id is a list of instance ids"""
464 LOG.debug(_("Going to start instances"))
465- self._do_instances(self.compute_api.start, context, instance_id)
466+ instance_cat = self._get_category(**kwargs)
467+ self._do_instances(self.compute_api.start, context, instance_id,
468+ instance_cat)
469 return True
470
471 def rescue_instance(self, context, instance_id, **kwargs):
472 """This is an extension to the normal ec2_api"""
473- self._do_instance(self.compute_api.rescue, contect, instnace_id)
474+ instance_cat = self._get_category(**kwargs)
475+ self._do_instance(self.compute_api.rescue, context, instance_id,
476+ instance_cat)
477 return True
478
479 def unrescue_instance(self, context, instance_id, **kwargs):
480 """This is an extension to the normal ec2_api"""
481- self._do_instance(self.compute_api.unrescue, context, instance_id)
482+ instance_cat = self._get_category(**kwargs)
483+ self._do_instance(self.compute_api.unrescue, context, instance_id,
484+ instance_cat)
485 return True
486
487 def update_instance(self, context, instance_id, **kwargs):
488@@ -1083,6 +1162,38 @@
489 i['architecture'] = image['properties'].get('architecture')
490 return i
491
492+ def _filter_images(self, context, images, category):
493+ result = []
494+ admin_image_names = instance_categories.get_admin_image_names(category)
495+ for i in admin_image_names:
496+ LOG.debug(_("admin_image_names: %s"), i)
497+
498+ for image in images:
499+ name = str(image.get('id'))
500+ add_name = False
501+ if category is None:
502+ # if no category set - filter all admin images
503+ #if name not in admin_image_names:
504+ if context.is_admin \
505+ or name not in admin_image_names:
506+ add_name = True
507+ else:
508+ # if category is set - return images for this category only
509+ if name in admin_image_names:
510+ add_name = True
511+
512+ if add_name:
513+ LOG.debug(("filter_images: +++ add name %s, "
514+ "category %s, is_admin=%s"),
515+ name, category, str(context.is_admin))
516+ result.append(image)
517+ else:
518+ LOG.debug(("filter_images: --- skip name %s, "
519+ "category %s, is_admin=%s"),
520+ name, category, str(context.is_admin))
521+
522+ return result
523+
524 def describe_images(self, context, image_id=None, **kwargs):
525 # NOTE: image_id is a list!
526 if image_id:
527@@ -1095,7 +1206,12 @@
528 images.append(image)
529 else:
530 images = self.image_service.detail(context)
531- images = [self._format_image(i) for i in images]
532+
533+ category = self._get_category_name(**kwargs)
534+ filtered_images = self._filter_images(context,
535+ images, category=category)
536+
537+ images = [self._format_image(i) for i in filtered_images]
538 return {'imagesSet': images}
539
540 def deregister_image(self, context, image_id, **kwargs):
541
542=== modified file 'nova/api/openstack/create_instance_helper.py'
543--- nova/api/openstack/create_instance_helper.py 2011-06-17 17:17:14 +0000
544+++ nova/api/openstack/create_instance_helper.py 2011-06-28 22:54:37 +0000
545@@ -28,6 +28,7 @@
546 from nova import utils
547
548 from nova.compute import instance_types
549+from nova.compute import instance_categories
550 from nova.api.openstack import faults
551 from nova.api.openstack import wsgi
552 from nova.auth import manager as auth_manager
553@@ -104,6 +105,17 @@
554
555 flavor_id = self.controller._flavor_id_from_req_data(body)
556
557+ if 'category' in body['server']:
558+ category = self.controller._instance_cat_from_req_data(body)
559+ try:
560+ instance_cat = \
561+ instance_categories.get_instance_cat_by_name(category)
562+ except exception.NoFound:
563+ raise exc.HTTPBadRequest(_("Category %(category)s not found"))
564+ else:
565+ instance_cat = None
566+ category = instance_categories.default_category_name()
567+
568 if not 'name' in body['server']:
569 msg = _("Server name is not defined")
570 raise exc.HTTPBadRequest(explanation=msg)
571@@ -120,6 +132,7 @@
572 instance_types.get_instance_type_by_flavor_id(flavor_id)
573 extra_values = {
574 'instance_type': inst_type,
575+ 'category': category,
576 'image_ref': image_href,
577 'password': password}
578
579@@ -137,7 +150,8 @@
580 injected_files=injected_files,
581 admin_password=password,
582 zone_blob=zone_blob,
583- reservation_id=reservation_id))
584+ reservation_id=reservation_id,
585+ instance_cat=instance_cat))
586 except quota.QuotaError as error:
587 self._handle_quota_error(error)
588 except exception.ImageNotFound as error:
589
590=== modified file 'nova/api/openstack/servers.py'
591--- nova/api/openstack/servers.py 2011-06-15 20:58:55 +0000
592+++ nova/api/openstack/servers.py 2011-06-28 22:54:37 +0000
593@@ -124,6 +124,7 @@
594
595 builder = self._get_view_builder(req)
596 server = builder.build(inst, is_detail=True)
597+ server['server']['category'] = extra_values['category']
598 server['server']['adminPass'] = extra_values['password']
599 return server
600
601@@ -410,6 +411,9 @@
602 def _flavor_id_from_req_data(self, data):
603 return data['server']['flavorId']
604
605+ def _instance_cat_from_req_data(self, data):
606+ return data['server']['category']
607+
608 def _get_view_builder(self, req):
609 addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10()
610 return nova.api.openstack.views.servers.ViewBuilderV10(
611@@ -473,6 +477,10 @@
612 href = data['server']['flavorRef']
613 return common.get_id_from_href(href)
614
615+ def _instance_cat_from_req_data(self, data):
616+ href = data['server']['category']
617+ return common.get_id_from_href(href)
618+
619 def _get_view_builder(self, req):
620 base_url = req.application_url
621 flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
622@@ -591,7 +599,7 @@
623 "attributes": {
624 "server": ["id", "imageId", "name", "flavorId", "hostId",
625 "status", "progress", "adminPass", "flavorRef",
626- "imageRef"],
627+ "imageRef", "category"],
628 "link": ["rel", "type", "href"],
629 },
630 "dict_collections": {
631
632=== modified file 'nova/cloudpipe/pipelib.py'
633--- nova/cloudpipe/pipelib.py 2011-04-19 16:32:33 +0000
634+++ nova/cloudpipe/pipelib.py 2011-06-28 22:54:37 +0000
635@@ -107,6 +107,7 @@
636 max_count=1,
637 min_count=1,
638 instance_type='m1.tiny',
639+ instance_cat=FLAGS.instance_cat_vpn,
640 image_id=ec2_id,
641 key_name=key_name,
642 security_group=[group_name])
643
644=== modified file 'nova/compute/api.py'
645--- nova/compute/api.py 2011-06-24 12:07:33 +0000
646+++ nova/compute/api.py 2011-06-28 22:54:37 +0000
647@@ -33,6 +33,7 @@
648 from nova import utils
649 from nova import volume
650 from nova.compute import instance_types
651+from nova.compute import instance_categories
652 from nova.compute import power_state
653 from nova.compute.utils import terminate_volumes
654 from nova.scheduler import api as scheduler_api
655@@ -148,13 +149,17 @@
656 key_name=None, key_data=None, security_group='default',
657 availability_zone=None, user_data=None, metadata={},
658 injected_files=None, admin_password=None, zone_blob=None,
659- reservation_id=None):
660+ reservation_id=None,
661+ instance_cat=None):
662 """Verify all the input parameters regardless of the provisioning
663 strategy being performed."""
664
665 if not instance_type:
666 instance_type = instance_types.get_default_instance_type()
667
668+ if not instance_cat:
669+ instance_cat = instance_categories.get_default_instance_cat()
670+
671 num_instances = quota.allowed_instances(context, max_count,
672 instance_type)
673 if num_instances < min_count:
674@@ -234,6 +239,7 @@
675 'project_id': context.project_id,
676 'launch_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
677 'instance_type_id': instance_type['id'],
678+ 'instance_cat_id': instance_cat['id'],
679 'memory_mb': instance_type['memory_mb'],
680 'vcpus': instance_type['vcpus'],
681 'local_gb': instance_type['local_gb'],
682@@ -303,7 +309,8 @@
683 return instance
684
685 def _ask_scheduler_to_create_instance(self, context, base_options,
686- instance_type, zone_blob,
687+ instance_type, instance_cat,
688+ zone_blob,
689 availability_zone, injected_files,
690 admin_password,
691 instance_id=None, num_instances=1):
692@@ -317,10 +324,14 @@
693 LOG.debug(_("Casting to scheduler for %(pid)s/%(uid)s's"
694 " (all-at-once)") % locals())
695
696+ if not instance_cat:
697+ instance_cat = instance_categories.get_default_instance_cat()
698+
699 filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter'
700 request_spec = {
701 'instance_properties': base_options,
702 'instance_type': instance_type,
703+ 'category': instance_cat['name'],
704 'filter': filter_class,
705 'blob': zone_blob,
706 'num_instances': num_instances,
707@@ -343,7 +354,8 @@
708 key_name=None, key_data=None, security_group='default',
709 availability_zone=None, user_data=None, metadata={},
710 injected_files=None, admin_password=None, zone_blob=None,
711- reservation_id=None):
712+ reservation_id=None,
713+ instance_cat=None):
714 """Provision the instances by passing the whole request to
715 the Scheduler for execution. Returns a Reservation ID
716 related to the creation of all of these instances."""
717@@ -356,10 +368,10 @@
718 key_name, key_data, security_group,
719 availability_zone, user_data, metadata,
720 injected_files, admin_password, zone_blob,
721- reservation_id)
722+ reservation_id, instance_cat)
723
724 self._ask_scheduler_to_create_instance(context, base_options,
725- instance_type, zone_blob,
726+ instance_type, instance_cat, zone_blob,
727 availability_zone, injected_files,
728 admin_password,
729 num_instances=num_instances)
730@@ -373,7 +385,8 @@
731 key_name=None, key_data=None, security_group='default',
732 availability_zone=None, user_data=None, metadata={},
733 injected_files=None, admin_password=None, zone_blob=None,
734- reservation_id=None, block_device_mapping=None):
735+ reservation_id=None, block_device_mapping=None,
736+ instance_cat=None):
737 """
738 Provision the instances by sending off a series of single
739 instance requests to the Schedulers. This is fine for trival
740@@ -392,7 +405,7 @@
741 key_name, key_data, security_group,
742 availability_zone, user_data, metadata,
743 injected_files, admin_password, zone_blob,
744- reservation_id)
745+ reservation_id, instance_cat)
746
747 block_device_mapping = block_device_mapping or []
748 instances = []
749@@ -405,7 +418,8 @@
750 instance_id = instance['id']
751
752 self._ask_scheduler_to_create_instance(context, base_options,
753- instance_type, zone_blob,
754+ instance_type, instance_cat,
755+ zone_blob,
756 availability_zone, injected_files,
757 admin_password,
758 instance_id=instance_id)
759@@ -520,9 +534,9 @@
760 rv = self.db.instance_update(context, instance_id, kwargs)
761 return dict(rv.iteritems())
762
763- def _get_instance(self, context, instance_id, action_str):
764+ def _get_instance(self, context, instance_id, instance_cat, action_str):
765 try:
766- return self.get(context, instance_id)
767+ return self.get(context, instance_id, instance_cat)
768 except exception.NotFound:
769 LOG.warning(_("Instance %(instance_id)s was not found during "
770 "%(action_str)s") %
771@@ -530,10 +544,11 @@
772 raise
773
774 @scheduler_api.reroute_compute("delete")
775- def delete(self, context, instance_id):
776+ def delete(self, context, instance_id, instance_cat=None):
777 """Terminate an instance."""
778 LOG.debug(_("Going to try to terminate %s"), instance_id)
779- instance = self._get_instance(context, instance_id, 'terminating')
780+ instance = self._get_instance(context, instance_id, instance_cat,
781+ 'terminating')
782
783 if not _is_able_to_shutdown(instance, instance_id):
784 return
785@@ -553,11 +568,12 @@
786 self.db.instance_destroy(context, instance_id)
787
788 @scheduler_api.reroute_compute("stop")
789- def stop(self, context, instance_id):
790+ def stop(self, context, instance_id, instance_cat=None):
791 """Stop an instance."""
792 LOG.debug(_("Going to try to stop %s"), instance_id)
793
794- instance = self._get_instance(context, instance_id, 'stopping')
795+ instance = self._get_instance(context, instance_id, instance_cat,
796+ 'stopping')
797 if not _is_able_to_shutdown(instance, instance_id):
798 return
799
800@@ -572,10 +588,11 @@
801 self._cast_compute_message('stop_instance', context,
802 instance_id, host)
803
804- def start(self, context, instance_id):
805+ def start(self, context, instance_id, instance_cat=None):
806 """Start an instance."""
807 LOG.debug(_("Going to try to start %s"), instance_id)
808- instance = self._get_instance(context, instance_id, 'starting')
809+ instance = self._get_instance(context, instance_id, instance_cat,
810+ 'starting')
811 if instance['state_description'] != 'stopped':
812 _state_description = instance['state_description']
813 LOG.warning(_("Instance %(instance_id)s is not "
814@@ -591,7 +608,7 @@
815 "args": {"topic": FLAGS.compute_topic,
816 "instance_id": instance_id}})
817
818- def get(self, context, instance_id):
819+ def get(self, context, instance_id, instance_cat=None):
820 """Get a single instance with the given instance_id."""
821 # NOTE(sirp): id used to be exclusively integer IDs; now we're
822 # accepting both UUIDs and integer IDs. The handling of this
823@@ -601,17 +618,22 @@
824 instance = self.db.instance_get_by_uuid(context, uuid)
825 else:
826 instance = self.db.instance_get(context, instance_id)
827- return dict(instance.iteritems())
828+ instance = dict(instance.iteritems())
829+ if instance_cat is not None:
830+ if not instance_categories.is_access_allowed(context, instance,
831+ instance_cat):
832+ raise exception.AdminRequired()
833+ return instance
834
835 @scheduler_api.reroute_compute("get")
836- def routing_get(self, context, instance_id):
837+ def routing_get(self, context, instance_id, instance_cat=None):
838 """A version of get with special routing characteristics.
839
840 Use this method instead of get() if this is the only operation you
841 intend to to. It will route to novaclient.get if the instance is not
842 found.
843 """
844- return self.get(context, instance_id)
845+ return self.get(context, instance_id, instance_cat)
846
847 def get_all_across_zones(self, context, reservation_id):
848 """Get all instances with this reservation_id, across
849@@ -633,33 +655,47 @@
850 return instances
851
852 def get_all(self, context, project_id=None, reservation_id=None,
853- fixed_ip=None):
854+ fixed_ip=None, category=None):
855 """Get all instances filtered by one of the given parameters.
856
857 If there is no filter and the context is an admin, it will retreive
858 all instances in the system.
859 """
860+ instance_cat = instance_categories.get_instance_cat_by_name(category)
861+
862 if reservation_id is not None:
863- return self.get_all_across_zones(context, reservation_id)
864-
865- if fixed_ip is not None:
866- return self.db.fixed_ip_get_instance(context, fixed_ip)
867-
868- if project_id or not context.is_admin:
869+ instances = self.get_all_across_zones(context, reservation_id)
870+
871+ elif fixed_ip is not None:
872+ instances = \
873+ self.db.fixed_ip_get_instance(context, fixed_ip)
874+
875+ elif project_id or not context.is_admin:
876 if not context.project:
877- return self.db.instance_get_all_by_user(
878- context, context.user_id)
879-
880- if project_id is None:
881- project_id = context.project_id
882-
883- return self.db.instance_get_all_by_project(
884- context, project_id)
885-
886- return self.db.instance_get_all(context)
887+ instances = self.db.instance_get_all_by_user(
888+ context, context.user_id)
889+ else:
890+ if project_id is None:
891+ project_id = context.project_id
892+
893+ instances = self.db.instance_get_all_by_project(
894+ context, project_id)
895+
896+ elif category is not None:
897+ instances = self.db.instance_get_all_by_cat(context,
898+ instance_cat['id'])
899+ else:
900+ instances = self.db.instance_get_all(context)
901+
902+ return self._filter_instances(context, instances, instance_cat)
903+
904+ def _filter_instances(self, context, instances, instance_cat):
905+ return [instance for instance in instances \
906+ if instance_categories.is_access_allowed(context, instance,
907+ instance_cat)]
908
909 def _cast_compute_message(self, method, context, instance_id, host=None,
910- params=None):
911+ params=None, instance_cat=None):
912 """Generic handler for RPC casts to compute.
913
914 :param params: Optional dictionary of arguments to be passed to the
915@@ -667,18 +703,23 @@
916
917 :returns: None
918 """
919+ instance = self.get(context, instance_id)
920+ if not instance_categories.is_access_allowed(context, instance,
921+ instance_cat):
922+ raise exception.AdminRequired()
923+
924 if not params:
925 params = {}
926 if not host:
927- instance = self.get(context, instance_id)
928 host = instance['host']
929+
930 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
931 params['instance_id'] = instance_id
932 kwargs = {'method': method, 'args': params}
933 rpc.cast(context, queue, kwargs)
934
935 def _call_compute_message(self, method, context, instance_id, host=None,
936- params=None):
937+ params=None, instance_cat=None):
938 """Generic handler for RPC calls to compute.
939
940 :param params: Optional dictionary of arguments to be passed to the
941@@ -686,10 +727,14 @@
942
943 :returns: Result returned by compute worker
944 """
945+ instance = self.get(context, instance_id)
946+ if not instance_categories.is_access_allowed(context, instance,
947+ instance_cat):
948+ raise exception.AdminRequired()
949+
950 if not params:
951 params = {}
952 if not host:
953- instance = self.get(context, instance_id)
954 host = instance["host"]
955 queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
956 params['instance_id'] = instance_id
957@@ -728,9 +773,10 @@
958 params=params)
959 return recv_meta
960
961- def reboot(self, context, instance_id):
962+ def reboot(self, context, instance_id, instance_cat=None):
963 """Reboot the given instance."""
964- self._cast_compute_message('reboot_instance', context, instance_id)
965+ self._cast_compute_message('reboot_instance', context, instance_id,
966+ instance_cat=instance_cat)
967
968 def rebuild(self, context, instance_id, image_href, name=None,
969 metadata=None, files_to_inject=None):
970@@ -857,14 +903,16 @@
971 self._cast_compute_message('resume_instance', context, instance_id)
972
973 @scheduler_api.reroute_compute("rescue")
974- def rescue(self, context, instance_id):
975+ def rescue(self, context, instance_id, instance_cat=None):
976 """Rescue the given instance."""
977- self._cast_compute_message('rescue_instance', context, instance_id)
978+ self._cast_compute_message('rescue_instance', context, instance_id,
979+ instance_cat=instance_cat)
980
981 @scheduler_api.reroute_compute("unrescue")
982- def unrescue(self, context, instance_id):
983+ def unrescue(self, context, instance_id, instance_cat=None):
984 """Unrescue the given instance."""
985- self._cast_compute_message('unrescue_instance', context, instance_id)
986+ self._cast_compute_message('unrescue_instance', context, instance_id,
987+ instance_cat=instance_cat)
988
989 def set_admin_password(self, context, instance_id, password=None):
990 """Set the root/admin password for the given instance."""
991
992=== added file 'nova/compute/instance_categories.py'
993--- nova/compute/instance_categories.py 1970-01-01 00:00:00 +0000
994+++ nova/compute/instance_categories.py 2011-06-28 22:54:37 +0000
995@@ -0,0 +1,196 @@
996+# vim: tabstop=4 shiftwidth=4 softtabstop=4
997+
998+# Copyright 2010 United States Government as represented by the
999+# Administrator of the National Aeronautics and Space Administration.
1000+# All Rights Reserved.
1001+# Copyright (c) 2010 Citrix Systems, Inc.
1002+# Copyright 2011 Ken Pepple
1003+#
1004+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1005+# not use this file except in compliance with the License. You may obtain
1006+# a copy of the License at
1007+#
1008+# http://www.apache.org/licenses/LICENSE-2.0
1009+#
1010+# Unless required by applicable law or agreed to in writing, software
1011+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1012+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1013+# License for the specific language governing permissions and limitations
1014+# under the License.
1015+
1016+"""Built-in instance categories properties."""
1017+
1018+from nova import context
1019+from nova import db
1020+from nova import exception
1021+from nova import flags
1022+from nova import log as logging
1023+
1024+FLAGS = flags.FLAGS
1025+LOG = logging.getLogger('nova.instance_categories')
1026+
1027+
1028+def create(name, description, image_id=None, specs=None):
1029+ """Creates instance category."""
1030+ try:
1031+ db.instance_cat_create(
1032+ context.get_admin_context(),
1033+ dict(name=name,
1034+ description=description,
1035+ image_id=image_id,
1036+ specs=specs))
1037+ except exception.DBError, e:
1038+ LOG.exception(_('DB error: %s') % e)
1039+ raise exception.ApiError(_("Cannot create instance category with name"
1040+ " %(name)s, description %(description)s and"
1041+ " specs %(specs)s") % locals())
1042+
1043+
1044+def update(name, description=None, image_id=None, specs=None):
1045+ """Updates instance category the instance in the datastore. """
1046+ rv = db.instance_cat_update(
1047+ context.get_admin_context(),
1048+ name,
1049+ dict(description=description,
1050+ image_id=image_id,
1051+ specs=specs))
1052+ return dict(rv.iteritems())
1053+
1054+
1055+def destroy(name):
1056+ """Marks instance categories as deleted."""
1057+ if name is None:
1058+ raise exception.InvalidInstanceCategory(instance_cat=name)
1059+ else:
1060+ try:
1061+ db.instance_cat_destroy(context.get_admin_context(), name)
1062+ except exception.NotFound:
1063+ LOG.exception(_('Category %s not found for deletion') % name)
1064+ raise exception.ApiError(_("Unknown instance category: %s") % name)
1065+
1066+
1067+def purge(name):
1068+ """Removes instance categories from database."""
1069+ if name is None:
1070+ raise exception.InvalidInstanceCategory(instance_cat=name)
1071+ else:
1072+ try:
1073+ db.instance_cat_purge(context.get_admin_context(), name)
1074+ except exception.NotFound:
1075+ LOG.exception(_('Instance category %s not found for purge') % name)
1076+ raise exception.ApiError(_("Unknown instance category: %s") % name)
1077+
1078+
1079+def get_all_categories(inactive=0):
1080+ """Get all non-deleted instance categories.
1081+
1082+ Pass true as argument if you want deleted categories returned also.
1083+
1084+ """
1085+ return db.instance_cat_get_all(context.get_admin_context(), inactive)
1086+
1087+
1088+def default_category_name():
1089+ return FLAGS.instance_cat_regular
1090+
1091+
1092+def get_default_instance_cat():
1093+ """Get the default instance category."""
1094+ return get_instance_cat_by_name(default_category_name())
1095+
1096+
1097+def get_instance_cat(id):
1098+ """Retrieves single instance category by id."""
1099+ if id is None:
1100+ return get_default_instance_cat()
1101+ try:
1102+ ctxt = context.get_admin_context()
1103+ return db.instance_cat_get_by_id(ctxt, id)
1104+ except exception.NotFound:
1105+ LOG.exception(_('Instance category with id %d not found') % id)
1106+ raise
1107+
1108+
1109+def get_instance_cat_by_name(name):
1110+ """Retrieves single instance category by name."""
1111+ if name is None:
1112+ return get_default_instance_cat()
1113+ try:
1114+ ctxt = context.get_admin_context()
1115+ return db.instance_cat_get_by_name(ctxt, name)
1116+ except exception.NotFound:
1117+ LOG.exception(_('Instance category with name %s not found') % name)
1118+ raise
1119+
1120+
1121+def get_instance_cat_id_by_name(name):
1122+ """Retrieves single instance category ID by name."""
1123+ return get_instance_cat_by_name(name)['id']
1124+
1125+
1126+def get_instance_cat_name(instance):
1127+ """Returns instance category name by instance_ref"""
1128+ if 'instance_cat' in instance \
1129+ and 'name' in instance['instance_cat']:
1130+ return instance['instance_cat']['name']
1131+ else:
1132+ return get_instance_cat(instance['instance_cat_id'])['name']
1133+
1134+
1135+def is_admin_cat_name(name):
1136+ return False if (name is None) or \
1137+ (name == FLAGS.instance_cat_regular) else True
1138+
1139+
1140+def is_access_allowed(context, instance, instance_cat=None):
1141+ """Determines if access to instance is allowed
1142+
1143+ If admin context - allow everything, otherwise:
1144+ if regular instance - allow
1145+ otherwise (one of categories)
1146+ if instance's cat matches instance_cat - allow
1147+ if not matches: - restrict
1148+ """
1149+
1150+ if context.is_admin:
1151+ return True
1152+ if not is_admin_instance(instance):
1153+ return True
1154+ if instance_cat is not None \
1155+ and get_instance_cat_name(instance) == instance_cat['name']:
1156+ return True
1157+ else:
1158+ return False
1159+
1160+
1161+def is_admin_category(instance_cat):
1162+ return is_admin_cat_name(instance_cat['name'])
1163+
1164+
1165+def is_admin_instance(instance, category=None):
1166+ """ Verifies if instance is one of admin instances.
1167+
1168+ If category specified - returns True only if category matches
1169+ """
1170+ if instance['instance_cat_id'] is None:
1171+ return False
1172+
1173+ try:
1174+ inst_cat_name = get_instance_cat_name(instance)
1175+ except exception.NotFound:
1176+ raise
1177+
1178+ if inst_cat_name == FLAGS.instance_cat_regular:
1179+ return False
1180+
1181+ if category is not None \
1182+ and category != inst_cat_name:
1183+ return False
1184+
1185+ return True
1186+
1187+
1188+def get_admin_image_names(category=None):
1189+ """Return list of image_ids for all or specified categories"""
1190+ return db.instance_cat_get_all_images(context.get_admin_context(),
1191+ category)
1192
1193=== modified file 'nova/compute/manager.py'
1194--- nova/compute/manager.py 2011-06-28 20:37:05 +0000
1195+++ nova/compute/manager.py 2011-06-28 22:54:37 +0000
1196@@ -54,6 +54,7 @@
1197 from nova import utils
1198 from nova import volume
1199 from nova.compute import power_state
1200+from nova.compute import instance_categories
1201 from nova.notifier import api as notifier_api
1202 from nova.compute.utils import terminate_volumes
1203 from nova.virt import driver
1204@@ -297,7 +298,8 @@
1205 power_state.NOSTATE,
1206 'networking')
1207
1208- is_vpn = instance_ref['image_ref'] == str(FLAGS.vpn_image_id)
1209+ is_vpn = instance_categories.is_admin_instance(instance_ref,
1210+ FLAGS.instance_cat_vpn)
1211 try:
1212 # NOTE(vish): This could be a cast because we don't do anything
1213 # with the address currently, but I'm leaving it as
1214
1215=== modified file 'nova/db/api.py'
1216--- nova/db/api.py 2011-06-28 20:53:45 +0000
1217+++ nova/db/api.py 2011-06-28 22:54:37 +0000
1218@@ -467,6 +467,11 @@
1219 return IMPL.instance_get_all_by_reservation(context, reservation_id)
1220
1221
1222+def instance_get_all_by_cat(context, instance_cat_id):
1223+ """Get all instance belonging to a category."""
1224+ return IMPL.instance_get_all_by_cat(context, instance_cat_id)
1225+
1226+
1227 def instance_get_fixed_address(context, instance_id):
1228 """Get the fixed ip address of an instance."""
1229 return IMPL.instance_get_fixed_address(context, instance_id)
1230@@ -481,9 +486,9 @@
1231 return IMPL.instance_get_floating_address(context, instance_id)
1232
1233
1234-def instance_get_project_vpn(context, project_id):
1235+def instance_get_project_admin(context, project_id, cat_id):
1236 """Get a vpn instance by project or return None."""
1237- return IMPL.instance_get_project_vpn(context, project_id)
1238+ return IMPL.instance_get_project_admin(context, project_id, cat_id)
1239
1240
1241 def instance_set_state(context, instance_id, state, description=None):
1242@@ -1279,6 +1284,57 @@
1243 return IMPL.instance_type_purge(context, name)
1244
1245
1246+##################
1247+
1248+
1249+def instance_cat_create(context, values):
1250+ """Create a new instance category."""
1251+ return IMPL.instance_cat_create(context, values)
1252+
1253+
1254+def instance_cat_update(context, name, values):
1255+ """Set the given properties on an instance category and update it.
1256+
1257+ Raises NotFound if category does not exist.
1258+
1259+ """
1260+ return IMPL.instance_cat_update(context, name, values)
1261+
1262+
1263+def instance_cat_get_all(context, inactive=False):
1264+ """Get all instance categories."""
1265+ return IMPL.instance_cat_get_all(context, inactive)
1266+
1267+
1268+def instance_cat_get_by_id(context, id):
1269+ """Get instance category by id."""
1270+ return IMPL.instance_cat_get_by_id(context, id)
1271+
1272+
1273+def instance_cat_get_by_name(context, name):
1274+ """Get instance category by name."""
1275+ return IMPL.instance_cat_get_by_name(context, name)
1276+
1277+
1278+def instance_cat_get_all_images(context, category=None):
1279+ """Get registered images for all or selected categories."""
1280+ return IMPL.instance_cat_get_all_images(context, category)
1281+
1282+
1283+def instance_cat_destroy(context, name):
1284+ """Delete an instance category."""
1285+ return IMPL.instance_cat_destroy(context, name)
1286+
1287+
1288+def instance_cat_purge(context, name):
1289+ """Purges (removes) an instance category from DB.
1290+
1291+ Use instance_type_destroy for most cases
1292+
1293+ """
1294+ return IMPL.instance_cat_purge(context, name)
1295+
1296+
1297 ####################
1298
1299
1300
1301=== modified file 'nova/db/sqlalchemy/api.py'
1302--- nova/db/sqlalchemy/api.py 2011-06-28 20:53:45 +0000
1303+++ nova/db/sqlalchemy/api.py 2011-06-28 22:54:37 +0000
1304@@ -932,6 +932,7 @@
1305 options(joinedload('volumes')).\
1306 options(joinedload_all('fixed_ip.network')).\
1307 options(joinedload('metadata')).\
1308+ options(joinedload('instance_cat')).\
1309 options(joinedload('instance_type'))
1310
1311 if is_admin_context(context):
1312@@ -951,6 +952,7 @@
1313 options(joinedload_all('fixed_ip.network')).\
1314 options(joinedload('metadata')).\
1315 options(joinedload('instance_type')).\
1316+ options(joinedload('instance_cat')).\
1317 filter_by(deleted=can_read_deleted(context)).\
1318 all()
1319
1320@@ -982,6 +984,7 @@
1321 options(joinedload_all('fixed_ip.network')).\
1322 options(joinedload('metadata')).\
1323 options(joinedload('instance_type')).\
1324+ options(joinedload('instance_cat')).\
1325 filter_by(deleted=can_read_deleted(context)).\
1326 filter_by(user_id=user_id).\
1327 all()
1328@@ -996,6 +999,7 @@
1329 options(joinedload_all('fixed_ip.network')).\
1330 options(joinedload('metadata')).\
1331 options(joinedload('instance_type')).\
1332+ options(joinedload('instance_cat')).\
1333 filter_by(host=host).\
1334 filter_by(deleted=can_read_deleted(context)).\
1335 all()
1336@@ -1012,6 +1016,7 @@
1337 options(joinedload_all('fixed_ip.network')).\
1338 options(joinedload('metadata')).\
1339 options(joinedload('instance_type')).\
1340+ options(joinedload('instance_cat')).\
1341 filter_by(project_id=project_id).\
1342 filter_by(deleted=can_read_deleted(context)).\
1343 all()
1344@@ -1028,6 +1033,7 @@
1345 options(joinedload_all('fixed_ip.network')).\
1346 options(joinedload('metadata')).\
1347 options(joinedload('instance_type')).\
1348+ options(joinedload('instance_cat')).\
1349 filter_by(reservation_id=reservation_id).\
1350 filter_by(deleted=can_read_deleted(context)).\
1351 all()
1352@@ -1038,6 +1044,7 @@
1353 options(joinedload_all('fixed_ip.network')).\
1354 options(joinedload('metadata')).\
1355 options(joinedload('instance_type')).\
1356+ options(joinedload('instance_cat')).\
1357 filter_by(project_id=context.project_id).\
1358 filter_by(reservation_id=reservation_id).\
1359 filter_by(deleted=False).\
1360@@ -1045,16 +1052,32 @@
1361
1362
1363 @require_admin_context
1364-def instance_get_project_vpn(context, project_id):
1365- session = get_session()
1366- return session.query(models.Instance).\
1367- options(joinedload_all('fixed_ip.floating_ips')).\
1368- options(joinedload('security_groups')).\
1369- options(joinedload_all('fixed_ip.network')).\
1370- options(joinedload('metadata')).\
1371- options(joinedload('instance_type')).\
1372+def instance_get_all_by_cat(context, instance_cat_id):
1373+ session = get_session()
1374+ return session.query(models.Instance).\
1375+ options(joinedload_all('fixed_ip.floating_ips')).\
1376+ options(joinedload('security_groups')).\
1377+ options(joinedload_all('fixed_ip.network')).\
1378+ options(joinedload('metadata')).\
1379+ options(joinedload('instance_type')).\
1380+ options(joinedload('instance_cat')).\
1381+ filter_by(instance_cat_id=instance_cat_id).\
1382+ filter_by(deleted=can_read_deleted(context)).\
1383+ all()
1384+
1385+
1386+@require_admin_context
1387+def instance_get_project_admin(context, project_id, cat_id):
1388+ session = get_session()
1389+ return session.query(models.Instance).\
1390+ options(joinedload_all('fixed_ip.floating_ips')).\
1391+ options(joinedload('security_groups')).\
1392+ options(joinedload_all('fixed_ip.network')).\
1393+ options(joinedload('metadata')).\
1394+ options(joinedload('instance_type')).\
1395+ options(joinedload('instance_cat')).\
1396 filter_by(project_id=project_id).\
1397- filter_by(image_ref=str(FLAGS.vpn_image_id)).\
1398+ filter_by(instance_cat_id=cat_id).\
1399 filter_by(deleted=can_read_deleted(context)).\
1400 first()
1401
1402@@ -2733,7 +2756,7 @@
1403 first()
1404
1405 if not inst_type:
1406- raise exception.InstanceTypeNotFound(instance_type=id)
1407+ raise exception.InstanceTypeNotFound(instance_type_id=id)
1408 else:
1409 return _dict_with_extra_specs(inst_type)
1410
1411@@ -2794,6 +2817,133 @@
1412 return instance_type_ref
1413
1414
1415+ ##################
1416+
1417+
1418+@require_admin_context
1419+def instance_cat_create(_context, values):
1420+ try:
1421+ instance_cat_ref = models.InstanceCategories()
1422+ instance_cat_ref.update(values)
1423+ instance_cat_ref.save()
1424+ except Exception, e:
1425+ raise exception.DBError(e)
1426+ return instance_cat_ref
1427+
1428+
1429+@require_admin_context
1430+def instance_cat_update(context, name, values):
1431+ session = get_session()
1432+ with session.begin():
1433+ instance_cat_ref = session.query(models.InstanceCategories).\
1434+ filter_by(name=name).\
1435+ first()
1436+ instance_cat_ref.update(values)
1437+ instance_cat_ref.save(session=session)
1438+ return instance_cat_ref
1439+
1440+
1441+@require_context
1442+def instance_cat_get_all(context, inactive=False):
1443+ """
1444+ Returns a dict describing all instance_categories with name as key.
1445+ """
1446+ session = get_session()
1447+ if inactive:
1448+ inst_cats = session.query(models.InstanceCategories).\
1449+ order_by("name").\
1450+ all()
1451+ else:
1452+ inst_cats = session.query(models.InstanceCategories).\
1453+ filter_by(deleted=False).\
1454+ order_by("name").\
1455+ all()
1456+ if inst_cats:
1457+ inst_cat_dict = {}
1458+ for i in inst_cats:
1459+ inst_cat_dict[i['name']] = dict(i)
1460+ return inst_cat_dict
1461+ else:
1462+ raise exception.NoInstanceCategoriesFound()
1463+
1464+
1465+@require_context
1466+def instance_cat_get_by_id(context, id):
1467+ """Returns a dict describing specific instance category"""
1468+ session = get_session()
1469+ inst_cat = session.query(models.InstanceCategories).\
1470+ filter_by(id=id).\
1471+ first()
1472+ if not inst_cat:
1473+ raise exception.InstanceCatNotFound(instance_cat_id=id)
1474+ else:
1475+ return dict(inst_cat)
1476+
1477+
1478+@require_context
1479+def instance_cat_get_by_name(context, name):
1480+ """Returns a dict describing specific instance category"""
1481+ session = get_session()
1482+ inst_cat = session.query(models.InstanceCategories).\
1483+ filter_by(name=name).\
1484+ first()
1485+ if not inst_cat:
1486+ raise exception.InstanceCatNotFoundByName(instance_cat_name=name)
1487+ else:
1488+ return dict(inst_cat)
1489+
1490+
1491+@require_admin_context
1492+def instance_cat_destroy(context, name):
1493+ """ Marks specific instance category as deleted"""
1494+ session = get_session()
1495+ instance_cat_ref = session.query(models.InstanceCategories).\
1496+ filter_by(name=name)
1497+ records = instance_cat_ref.update(dict(deleted=True))
1498+ if records == 0:
1499+ raise exception.InstanceCatNotFoundByName(instance_cat_name=name)
1500+ else:
1501+ return instance_cat_ref
1502+
1503+
1504+@require_admin_context
1505+def instance_cat_purge(context, name):
1506+ """ Removes specific instance category from DB
1507+ Usually instance_cat_destroy should be used
1508+ """
1509+ session = get_session()
1510+ instance_cat_ref = session.query(models.InstanceCategories).\
1511+ filter_by(name=name)
1512+ records = instance_cat_ref.delete()
1513+ if records == 0:
1514+ raise exception.InstanceCatNotFoundByName(instance_cat_name=name)
1515+ else:
1516+ return instance_cat_ref
1517+
1518+
1519+@require_context
1520+def instance_cat_get_all_images(context, category=None):
1521+ """
1522+ Returns a dict describing all instance_categories with name as key.
1523+ """
1524+ session = get_session()
1525+ if category is None:
1526+ images = session.query(models.InstanceCategories).\
1527+ filter(models.InstanceCategories.image_id != None).\
1528+ filter(models.InstanceCategories.name != \
1529+ FLAGS.instance_cat_regular).\
1530+ filter_by(deleted=False).\
1531+ all()
1532+ else:
1533+ images = session.query(models.InstanceCategories).\
1534+ filter(models.InstanceCategories.image_id != None).\
1535+ filter(models.InstanceCategories.name != \
1536+ FLAGS.instance_cat_regular).\
1537+ filter_by(deleted=False).\
1538+ filter_by(name=category).\
1539+ all()
1540+ return [image['image_id'] for image in images]
1541+
1542 ####################
1543
1544
1545
1546=== added file 'nova/db/sqlalchemy/migrate_repo/versions/029_add_instance_categories.py'
1547--- nova/db/sqlalchemy/migrate_repo/versions/029_add_instance_categories.py 1970-01-01 00:00:00 +0000
1548+++ nova/db/sqlalchemy/migrate_repo/versions/029_add_instance_categories.py 2011-06-28 22:54:37 +0000
1549@@ -0,0 +1,152 @@
1550+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1551+
1552+# Copyright 2010 OpenStack LLC.
1553+#
1554+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1555+# not use this file except in compliance with the License. You may obtain
1556+# a copy of the License at
1557+#
1558+# http://www.apache.org/licenses/LICENSE-2.0
1559+#
1560+# Unless required by applicable law or agreed to in writing, software
1561+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1562+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1563+# License for the specific language governing permissions and limitations
1564+# under the License.
1565+
1566+from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table
1567+from sqlalchemy import Text, Boolean
1568+# ForeignKey
1569+
1570+from nova import log as logging
1571+
1572+meta = MetaData()
1573+
1574+instances = Table('instances', meta,
1575+ Column('id', Integer(), primary_key=True, nullable=False),
1576+ Column('image_ref', String()),
1577+ )
1578+
1579+instance_cat_id = Column('instance_cat_id', Integer(), nullable=True)
1580+
1581+#
1582+# New Tables
1583+#
1584+instance_categories = Table('instance_categories', meta,
1585+ Column('created_at', DateTime(timezone=False)),
1586+ Column('updated_at', DateTime(timezone=False)),
1587+ Column('deleted_at', DateTime(timezone=False)),
1588+ Column('deleted', Boolean(create_constraint=True, name=None)),
1589+ Column('id', Integer(), primary_key=True, nullable=False),
1590+ Column('name',
1591+ String(length=255, convert_unicode=False, assert_unicode=None,
1592+ unicode_error=None, _warn_on_bytestring=False),
1593+ unique=True),
1594+ Column('description',
1595+ String(length=255, convert_unicode=False, assert_unicode=None,
1596+ unicode_error=None, _warn_on_bytestring=False),
1597+ unique=False, nullable=True),
1598+ Column('image_id',
1599+ String(length=255, convert_unicode=False, assert_unicode=None,
1600+ unicode_error=None, _warn_on_bytestring=False),
1601+ unique=False, nullable=True),
1602+ Column('specs',
1603+ Text(length=None, convert_unicode=False, assert_unicode=None,
1604+ unicode_error=None, _warn_on_bytestring=False),
1605+ nullable=True))
1606+
1607+
1608+def upgrade(migrate_engine):
1609+
1610+ from nova import context
1611+ from nova import db
1612+ from nova import flags
1613+
1614+ FLAGS = flags.FLAGS
1615+
1616+ # Upgrade operations go here. Don't create your own engine;
1617+ # bind migrate_engine to your metadata
1618+ meta.bind = migrate_engine
1619+
1620+ # Create instance_categories table
1621+ try:
1622+ instance_categories.create()
1623+ except Exception:
1624+ logging.info(repr(instance_categories))
1625+ logging.exception('Exception while creating instance_categories table')
1626+ raise
1627+
1628+ # Fill it up with default Categories
1629+ INSTANCE_CATEGORIES = {
1630+ FLAGS.instance_cat_regular: \
1631+ dict(description='Regular Compute Instance',
1632+ image_id=None,
1633+ specs=None),
1634+ FLAGS.instance_cat_vc: \
1635+ dict(description='Virtual Storage Controller',
1636+ image_id=None,
1637+ specs=None),
1638+ FLAGS.instance_cat_vpn: \
1639+ dict(description='VPN Instance',
1640+ image_id=str(FLAGS.vpn_image_id),
1641+ specs=None),
1642+ FLAGS.instance_cat_admin: \
1643+ dict(description='Generic Admin Instance',
1644+ image_id=None,
1645+ specs=None)}
1646+
1647+ try:
1648+ for name, values in INSTANCE_CATEGORIES.iteritems():
1649+ db.instance_cat_create(context.get_admin_context(),
1650+ {'name': name,
1651+ 'description': values["description"],
1652+ 'image_id': values["image_id"],
1653+ 'specs': values["specs"]})
1654+ except Exception:
1655+ logging.exception('Exception while adding default values to ' \
1656+ 'instance_categories table')
1657+ instance_categories.drop()
1658+ raise
1659+
1660+ # Add appropriate column to instances and fill it up with default values
1661+ instances.create_column(instance_cat_id)
1662+
1663+ try:
1664+ reg_recs = migrate_engine.execute(
1665+ instance_categories.select().\
1666+ where(instance_categories.c.name == \
1667+ FLAGS.instance_cat_regular)).\
1668+ fetchone()
1669+ vpn_recs = migrate_engine.execute(
1670+ instance_categories.select().\
1671+ where(instance_categories.c.name == \
1672+ FLAGS.instance_cat_vpn)).\
1673+ fetchone()
1674+
1675+ reg_cat_id = reg_recs['id']
1676+ vpn_cat_id = vpn_recs['id']
1677+
1678+ migrate_engine.execute(instances.update().\
1679+ where(instances.c.instance_cat_id == None).\
1680+ where(instances.c.image_ref != \
1681+ str(FLAGS.vpn_image_id)).\
1682+ values(instance_cat_id=reg_cat_id))
1683+
1684+ migrate_engine.execute(instances.update().\
1685+ where(instances.c.instance_cat_id == None).\
1686+ where(instances.c.image_ref == \
1687+ str(FLAGS.vpn_image_id)).\
1688+ values(instance_cat_id=vpn_cat_id))
1689+ except Exception:
1690+ logging.info(repr(instances))
1691+ logging.exception('Exception while setting default categories ' \
1692+ 'in instances table')
1693+ raise
1694+
1695+
1696+def downgrade(migrate_engine):
1697+ meta.bind = migrate_engine
1698+
1699+ instances.drop_column(instance_cat_id)
1700+
1701+ instance_categories.drop()
1702
1703=== modified file 'nova/db/sqlalchemy/migration.py'
1704--- nova/db/sqlalchemy/migration.py 2011-06-24 12:01:51 +0000
1705+++ nova/db/sqlalchemy/migration.py 2011-06-28 22:54:37 +0000
1706@@ -64,7 +64,7 @@
1707 'users', 'user_project_association',
1708 'user_project_role_association',
1709 'user_role_association',
1710- 'volumes'):
1711+ 'volumes', 'instance_categories'):
1712 assert table in meta.tables
1713 return db_version_control(1)
1714 except AssertionError:
1715
1716=== modified file 'nova/db/sqlalchemy/models.py'
1717--- nova/db/sqlalchemy/models.py 2011-06-27 00:02:18 +0000
1718+++ nova/db/sqlalchemy/models.py 2011-06-28 22:54:37 +0000
1719@@ -210,6 +210,7 @@
1720 host = Column(String(255)) # , ForeignKey('hosts.id'))
1721
1722 instance_type_id = Column(Integer)
1723+ instance_cat_id = Column(Integer)
1724
1725 user_data = Column(Text)
1726
1727@@ -278,6 +279,22 @@
1728 'InstanceTypes.id)')
1729
1730
1731+class InstanceCategories(BASE, NovaBase):
1732+ """Represent possible instance_categories of VM offered"""
1733+ __tablename__ = "instance_categories"
1734+ id = Column(Integer, primary_key=True)
1735+ name = Column(String(255), unique=True)
1736+ description = Column(String(255), nullable=True)
1737+ image_id = Column(String(255), nullable=True)
1738+ specs = Column(Text, nullable=True)
1739+
1740+ instances = relationship(Instance,
1741+ backref=backref('instance_cat', uselist=False),
1742+ foreign_keys=id,
1743+ primaryjoin='and_(Instance.instance_cat_id == '
1744+ 'InstanceCategories.id)')
1745+
1746+
1747 class Volume(BASE, NovaBase):
1748 """Represents a block storage device that can be attached to a vm."""
1749 __tablename__ = 'volumes'
1750@@ -765,7 +782,8 @@
1751 Network, SecurityGroup, SecurityGroupIngressRule,
1752 SecurityGroupInstanceAssociation, AuthToken, User,
1753 Project, Certificate, ConsolePool, Console, Zone,
1754- AgentBuild, InstanceMetadata, InstanceTypeExtraSpecs, Migration)
1755+ AgentBuild, InstanceMetadata, InstanceTypeExtraSpecs, Migration,
1756+ InstanceCategories)
1757 engine = create_engine(FLAGS.sql_connection, echo=False)
1758 for model in models:
1759 model.metadata.create_all(engine)
1760
1761=== modified file 'nova/exception.py'
1762--- nova/exception.py 2011-06-28 15:13:52 +0000
1763+++ nova/exception.py 2011-06-28 22:54:37 +0000
1764@@ -145,6 +145,10 @@
1765 message = _("Invalid instance type %(instance_type)s.")
1766
1767
1768+class InvalidInstanceCategory(Invalid):
1769+ message = _("Invalid instance category %(instance_cat)s.")
1770+
1771+
1772 class InvalidPortRange(Invalid):
1773 message = _("Invalid port range %(from_port)s:%(to_port)s.")
1774
1775@@ -482,6 +486,19 @@
1776 "could not be found.")
1777
1778
1779+class NoInstanceCategoriesFound(NotFound):
1780+ message = _("Zero instance categories found.")
1781+
1782+
1783+class InstanceCatNotFound(NotFound):
1784+ message = _("Instance category %(instance_cat_id)d could not be found.")
1785+
1786+
1787+class InstanceCatNotFoundByName(InstanceCatNotFound):
1788+ message = _("Instance category with name %(instance_cat_name)s "
1789+ "could not be found.")
1790+
1791+
1792 class FlavorNotFound(NotFound):
1793 message = _("Flavor %(flavor_id)s could not be found.")
1794
1795
1796=== modified file 'nova/flags.py'
1797--- nova/flags.py 2011-06-24 12:01:51 +0000
1798+++ nova/flags.py 2011-06-28 22:54:37 +0000
1799@@ -324,6 +324,16 @@
1800 'default image to use, testing only')
1801 DEFINE_string('default_instance_type', 'm1.small',
1802 'default instance type to use, testing only')
1803+
1804+DEFINE_string('instance_cat_regular', 'regular',
1805+ 'default instance category for regular compute instances')
1806+DEFINE_string('instance_cat_vc', 'vc',
1807+ 'instance category for VC instances')
1808+DEFINE_string('instance_cat_vpn', 'vpn',
1809+ 'instance category for VPN instances')
1810+DEFINE_string('instance_cat_admin', 'admin',
1811+ 'instance category for generic Admin instances')
1812+
1813 DEFINE_string('null_kernel', 'nokernel',
1814 'kernel image that indicates not to use a kernel,'
1815 ' but to use a raw disk image instead')
1816
1817=== modified file 'nova/tests/api/openstack/test_servers.py'
1818--- nova/tests/api/openstack/test_servers.py 2011-06-24 12:01:51 +0000
1819+++ nova/tests/api/openstack/test_servers.py 2011-06-28 22:54:37 +0000
1820@@ -34,6 +34,7 @@
1821 from nova.api.openstack import create_instance_helper
1822 import nova.compute.api
1823 from nova.compute import instance_types
1824+from nova.compute import instance_categories
1825 from nova.compute import power_state
1826 import nova.db.api
1827 import nova.scheduler.api
1828@@ -129,6 +130,7 @@
1829 metadata.append(InstanceMetadata(key='seq', value=id))
1830
1831 inst_type = instance_types.get_instance_type_by_flavor_id(1)
1832+ inst_cat = instance_categories.get_default_instance_cat()
1833
1834 if public_addresses is None:
1835 public_addresses = list()
1836@@ -160,6 +162,8 @@
1837 "hostname": "",
1838 "host": host,
1839 "instance_type": dict(inst_type),
1840+ "instance_cat": dict(inst_cat),
1841+ "instance_cat_id": dict(inst_cat)['id'],
1842 "user_data": "",
1843 "reservation_id": reservation_id,
1844 "mac_address": "",
1845
1846=== added file 'nova/tests/test_instance_categories.py'
1847--- nova/tests/test_instance_categories.py 1970-01-01 00:00:00 +0000
1848+++ nova/tests/test_instance_categories.py 2011-06-28 22:54:37 +0000
1849@@ -0,0 +1,295 @@
1850+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1851+
1852+# Copyright 2011 Ken Pepple
1853+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1854+# not use this file except in compliance with the License. You may obtain
1855+# a copy of the License at
1856+#
1857+# http://www.apache.org/licenses/LICENSE-2.0
1858+#
1859+# Unless required by applicable law or agreed to in writing, software
1860+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1861+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1862+# License for the specific language governing permissions and limitations
1863+# under the License.
1864+"""
1865+Unit Tests for instance categories code
1866+"""
1867+import time
1868+
1869+from nova import context
1870+from nova import db
1871+from nova import exception
1872+from nova import flags
1873+from nova import log as logging
1874+from nova import test
1875+from nova import utils
1876+from nova.compute import instance_categories
1877+from nova.db.sqlalchemy.session import get_session
1878+from nova.db.sqlalchemy import models
1879+
1880+FLAGS = flags.FLAGS
1881+LOG = logging.getLogger('nova.tests.compute')
1882+
1883+
1884+class InstanceCatTestCase(test.TestCase):
1885+ """Test cases for instance categories code"""
1886+ def setUp(self):
1887+ super(InstanceCatTestCase, self).setUp()
1888+ self.cntx = context.RequestContext(None, None)
1889+ self.cntx_admin = context.get_admin_context()
1890+ session = get_session()
1891+ max_id = session.query(models.InstanceCategories).\
1892+ order_by("id desc").\
1893+ first()
1894+ self.name = str(int(time.time()))
1895+ self.description = "Description_for_" + self.name
1896+ self.image_id = "Image_str_for_" + self.name
1897+
1898+ def test_instance_cat_create_then_delete(self):
1899+ """Ensure instance types can be created"""
1900+ starting_list_active = instance_categories.get_all_categories()
1901+ starting_list_inactive = instance_categories.get_all_categories(1)
1902+ instance_categories.create(self.name, self.description)
1903+ new = instance_categories.get_all_categories()
1904+ self.assertNotEqual(len(starting_list_active),
1905+ len(new),
1906+ 'instance category was not created')
1907+ self.assertEqual(self.name,
1908+ instance_categories.\
1909+ get_instance_cat_by_name(self.name)['name'],
1910+ 'instance category with expected name not found '\
1911+ 'or doesnt match')
1912+
1913+ cat_id = instance_categories.get_instance_cat_id_by_name(self.name)
1914+
1915+ self.assertEqual(self.name,
1916+ instance_categories.get_instance_cat(cat_id)["name"],
1917+ 'instance category with expected ID not found '\
1918+ 'or doesnt match')
1919+ self.assertEqual(self.description,
1920+ instance_categories.\
1921+ get_instance_cat(cat_id)["description"],
1922+ 'instance category description with expected ID '\
1923+ 'not found or doesnt match')
1924+
1925+ instance_categories.destroy(self.name)
1926+ self.assertNotEqual(instance_categories.get_all_categories(),
1927+ instance_categories.get_all_categories(1),
1928+ 'active and inactive should be different')
1929+ self.assertEqual(1,
1930+ instance_categories.\
1931+ get_instance_cat(cat_id)["deleted"],
1932+ 'instance category is not marked as deleted')
1933+ self.assertEqual(starting_list_active,
1934+ instance_categories.get_all_categories(),
1935+ 'instance category was not deleted')
1936+
1937+ instance_categories.purge(self.name)
1938+ self.assertEqual(starting_list_inactive,
1939+ instance_categories.get_all_categories(1),
1940+ 'instance category not purged')
1941+
1942+ def test_instance_cat_update(self):
1943+ """Ensure instance types can be created"""
1944+ instance_categories.create(self.name, self.description, self.image_id)
1945+ cat_id = instance_categories.get_instance_cat_id_by_name(self.name)
1946+
1947+ self.assertEqual(self.name,
1948+ instance_categories.get_instance_cat(cat_id)["name"],
1949+ 'instance category with expected ID not found '\
1950+ 'or doesnt match')
1951+ self.assertEqual(self.description,
1952+ instance_categories.\
1953+ get_instance_cat(cat_id)["description"],
1954+ 'description not found or doesnt match')
1955+ self.assertEqual(self.image_id,
1956+ instance_categories.\
1957+ get_instance_cat(cat_id)["image_id"],
1958+ 'image_id not found or doesnt match')
1959+
1960+ new_desc = 'New desc'
1961+ new_image_id = 'New image id'
1962+ instance_categories.update(self.name, new_desc, new_image_id)
1963+
1964+ self.assertEqual(new_desc,
1965+ instance_categories.\
1966+ get_instance_cat(cat_id)["description"],
1967+ 'description not found or doesnt match')
1968+ self.assertEqual(new_image_id,
1969+ instance_categories.\
1970+ get_instance_cat(cat_id)["image_id"],
1971+ 'image_id not found or doesnt match')
1972+
1973+ instance_categories.destroy(self.name)
1974+ instance_categories.purge(self.name)
1975+
1976+ def test_instance_cat_non_existant_shouldnt_delete_purge(self):
1977+ """
1978+ Ensures that instance category deletion/purge fails with invalid args
1979+ """
1980+ self.assertRaises(exception.InvalidInstanceCategory,
1981+ instance_categories.destroy, None)
1982+ self.assertRaises(exception.ApiError,
1983+ instance_categories.destroy, "sfsfsdfdfs")
1984+
1985+ self.assertRaises(exception.InvalidInstanceCategory,
1986+ instance_categories.purge, None)
1987+ self.assertRaises(exception.ApiError,
1988+ instance_categories.purge, "qweqwe")
1989+
1990+ def test_instance_cat_get_all_cats(self):
1991+ """Ensures that all instance categories can be retrieved"""
1992+ session = get_session()
1993+ total_instance_cats = session.query(models.InstanceCategories).\
1994+ count()
1995+ instance_cats = len(instance_categories.get_all_categories(1))
1996+ self.assertEqual(total_instance_cats, instance_cats)
1997+
1998+ def test_instance_cat_dup_name_should_raise_api_error(self):
1999+ """Ensures that duplicate instance category raises ApiError"""
2000+ new_name = self.name + "dup"
2001+ instance_categories.create(new_name, "desc - duplicate test")
2002+ instance_categories.destroy(new_name)
2003+ self.assertRaises(exception.ApiError,
2004+ instance_categories.create, new_name,
2005+ "desc - duplicate category")
2006+ instance_categories.purge(new_name)
2007+
2008+ def test_instance_cat_get_by_incorrect_id_name(self):
2009+ """Ensures that get cat fails with incorrect args
2010+ or returns default values"""
2011+ self.assertEqual(instance_categories.get_instance_cat(None),
2012+ instance_categories.get_default_instance_cat(),
2013+ 'get instance category without id should return '\
2014+ 'default category')
2015+
2016+ self.assertEqual(instance_categories.get_instance_cat_by_name(None),
2017+ instance_categories.get_default_instance_cat(),
2018+ 'get instance category without name should return '\
2019+ 'default category')
2020+
2021+ session = get_session()
2022+ max_id = session.query(models.InstanceCategories).\
2023+ order_by("id desc").\
2024+ first()
2025+ wrong_id = max_id["id"] + 100
2026+ self.assertRaises(exception.NotFound,
2027+ instance_categories.get_instance_cat, wrong_id)
2028+
2029+ self.assertRaises(exception.NotFound,
2030+ instance_categories.get_instance_cat_by_name,
2031+ "invalid_cat_name_qweqwe")
2032+
2033+ def test_instance_cat_is_admin_name(self):
2034+ """Ensures that is_admin_cat_name returns True for exerything except
2035+ empty or regular cat names"""
2036+ self.assertFalse(instance_categories.is_admin_cat_name(None))
2037+ self.assertFalse(instance_categories.\
2038+ is_admin_cat_name(FLAGS.instance_cat_regular))
2039+ self.assertTrue(instance_categories.\
2040+ is_admin_cat_name(FLAGS.instance_cat_vpn))
2041+ self.assertTrue(instance_categories.\
2042+ is_admin_cat_name(FLAGS.instance_cat_vc))
2043+ self.assertTrue(instance_categories.\
2044+ is_admin_cat_name(FLAGS.instance_cat_admin))
2045+ self.assertTrue(instance_categories.\
2046+ is_admin_cat_name("any_cat_name_asdqwe"))
2047+
2048+ def test_instance_cat_is_admin_category(self):
2049+ """Ensures that is_admin_category returns True for exerything
2050+ except empty or regular cat names"""
2051+ cat = {}
2052+ cat['name'] = FLAGS.instance_cat_regular
2053+ self.assertFalse(instance_categories.is_admin_category(cat))
2054+ cat['name'] = FLAGS.instance_cat_admin
2055+ self.assertTrue(instance_categories.is_admin_cat_name(cat))
2056+
2057+ def test_instance_cat_check_default_values(self):
2058+ """Ensures correctness of default values"""
2059+ self.assertEqual(instance_categories.default_category_name(),
2060+ FLAGS.instance_cat_regular)
2061+ self.assertEqual(instance_categories.\
2062+ get_default_instance_cat()['name'],
2063+ FLAGS.instance_cat_regular)
2064+
2065+ def test_instance_cat_create_check_params_and_admin_func(self):
2066+ """Ensures that different get and is_admin funcs operate correctly"""
2067+ instance_categories.create(self.name, self.description)
2068+
2069+ self.assertEqual(self.name,
2070+ instance_categories.\
2071+ get_instance_cat_by_name(self.name)['name'])
2072+
2073+ cat_id = instance_categories.get_instance_cat_id_by_name(self.name)
2074+ self.assertEqual(self.name,
2075+ instance_categories.get_instance_cat(cat_id)['name'])
2076+
2077+ instance = {}
2078+ instance['name'] = 'test compute instance'
2079+ instance['instance_cat_id'] = None
2080+ # no 'instance_cat_id' field
2081+ self.assertFalse(instance_categories.is_admin_instance(instance))
2082+
2083+ instance['instance_cat_id'] = cat_id + 1
2084+ # invalid category id
2085+ self.assertRaises(exception.NotFound,
2086+ instance_categories.is_admin_instance, instance)
2087+
2088+ instance['instance_cat_id'] = cat_id
2089+ self.assertTrue(instance_categories.is_admin_instance(instance))
2090+ self.assertEqual(self.name,
2091+ instance_categories.get_instance_cat_name(instance))
2092+
2093+ instance['instance_cat'] = {}
2094+ self.assertEqual(self.name,
2095+ instance_categories.get_instance_cat_name(instance))
2096+ instance['instance_cat']['name'] = self.name
2097+ self.assertEqual(self.name,
2098+ instance_categories.get_instance_cat_name(instance))
2099+
2100+ self.assertTrue(instance_categories.is_admin_instance(instance))
2101+ self.assertTrue(instance_categories.\
2102+ is_admin_instance(instance,
2103+ self.name))
2104+ self.assertFalse(instance_categories.\
2105+ is_admin_instance(instance,
2106+ FLAGS.instance_cat_vpn))
2107+
2108+ instance['instance_cat']['name'] = FLAGS.instance_cat_regular
2109+ self.assertFalse(instance_categories.is_admin_instance(instance))
2110+ self.assertFalse(instance_categories.\
2111+ is_admin_instance(instance,
2112+ FLAGS.instance_cat_regular))
2113+ self.assertFalse(instance_categories.\
2114+ is_admin_instance(instance,
2115+ FLAGS.instance_cat_vpn))
2116+
2117+ instance_categories.destroy(self.name)
2118+ instance_categories.purge(self.name)
2119+
2120+ self.assertTrue(instance_categories.\
2121+ is_access_allowed(self.cntx, instance))
2122+
2123+ instance['instance_cat']['name'] = FLAGS.instance_cat_vpn
2124+ self.assertFalse(instance_categories.\
2125+ is_access_allowed(self.cntx, instance))
2126+
2127+ instance_cat = instance_categories.\
2128+ get_instance_cat_by_name(FLAGS.instance_cat_vpn)
2129+ self.assertTrue(instance_categories.\
2130+ is_access_allowed(self.cntx,
2131+ instance, instance_cat))
2132+ self.assertTrue(instance_categories.\
2133+ is_access_allowed(self.cntx_admin,
2134+ instance))
2135+
2136+ instance_cat = instance_categories.\
2137+ get_instance_cat_by_name(FLAGS.instance_cat_vc)
2138+ self.assertTrue(instance_categories.\
2139+ is_access_allowed(self.cntx_admin,
2140+ instance, instance_cat))
2141+
2142+ instance['instance_cat']['name'] = FLAGS.instance_cat_regular
2143+ self.assertTrue(instance_categories.\
2144+ is_access_allowed(self.cntx_admin, instance))
2145
2146=== modified file 'nova/virt/libvirt/firewall.py'
2147--- nova/virt/libvirt/firewall.py 2011-06-24 12:01:51 +0000
2148+++ nova/virt/libvirt/firewall.py 2011-06-28 22:54:37 +0000
2149@@ -26,6 +26,7 @@
2150 from nova import log as logging
2151 from nova import utils
2152 from nova.virt.libvirt import netutils
2153+from nova.compute import instance_categories
2154
2155
2156 LOG = logging.getLogger("nova.virt.libvirt.firewall")
2157@@ -204,7 +205,8 @@
2158 logging.info('ensuring static filters')
2159 self._ensure_static_filters()
2160
2161- if instance['image_ref'] == str(FLAGS.vpn_image_id):
2162+ if instance_categories.is_admin_instance(instance,
2163+ FLAGS.instance_cat_vpn):
2164 base_filter = 'nova-vpn'
2165 else:
2166 base_filter = 'nova-base'
2167@@ -375,7 +377,8 @@
2168
2169 def _create_network_filters(self, instance, network_info,
2170 instance_secgroup_filter_name):
2171- if instance['image_ref'] == str(FLAGS.vpn_image_id):
2172+ if instance_categories.is_admin_instance(instance,
2173+ FLAGS.instance_cat_vpn):
2174 base_filter = 'nova-vpn'
2175 else:
2176 base_filter = 'nova-base'