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