1# Copyright 2014, Rackspace, US, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""API over the nova service."""
15from collections import OrderedDict
16
17from django.utils import http as utils_http
18from django.utils.translation import ugettext_lazy as _
19from django.views import generic
20from novaclient import exceptions
21
22from horizon import exceptions as hz_exceptions
23
24from openstack_dashboard import api
25from openstack_dashboard.api.rest import json_encoder
26from openstack_dashboard.api.rest import urls
27from openstack_dashboard.api.rest import utils as rest_utils
28from openstack_dashboard.dashboards.project.instances \
29    import utils as instances_utils
30from openstack_dashboard.usage import quotas
31
32
33@urls.register
34class Snapshots(generic.View):
35    """API for nova snapshots."""
36    url_regex = r'nova/snapshots/$'
37
38    @rest_utils.ajax(data_required=True)
39    def post(self, request):
40        instance_id = request.DATA['instance_id']
41        name = request.DATA['name']
42        result = api.nova.snapshot_create(request,
43                                          instance_id=instance_id,
44                                          name=name)
45        return result
46
47
48@urls.register
49class Features(generic.View):
50    """API for check if a specified feature is supported."""
51    url_regex = r'nova/features/(?P<name>[^/]+)/$'
52
53    @rest_utils.ajax()
54    def get(self, request, name):
55        """Check if a specified feature is supported."""
56        return api.nova.is_feature_available(request, (name,))
57
58
59@urls.register
60class Keypairs(generic.View):
61    """API for nova keypairs."""
62    url_regex = r'nova/keypairs/$'
63
64    @rest_utils.ajax()
65    def get(self, request):
66        """Get a list of keypairs associated with the current logged-in user.
67
68        The listing result is an object with property "items".
69        """
70        result = api.nova.keypair_list(request)
71        return {'items': [u.to_dict() for u in result]}
72
73    @rest_utils.ajax(data_required=True)
74    def post(self, request):
75        """Create a keypair.
76
77        Create a keypair using the parameters supplied in the POST
78        application/json object. The parameters are:
79
80        :param name: the name to give the keypair
81        :param public_key: (optional) a key to import
82
83        This returns the new keypair object on success.
84        """
85        if 'public_key' in request.DATA:
86            new = api.nova.keypair_import(request, request.DATA['name'],
87                                          request.DATA['public_key'],
88                                          request.DATA['key_type'])
89        else:
90            new = api.nova.keypair_create(request,
91                                          request.DATA['name'],
92                                          request.DATA['key_type'])
93        return rest_utils.CreatedResponse(
94            '/api/nova/keypairs/%s' % utils_http.urlquote(new.name),
95            new.to_dict()
96        )
97
98
99@urls.register
100class Keypair(generic.View):
101    """API for retrieving a single keypair."""
102    url_regex = r'nova/keypairs/(?P<name>[^/]+)$'
103
104    @rest_utils.ajax()
105    def get(self, request, name):
106        """Get a specific keypair."""
107        return api.nova.keypair_get(request, name).to_dict()
108
109    @rest_utils.ajax()
110    def delete(self, request, name):
111        api.nova.keypair_delete(request, name)
112
113
114@urls.register
115class Services(generic.View):
116    """API for nova services."""
117    url_regex = r'nova/services/$'
118
119    @rest_utils.ajax()
120    def get(self, request):
121        """Get a list of nova services.
122
123        Will return HTTP 501 status code if the compute service is enabled.
124        """
125        if api.base.is_service_enabled(request, 'compute'):
126            result = api.nova.service_list(request)
127            return {'items': [u.to_dict() for u in result]}
128        raise rest_utils.AjaxError(501, '')
129
130
131@urls.register
132class AvailabilityZones(generic.View):
133    """API for nova availability zones."""
134    url_regex = r'nova/availzones/$'
135
136    @rest_utils.ajax()
137    def get(self, request):
138        """Get a list of availability zones.
139
140        The following get parameters may be passed in the GET
141        request:
142
143        :param detailed: If this equals "true" then the result will
144            include more detail.
145
146        The listing result is an object with property "items".
147        """
148        detailed = request.GET.get('detailed') == 'true'
149        result = api.nova.availability_zone_list(request, detailed)
150        return {'items': [u.to_dict() for u in result]}
151
152
153@urls.register
154class Limits(generic.View):
155    """API for nova limits."""
156    url_regex = r'nova/limits/$'
157
158    @rest_utils.ajax(json_encoder=json_encoder.NaNJSONEncoder)
159    def get(self, request):
160        """Get an object describing the current project limits.
161
162        Note: the Horizon API doesn't support any other project (tenant) but
163        the underlying client does...
164
165        The following get parameters may be passed in the GET
166        request:
167
168        :param reserved: Take into account the reserved limits. Reserved limits
169            may be instances in the rebuild process for example.
170
171        The result is an object with limits as properties.
172        """
173        reserved = request.GET.get('reserved') == 'true'
174        result = api.nova.tenant_absolute_limits(request, reserved)
175        return result
176
177
178@urls.register
179class ServerActions(generic.View):
180    """API over all server actions."""
181    url_regex = r'nova/servers/(?P<server_id>[^/]+)/actions/$'
182
183    @rest_utils.ajax()
184    def get(self, request, server_id):
185        """Get a list of server actions.
186
187        The listing result is an object with property "items". Each item is
188        an action taken against the given server.
189
190        Example GET:
191        http://localhost/api/nova/servers/abcd/actions/
192        """
193        actions = api.nova.instance_action_list(request, server_id)
194        return {'items': [s.to_dict() for s in actions]}
195
196
197@urls.register
198class SecurityGroups(generic.View):
199    """API over all server security groups."""
200    url_regex = r'nova/servers/(?P<server_id>[^/]+)/security-groups/$'
201
202    @rest_utils.ajax()
203    def get(self, request, server_id):
204        """Get a list of server security groups.
205
206        The listing result is an object with property "items". Each item is
207        security group associated with this server.
208
209        Example GET:
210        http://localhost/api/nova/servers/abcd/security-groups/
211        """
212        groups = api.neutron.server_security_groups(request, server_id)
213        return {'items': [s.to_dict() for s in groups]}
214
215
216@urls.register
217class Volumes(generic.View):
218    """API over all server volumes."""
219    url_regex = r'nova/servers/(?P<server_id>[^/]+)/volumes/$'
220
221    @rest_utils.ajax()
222    def get(self, request, server_id):
223        """Get a list of server volumes.
224
225        The listing result is an object with property "items". Each item is
226        a volume.
227
228        Example GET:
229        http://localhost/api/nova/servers/abcd/volumes/
230        """
231        volumes = api.nova.instance_volumes_list(request, server_id)
232        return {'items': [s.to_dict() for s in volumes]}
233
234
235@urls.register
236class RemoteConsoleInfo(generic.View):
237    """API for remote console information."""
238    url_regex = r'nova/servers/(?P<server_id>[^/]+)/console-info/$'
239
240    @rest_utils.ajax()
241    def post(self, request, server_id):
242        """Gets information of a remote console for the given server.
243
244        Example POST:
245        http://localhost/api/nova/servers/abcd/console-info/
246        """
247        console_type = request.DATA.get('console_type', 'AUTO')
248        CONSOLES = OrderedDict([('VNC', api.nova.server_vnc_console),
249                                ('SPICE', api.nova.server_spice_console),
250                                ('RDP', api.nova.server_rdp_console),
251                                ('SERIAL', api.nova.server_serial_console),
252                                ('MKS', api.nova.server_mks_console)])
253
254        """Get a tuple of console url and console type."""
255        if console_type == 'AUTO':
256            check_consoles = CONSOLES
257        else:
258            try:
259                check_consoles = {console_type: CONSOLES[console_type]}
260            except KeyError:
261                msg = _('Console type "%s" not supported.') % console_type
262                raise hz_exceptions.NotAvailable(msg)
263
264        # Ugly workaround due novaclient API change from 2.17 to 2.18.
265        try:
266            httpnotimplemented = exceptions.HttpNotImplemented
267        except AttributeError:
268            httpnotimplemented = exceptions.HTTPNotImplemented
269
270        for con_type, api_call in check_consoles.items():
271            try:
272                console = api_call(request, server_id)
273            # If not supported, don't log it to avoid lot of errors in case
274            # of AUTO.
275            except httpnotimplemented:
276                continue
277            except Exception:
278                continue
279
280            if con_type == 'SERIAL':
281                console_url = console.url
282            else:
283                console_url = "%s&%s(%s)" % (
284                              console.url,
285                              utils_http.urlencode({'title': _("Console")}),
286                              server_id)
287
288            return {"type": con_type, "url": console_url}
289        raise hz_exceptions.NotAvailable(_('No available console found.'))
290
291
292@urls.register
293class ConsoleOutput(generic.View):
294    """API for console output."""
295    url_regex = r'nova/servers/(?P<server_id>[^/]+)/console-output/$'
296
297    @rest_utils.ajax()
298    def post(self, request, server_id):
299        """Get a list of lines of console output.
300
301        The listing result is an object with property "items". Each item is
302        a line of output from the server.
303
304        Example GET:
305        http://localhost/api/nova/servers/abcd/console-output/
306        """
307        log_length = request.DATA.get('length', 100)
308        console_lines = api.nova.server_console_output(request, server_id,
309                                                       tail_length=log_length)
310        return {"lines": console_lines.split('\n')}
311
312
313@urls.register
314class Servers(generic.View):
315    """API over all servers."""
316    url_regex = r'nova/servers/$'
317
318    _optional_create = [
319        'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
320        'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
321        'config_drive', 'scheduler_hints', 'description'
322    ]
323
324    @rest_utils.ajax()
325    def get(self, request):
326        """Get a list of servers.
327
328        The listing result is an object with property "items". Each item is
329        a server.
330
331        Example GET:
332        http://localhost/api/nova/servers
333        """
334        servers = api.nova.server_list(request)[0]
335        return {'items': [s.to_dict() for s in servers]}
336
337    @rest_utils.ajax(data_required=True)
338    def post(self, request):
339        """Create a server.
340
341        Create a server using the parameters supplied in the POST
342        application/json object. The required parameters as specified by
343        the underlying novaclient are:
344
345        :param name: The new server name.
346        :param source_id: The ID of the image to use.
347        :param flavor_id: The ID of the flavor to use.
348        :param key_name: (optional) name of previously created
349                      keypair to inject into the instance.
350        :param user_data: user data to pass to be exposed by the metadata
351                      server this can be a file type object as well or a
352                      string.
353        :param security_groups: An array of one or more objects with a "name"
354            attribute.
355
356        Other parameters are accepted as per the underlying novaclient:
357        "block_device_mapping", "block_device_mapping_v2", "nics", "meta",
358        "availability_zone", "instance_count", "admin_pass", "disk_config",
359        "config_drive", "scheduler_hints"
360
361        This returns the new server object on success.
362        """
363        try:
364            args = (
365                request,
366                request.DATA['name'],
367                request.DATA['source_id'],
368                request.DATA['flavor_id'],
369                request.DATA['key_name'],
370                request.DATA['user_data'],
371                request.DATA['security_groups'],
372            )
373        except KeyError as e:
374            raise rest_utils.AjaxError(400, 'missing required parameter '
375                                            "'%s'" % e.args[0])
376        kw = {}
377        for name in self._optional_create:
378            if name in request.DATA:
379                kw[name] = request.DATA[name]
380
381        new = api.nova.server_create(*args, **kw)
382        return rest_utils.CreatedResponse(
383            '/api/nova/servers/%s' % utils_http.urlquote(new.id),
384            new.to_dict()
385        )
386
387
388@urls.register
389class Server(generic.View):
390    """API for retrieving a single server"""
391    url_regex = r'nova/servers/(?P<server_id>[^/]+|default)$'
392
393    @rest_utils.ajax()
394    def get(self, request, server_id):
395        """Get a specific server
396
397        http://localhost/api/nova/servers/1
398        """
399        return api.nova.server_get(request, server_id).to_dict()
400
401    @rest_utils.ajax(data_required=True)
402    def post(self, request, server_id):
403        """Perform a change to a server"""
404        operation = request.DATA.get('operation', 'none')
405        operations = {
406            'stop': api.nova.server_stop,
407            'start': api.nova.server_start,
408            'pause': api.nova.server_pause,
409            'unpause': api.nova.server_unpause,
410            'suspend': api.nova.server_suspend,
411            'resume': api.nova.server_resume,
412            'hard_reboot': lambda r, s: api.nova.server_reboot(r, s, False),
413            'soft_reboot': lambda r, s: api.nova.server_reboot(r, s, True),
414        }
415        return operations[operation](request, server_id)
416
417    @rest_utils.ajax()
418    def delete(self, request, server_id):
419        api.nova.server_delete(request, server_id)
420
421
422@urls.register
423class ServerGroups(generic.View):
424    """API for nova server groups."""
425    url_regex = r'nova/servergroups/$'
426
427    @rest_utils.ajax()
428    def get(self, request):
429        """Get a list of server groups.
430
431        The listing result is an object with property "items".
432        """
433        result = api.nova.server_group_list(request)
434        return {'items': [u.to_dict() for u in result]}
435
436    @rest_utils.ajax(data_required=True)
437    def post(self, request):
438        """Create a server group.
439
440        Create a server group using parameters supplied in the POST
441        application/json object. The "name" (string) parameter is required
442        and the "policies" (array) parameter is required.
443
444        This method returns the new server group object on success.
445        """
446        new_servergroup = api.nova.server_group_create(request, **request.DATA)
447        return rest_utils.CreatedResponse(
448            '/api/nova/servergroups/%s' % utils_http.urlquote(
449                new_servergroup.id), new_servergroup.to_dict()
450        )
451
452
453@urls.register
454class ServerGroup(generic.View):
455    url_regex = r'nova/servergroups/(?P<servergroup_id>[^/]+)/$'
456
457    @rest_utils.ajax()
458    def delete(self, request, servergroup_id):
459        """Delete a specific server group
460
461        DELETE http://localhost/api/nova/servergroups/<servergroup_id>
462        """
463        api.nova.server_group_delete(request, servergroup_id)
464
465    @rest_utils.ajax()
466    def get(self, request, servergroup_id):
467        """Get a specific server group
468
469        http://localhost/api/nova/servergroups/1
470        """
471        return api.nova.server_group_get(request, servergroup_id).to_dict()
472
473
474@urls.register
475class ServerMetadata(generic.View):
476    """API for server metadata."""
477    url_regex = r'nova/servers/(?P<server_id>[^/]+|default)/metadata$'
478
479    @rest_utils.ajax()
480    def get(self, request, server_id):
481        """Get a specific server's metadata
482
483        http://localhost/api/nova/servers/1/metadata
484        """
485        return api.nova.server_get(request,
486                                   server_id).to_dict().get('metadata')
487
488    @rest_utils.ajax()
489    def patch(self, request, server_id):
490        """Update metadata items for a server
491
492        http://localhost/api/nova/servers/1/metadata
493        """
494        updated = request.DATA['updated']
495        removed = request.DATA['removed']
496        if updated:
497            api.nova.server_metadata_update(request, server_id, updated)
498        if removed:
499            api.nova.server_metadata_delete(request, server_id, removed)
500
501
502@urls.register
503class Flavors(generic.View):
504    """API for nova flavors."""
505    url_regex = r'nova/flavors/$'
506
507    @rest_utils.ajax()
508    def get(self, request):
509        """Get a list of flavors.
510
511        The listing result is an object with property "items". Each item is
512        a flavor. By default this will return the flavors for the user's
513        current project. If the user is admin, public flavors will also be
514        returned.
515
516        :param is_public: For a regular user, set to True to see all public
517            flavors. For an admin user, set to False to not see public flavors.
518        :param get_extras: Also retrieve the extra specs.
519
520        Example GET:
521        http://localhost/api/nova/flavors?is_public=true
522        """
523        is_public = request.GET.get('is_public')
524        is_public = (is_public and is_public.lower() == 'true')
525        get_extras = request.GET.get('get_extras')
526        get_extras = bool(get_extras and get_extras.lower() == 'true')
527        flavors = api.nova.flavor_list(request, is_public=is_public,
528                                       get_extras=get_extras)
529        flavors = instances_utils.sort_flavor_list(request, flavors,
530                                                   with_menu_label=False)
531        result = {'items': []}
532        for flavor in flavors:
533            d = flavor.to_dict()
534            if get_extras:
535                d['extras'] = flavor.extras
536            result['items'].append(d)
537        return result
538
539    @rest_utils.ajax(data_required=True)
540    def post(self, request):
541        flavor_access = request.DATA.get('flavor_access', [])
542        flavor_id = request.DATA['id']
543        is_public = not flavor_access
544
545        flavor = api.nova.flavor_create(request,
546                                        name=request.DATA['name'],
547                                        memory=request.DATA['ram'],
548                                        vcpu=request.DATA['vcpus'],
549                                        disk=request.DATA['disk'],
550                                        ephemeral=request
551                                        .DATA['OS-FLV-EXT-DATA:ephemeral'],
552                                        swap=request.DATA['swap'],
553                                        flavorid=flavor_id,
554                                        is_public=is_public
555                                        )
556
557        for project in flavor_access:
558            api.nova.add_tenant_to_flavor(
559                request, flavor.id, project.get('id'))
560
561        return rest_utils.CreatedResponse(
562            '/api/nova/flavors/%s' % flavor.id,
563            flavor.to_dict()
564        )
565
566
567@urls.register
568class Flavor(generic.View):
569    """API for retrieving a single flavor"""
570    url_regex = r'nova/flavors/(?P<flavor_id>[^/]+)/$'
571
572    @rest_utils.ajax()
573    def get(self, request, flavor_id):
574        """Get a specific flavor
575
576        :param get_extras: Also retrieve the extra specs.
577
578        Example GET:
579        http://localhost/api/nova/flavors/1
580        """
581        get_extras = self.extract_boolean(request, 'get_extras')
582        get_access_list = self.extract_boolean(request, 'get_access_list')
583        flavor = api.nova.flavor_get(request, flavor_id, get_extras=get_extras)
584
585        result = flavor.to_dict()
586        # Bug: nova API stores and returns empty string when swap equals 0
587        # https://bugs.launchpad.net/nova/+bug/1408954
588        if 'swap' in result and result['swap'] == '':
589            result['swap'] = 0
590        if get_extras:
591            result['extras'] = flavor.extras
592
593        if get_access_list and not flavor.is_public:
594            access_list = [item.tenant_id for item in
595                           api.nova.flavor_access_list(request, flavor_id)]
596            result['access-list'] = access_list
597        return result
598
599    @rest_utils.ajax()
600    def delete(self, request, flavor_id):
601        api.nova.flavor_delete(request, flavor_id)
602
603    @rest_utils.ajax(data_required=True)
604    def patch(self, request, flavor_id):
605        flavor_access = request.DATA.get('flavor_access', [])
606        is_public = not flavor_access
607
608        # Grab any existing extra specs, because flavor edit is currently
609        # implemented as a delete followed by a create.
610        extras_dict = api.nova.flavor_get_extras(request, flavor_id, raw=True)
611        # Mark the existing flavor as deleted.
612        api.nova.flavor_delete(request, flavor_id)
613        # Then create a new flavor with the same name but a new ID.
614        # This is in the same try/except block as the delete call
615        # because if the delete fails the API will error out because
616        # active flavors can't have the same name.
617        flavor = api.nova.flavor_create(request,
618                                        name=request.DATA['name'],
619                                        memory=request.DATA['ram'],
620                                        vcpu=request.DATA['vcpus'],
621                                        disk=request.DATA['disk'],
622                                        ephemeral=request
623                                        .DATA['OS-FLV-EXT-DATA:ephemeral'],
624                                        swap=request.DATA['swap'],
625                                        flavorid=flavor_id,
626                                        is_public=is_public
627                                        )
628        for project in flavor_access:
629            api.nova.add_tenant_to_flavor(
630                request, flavor.id, project.get('id'))
631
632        if extras_dict:
633            api.nova.flavor_extra_set(request, flavor.id, extras_dict)
634
635    def extract_boolean(self, request, name):
636        bool_string = request.GET.get(name)
637        return bool(bool_string and bool_string.lower() == 'true')
638
639
640@urls.register
641class FlavorExtraSpecs(generic.View):
642    """API for managing flavor extra specs"""
643    url_regex = r'nova/flavors/(?P<flavor_id>[^/]+)/extra-specs/$'
644
645    @rest_utils.ajax()
646    def get(self, request, flavor_id):
647        """Get a specific flavor's extra specs
648
649        Example GET:
650        http://localhost/api/nova/flavors/1/extra-specs
651        """
652        return api.nova.flavor_get_extras(request, flavor_id, raw=True)
653
654    @rest_utils.ajax(data_required=True)
655    def patch(self, request, flavor_id):
656        """Update a specific flavor's extra specs.
657
658        This method returns HTTP 204 (no content) on success.
659        """
660        if request.DATA.get('removed'):
661            api.nova.flavor_extra_delete(
662                request, flavor_id, request.DATA.get('removed')
663            )
664        api.nova.flavor_extra_set(
665            request, flavor_id, request.DATA['updated']
666        )
667
668
669@urls.register
670class AggregateExtraSpecs(generic.View):
671    """API for managing aggregate extra specs"""
672    url_regex = r'nova/aggregates/(?P<aggregate_id>[^/]+)/extra-specs/$'
673
674    @rest_utils.ajax()
675    def get(self, request, aggregate_id):
676        """Get a specific aggregate's extra specs
677
678        Example GET:
679        http://localhost/api/nova/flavors/1/extra-specs
680        """
681        return api.nova.aggregate_get(request, aggregate_id).metadata
682
683    @rest_utils.ajax(data_required=True)
684    def patch(self, request, aggregate_id):
685        """Update a specific aggregate's extra specs.
686
687        This method returns HTTP 204 (no content) on success.
688        """
689        updated = request.DATA['updated']
690        if request.DATA.get('removed'):
691            for name in request.DATA.get('removed'):
692                updated[name] = None
693        api.nova.aggregate_set_metadata(request, aggregate_id, updated)
694
695
696@urls.register
697class DefaultQuotaSets(generic.View):
698    """API for getting default quotas for nova"""
699    url_regex = r'nova/quota-sets/defaults/$'
700
701    @rest_utils.ajax()
702    def get(self, request):
703        """Get the values for Nova specific quotas
704
705        Example GET:
706        http://localhost/api/nova/quota-sets/defaults/
707        """
708        if not api.base.is_service_enabled(request, 'compute'):
709            raise rest_utils.AjaxError(501, _('Service Nova is disabled.'))
710
711        quota_set = api.nova.default_quota_get(request,
712                                               request.user.tenant_id)
713
714        disabled_quotas = quotas.get_disabled_quotas(request)
715
716        filtered_quotas = [quota for quota in quota_set
717                           if quota.name not in disabled_quotas]
718
719        result = [{
720            'display_name': quotas.QUOTA_NAMES.get(
721                quota.name,
722                quota.name.replace("_", " ").title()
723            ) + '',
724            'name': quota.name,
725            'limit': quota.limit
726        } for quota in filtered_quotas]
727
728        return {'items': result}
729
730    @rest_utils.ajax(data_required=True)
731    def patch(self, request):
732        """Update the values for Nova specific quotas
733
734        This method returns HTTP 204 (no content) on success.
735        """
736        if api.base.is_service_enabled(request, 'compute'):
737            disabled_quotas = quotas.get_disabled_quotas(request)
738
739            filtered_quotas = [quota for quota in quotas.NOVA_QUOTA_FIELDS
740                               if quota not in disabled_quotas]
741
742            request_data = {
743                key: request.DATA.get(key, None) for key in filtered_quotas
744            }
745
746            nova_data = {key: value for key, value in request_data.items()
747                         if value is not None}
748
749            api.nova.default_quota_update(request, **nova_data)
750        else:
751            raise rest_utils.AjaxError(501, _('Service Nova is disabled.'))
752
753
754@urls.register
755class EditableQuotaSets(generic.View):
756    """API for editable quotas."""
757    url_regex = r'nova/quota-sets/editable/$'
758
759    @rest_utils.ajax()
760    def get(self, request):
761        """Get a list of editable quota fields.
762
763        The listing result is an object with property "items". Each item
764        is an editable quota. Returns an empty list in case no editable
765        quota is found.
766        """
767        disabled_quotas = quotas.get_disabled_quotas(request)
768        editable_quotas = [quota for quota in quotas.QUOTA_FIELDS
769                           if quota not in disabled_quotas]
770        return {'items': editable_quotas}
771
772
773@urls.register
774class QuotaSets(generic.View):
775    """API for setting quotas for a given project."""
776    url_regex = r'nova/quota-sets/(?P<project_id>[0-9a-f]+)$'
777
778    @rest_utils.ajax(data_required=True)
779    def patch(self, request, project_id):
780        """Update a single project quota data.
781
782        The PATCH data should be an application/json object with the
783        attributes to set to new quota values.
784
785        This method returns HTTP 204 (no content) on success.
786        """
787        disabled_quotas = quotas.get_disabled_quotas(request)
788
789        if api.base.is_service_enabled(request, 'compute'):
790            nova_data = {
791                key: request.DATA[key] for key in quotas.NOVA_QUOTA_FIELDS
792                if key not in disabled_quotas
793            }
794
795            api.nova.tenant_quota_update(request, project_id, **nova_data)
796        else:
797            raise rest_utils.AjaxError(501, _('Service Nova is disabled.'))
798