# Copyright 2014, Rackspace, US, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""API over the nova service.
"""
from django.http import HttpResponse
from django.template.defaultfilters import slugify
from django.utils import http as utils_http
from django.views import generic
from novaclient import exceptions
from openstack_dashboard import api
from openstack_dashboard.api.rest import json_encoder
from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
@urls.register
[docs]class Keypairs(generic.View):
"""API for nova keypairs.
"""
url_regex = r'nova/keypairs/$'
@rest_utils.ajax()
[docs] def get(self, request):
"""Get a list of keypairs associated with the current logged-in
account.
The listing result is an object with property "items".
"""
result = api.nova.keypair_list(request)
return {'items': [u.to_dict() for u in result]}
@rest_utils.ajax(data_required=True)
[docs] def post(self, request):
"""Create a keypair.
Create a keypair using the parameters supplied in the POST
application/json object. The parameters are:
:param name: the name to give the keypair
:param public_key: (optional) a key to import
This returns the new keypair object on success.
"""
if 'public_key' in request.DATA:
new = api.nova.keypair_import(request, request.DATA['name'],
request.DATA['public_key'])
else:
new = api.nova.keypair_create(request, request.DATA['name'])
return rest_utils.CreatedResponse(
'/api/nova/keypairs/%s' % utils_http.urlquote(new.name),
new.to_dict()
)
@urls.register
[docs]class Keypair(generic.View):
url_regex = r'nova/keypairs/(?P<keypair_name>.+)/$'
[docs] def get(self, request, keypair_name):
"""Creates a new keypair and associates it to the current project.
* Since the response for this endpoint creates a new keypair and
is not idempotent, it normally would be represented by a POST HTTP
request. However, this solution was adopted as it
would support automatic file download across browsers.
:param keypair_name: the name to associate the keypair to
:param regenerate: (optional) if set to the string 'true',
replaces the existing keypair with a new keypair
This returns the new keypair object on success.
"""
try:
regenerate = request.GET.get('regenerate') == 'true'
if regenerate:
api.nova.keypair_delete(request, keypair_name)
keypair = api.nova.keypair_create(request, keypair_name)
except exceptions.Conflict:
return HttpResponse(status=409)
except Exception:
return HttpResponse(status=500)
else:
response = HttpResponse(content_type='application/binary')
response['Content-Disposition'] = ('attachment; filename=%s.pem'
% slugify(keypair_name))
response.write(keypair.private_key)
response['Content-Length'] = str(len(response.content))
return response
@urls.register
[docs]class Services(generic.View):
"""API for nova services.
"""
url_regex = r'nova/services/$'
@rest_utils.ajax()
[docs] def get(self, request):
"""Get a list of nova services.
Will return HTTP 501 status code if the service_list extension is
not supported.
"""
if api.base.is_service_enabled(request, 'compute') \
and api.nova.extension_supported('Services', request):
result = api.nova.service_list(request)
return {'items': [u.to_dict() for u in result]}
else:
raise rest_utils.AjaxError(501, '')
@urls.register
[docs]class AvailabilityZones(generic.View):
"""API for nova availability zones.
"""
url_regex = r'nova/availzones/$'
@rest_utils.ajax()
[docs] def get(self, request):
"""Get a list of availability zones.
The following get parameters may be passed in the GET
request:
:param detailed: If this equals "true" then the result will
include more detail.
The listing result is an object with property "items".
"""
detailed = request.GET.get('detailed') == 'true'
result = api.nova.availability_zone_list(request, detailed)
return {'items': [u.to_dict() for u in result]}
@urls.register
[docs]class Limits(generic.View):
"""API for nova limits.
"""
url_regex = r'nova/limits/$'
@rest_utils.ajax(json_encoder=json_encoder.NaNJSONEncoder)
[docs] def get(self, request):
"""Get an object describing the current project limits.
Note: the Horizon API doesn't support any other project (tenant) but
the underlying client does...
The following get parameters may be passed in the GET
request:
:param reserved: This may be set to "true" but it's not
clear what the result of that is.
The result is an object with limits as properties.
"""
reserved = request.GET.get('reserved') == 'true'
result = api.nova.tenant_absolute_limits(request, reserved)
return result
@urls.register
[docs]class Servers(generic.View):
"""API over all servers.
"""
url_regex = r'nova/servers/$'
_optional_create = [
'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
'config_drive'
]
@rest_utils.ajax()
[docs] def get(self, request):
"""Get a list of servers.
The listing result is an object with property "items". Each item is
a server.
Example GET:
http://localhost/api/nova/servers
"""
servers = api.nova.server_list(request)[0]
return {'items': [s.to_dict() for s in servers]}
@rest_utils.ajax(data_required=True)
[docs] def post(self, request):
"""Create a server.
Create a server using the parameters supplied in the POST
application/json object. The required parameters as specified by
the underlying novaclient are:
:param name: The new server name.
:param source_id: The ID of the image to use.
:param flavor_id: The ID of the flavor to use.
:param key_name: (optional extension) name of previously created
keypair to inject into the instance.
:param user_data: user data to pass to be exposed by the metadata
server this can be a file type object as well or a
string.
:param security_groups: An array of one or more objects with a "name"
attribute.
Other parameters are accepted as per the underlying novaclient:
"block_device_mapping", "block_device_mapping_v2", "nics", "meta",
"availability_zone", "instance_count", "admin_pass", "disk_config",
"config_drive"
This returns the new server object on success.
"""
try:
args = (
request,
request.DATA['name'],
request.DATA['source_id'],
request.DATA['flavor_id'],
request.DATA['key_name'],
request.DATA['user_data'],
request.DATA['security_groups'],
)
except KeyError as e:
raise rest_utils.AjaxError(400, 'missing required parameter '
"'%s'" % e.args[0])
kw = {}
for name in self._optional_create:
if name in request.DATA:
kw[name] = request.DATA[name]
new = api.nova.server_create(*args, **kw)
return rest_utils.CreatedResponse(
'/api/nova/servers/%s' % utils_http.urlquote(new.id),
new.to_dict()
)
@urls.register
[docs]class Server(generic.View):
"""API for retrieving a single server
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+|default)$'
@rest_utils.ajax()
[docs] def get(self, request, server_id):
"""Get a specific server
http://localhost/api/nova/servers/1
"""
return api.nova.server_get(request, server_id).to_dict()
@urls.register
@urls.register
[docs]class Extensions(generic.View):
"""API for nova extensions.
"""
url_regex = r'nova/extensions/$'
@rest_utils.ajax()
[docs] def get(self, request):
"""Get a list of extensions.
The listing result is an object with property "items". Each item is
an image.
Example GET:
http://localhost/api/nova/extensions
"""
result = api.nova.list_extensions(request)
return {'items': [e.to_dict() for e in result]}
@urls.register
[docs]class Flavors(generic.View):
"""API for nova flavors.
"""
url_regex = r'nova/flavors/$'
@rest_utils.ajax()
[docs] def get(self, request):
"""Get a list of flavors.
The listing result is an object with property "items". Each item is
a flavor. By default this will return the flavors for the user's
current project. If the user is admin, public flavors will also be
returned.
:param is_public: For a regular user, set to True to see all public
flavors. For an admin user, set to False to not see public flavors.
:param get_extras: Also retrieve the extra specs.
Example GET:
http://localhost/api/nova/flavors?is_public=true
"""
is_public = request.GET.get('is_public')
is_public = (is_public and is_public.lower() == 'true')
get_extras = request.GET.get('get_extras')
get_extras = bool(get_extras and get_extras.lower() == 'true')
flavors = api.nova.flavor_list(request, is_public=is_public,
get_extras=get_extras)
result = {'items': []}
for flavor in flavors:
d = flavor.to_dict()
if get_extras:
d['extras'] = flavor.extras
result['items'].append(d)
return result
@rest_utils.ajax(data_required=True)
[docs] def post(self, request):
flavor_access = request.DATA.get('flavor_access', [])
flavor_id = request.DATA['id']
is_public = not flavor_access
flavor = api.nova.flavor_create(request,
name=request.DATA['name'],
memory=request.DATA['ram'],
vcpu=request.DATA['vcpus'],
disk=request.DATA['disk'],
ephemeral=request
.DATA['OS-FLV-EXT-DATA:ephemeral'],
swap=request.DATA['swap'],
flavorid=flavor_id,
is_public=is_public
)
for project in flavor_access:
api.nova.add_tenant_to_flavor(
request, flavor.id, project.get('id'))
return rest_utils.CreatedResponse(
'/api/nova/flavors/%s' % flavor.id,
flavor.to_dict()
)
@urls.register
[docs]class Flavor(generic.View):
"""API for retrieving a single flavor
"""
url_regex = r'nova/flavors/(?P<flavor_id>[^/]+)/$'
@rest_utils.ajax()
[docs] def get(self, request, flavor_id):
"""Get a specific flavor
:param get_extras: Also retrieve the extra specs.
Example GET:
http://localhost/api/nova/flavors/1
"""
get_extras = self.extract_boolean(request, 'get_extras')
get_access_list = self.extract_boolean(request, 'get_access_list')
flavor = api.nova.flavor_get(request, flavor_id, get_extras=get_extras)
result = flavor.to_dict()
# Bug: nova API stores and returns empty string when swap equals 0
# https://bugs.launchpad.net/nova/+bug/1408954
if 'swap' in result and result['swap'] == '':
result['swap'] = 0
if get_extras:
result['extras'] = flavor.extras
if get_access_list and not flavor.is_public:
access_list = [item.tenant_id for item in
api.nova.flavor_access_list(request, flavor_id)]
result['access-list'] = access_list
return result
@rest_utils.ajax()
[docs] def delete(self, request, flavor_id):
api.nova.flavor_delete(request, flavor_id)
@rest_utils.ajax(data_required=True)
[docs] def patch(self, request, flavor_id):
flavor_access = request.DATA.get('flavor_access', [])
is_public = not flavor_access
# Grab any existing extra specs, because flavor edit is currently
# implemented as a delete followed by a create.
extras_dict = api.nova.flavor_get_extras(request, flavor_id, raw=True)
# Mark the existing flavor as deleted.
api.nova.flavor_delete(request, flavor_id)
# Then create a new flavor with the same name but a new ID.
# This is in the same try/except block as the delete call
# because if the delete fails the API will error out because
# active flavors can't have the same name.
flavor = api.nova.flavor_create(request,
name=request.DATA['name'],
memory=request.DATA['ram'],
vcpu=request.DATA['vcpus'],
disk=request.DATA['disk'],
ephemeral=request
.DATA['OS-FLV-EXT-DATA:ephemeral'],
swap=request.DATA['swap'],
flavorid=flavor_id,
is_public=is_public
)
for project in flavor_access:
api.nova.add_tenant_to_flavor(
request, flavor.id, project.get('id'))
if extras_dict:
api.nova.flavor_extra_set(request, flavor.id, extras_dict)
@urls.register
@urls.register