powervc-driver/common-powervc/powervc/common/client/extensions/nova.py

565 lines
22 KiB
Python

# Copyright 2014 IBM Corp.
import six
import urllib
from novaclient import base as client_base
from novaclient import exceptions
from novaclient.v1_1 import servers
from novaclient.v1_1 import hypervisors
from novaclient.v1_1 import images
from novaclient.v1_1 import flavors
from novaclient.v1_1 import volumes
from novaclient.v1_1.volume_types import VolumeType
from powervc.common.client.extensions import base
from powervc.common.gettextutils import _
from powervc.common import utils
from webob import exc
import logging
LOG = logging.getLogger(__name__)
class Client(base.ClientExtension):
def __init__(self, client):
super(Client, self).__init__(client)
self.manager = PVCServerManager(client)
self.hypervisors = PVCHypervisorManager(client)
self.images = images.ImageManager(client)
self.flavors = flavors.FlavorManager(client)
self.storage_connectivity_groups = \
StorageConnectivityGroupManager(client)
self.volumes = volumes.VolumeManager(client)
self.scg_images = SCGImageManager(client)
# any extensions to std nova client go below
class PVCHypervisorManager(hypervisors.HypervisorManager):
"""
This HypervisorManager class is specific for extending PowerVC driver
feature to get/set the hypervisor status and maintenance mode.
"""
def get_host_maintenance_mode(self, hostname):
"""Get host maintenance mode by host name from PowerVC driver
"""
# If cannot find hypervisor by hostname, will raise
# itemNotFoundException from novaclient, just raise
# to upper layer to handle.
hypervisors = self.search(hostname)
if not hypervisors[0] or not self.get(hypervisors[0]):
raise exc.HTTPNotFound(_("No hypervisor matching '%s' could be"
" found.") % hostname)
try:
hypervisor = self.get(hypervisors[0])
except Exception as ex:
raise exc.HTTPNotFound(explanation=six.text_type(ex))
# Either "ok" (maintenance off), "entering", "on" or "error"
# compatible with previous powervc version, if no such property
# set as "ok"
maintenance_status = getattr(hypervisor, "maintenance_status", "ok")
# Either the empty string (i.e., not in maintenance),
# "none": dont migrate anything
# "active-only": migrate active-only vm
# "all": migrate all vm
maintenance_migration_action = \
getattr(hypervisor, "maintenance_migration_action", "none")
return {"maintenance_status": maintenance_status,
"maintenance_migration_action": maintenance_migration_action}
def update_host_maintenance_mode(self, hostname, enabled, migrate=None,
target_host=None):
"""Update host maintenance mode status.
:hostname: The hostname of the hypervisor
:enabled: should be "enable" or "disable"
:migrate: should be
"none", do not migrate any vm
"active-only", migrate only active vm
"all", migrate all vm
"""
# Refer to PowerVC HLD host maintenance mode chapter
url = "/ego/prs/hypervisor_maintenance/%s" % hostname
if not migrate:
body = {"status": enabled}
else:
if target_host:
body = {"status": enabled,
"migrate": migrate,
"target_host": target_host}
else:
body = {"status": enabled,
"migrate": migrate}
# send set maintenance mode request by put http method
try:
_resp, resp_body = self.api.client.put(url, body=body)
except Exception as ex:
raise exc.HTTPBadRequest(explanation=six.text_type(ex))
# check response content
if "hypervisor_maintenance" not in resp_body:
raise exceptions.NotFound(_("response body doesn't contain "
"maintenance status info for %s.")
% hostname)
return resp_body
class PVCServerManager(servers.ServerManager):
"""
This ServerManager class is specific for PowerVC booting a VM.
As the PowerVC boot API does not follow the standard openstack boot API,
need to rewrite the default boot method to satisfy powerVC boot restAPI
content.
"""
def list(self, detailed=True, search_opts=None,
scgUUID=None,
scgName=None):
"""
Get a list of the Servers that filtered by a specified SCG UUID
or SCG name, if both SCG UUID and SCG name are specified, UUID has the
high priority to check.
:rtype: list of :class:`Server`
"""
if scgUUID or scgName:
return utils.get_utils().get_scg_accessible_servers(scgUUID,
scgName,
detailed,
search_opts
)
else:
# This will get all scgs accessible servers
return utils.get_utils().\
get_multi_scg_accessible_servers(None,
None,
detailed,
search_opts
)
def list_all_servers(self, detailed=True, search_opts=None):
"""
Get a list of all servers without filters.
Optional detailed returns details server info.
Optional reservation_id only returns instances with that
reservation_id.
:rtype: list of :class:`Server`
"""
if search_opts is None:
search_opts = {}
qparams = {}
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
return self._list("/servers%s%s" % (detail, query_string), "servers")
# This function was copied from (/usr/lib/python2.6/site-packages/
# novaclient/v1_1/servers.py) before, but changes needed when activation
# data contains userdata and files, because in a boot action, local OS
# novaclient's _boot will read them from CLI or GUI firstly, then when our
# driver is triggered, this version of _boot should just forward the data
# or file content to PowerVC without any reading, otherwise error happens.
# RTC/172018, add support to boot server with activation data.
def _boot(self, resource_url, response_key, name, image, flavor,
meta=None, files=None, userdata=None, reservation_id=None,
return_raw=False, min_count=None, max_count=None,
security_groups=None, key_name=None, availability_zone=None,
block_device_mapping=None, nics=None, scheduler_hints=None,
config_drive=None, admin_pass=None, **kwargs):
"""Create (boot) a new server.
:param name: Server Name.
:param image: The string of PowerVC `Image` UUID to boot with.
:param flavor: The :dict of `Flavor` that need to boot onto.
:param meta: A dict of arbitrary key/value metadata to store for this
server. A maximum of five entries is allowed, and both
keys and values must be 255 characters or less.
:param files: A dict of files to overrwrite on the server upon boot.
Keys are file names (i.e. ``/etc/passwd``) and values
are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed,
and each file must be 10k or less.
:param userdata: user data to pass to make config drive this can be a
file type object as well or a string. PowerVC don't use
metadata server for security considerations.
:param reservation_id: a UUID for the set of servers being requested.
:param return_raw: If True, don't try to coearse the result into
a Resource object.
:param security_groups: list of security group names
:param key_name: (optional extension) name of keypair to inject into
the instance
:param availability_zone: Name of the availability zone for instance
placement.
:param block_device_mapping: A dict of block device mappings for this
server.
:param nics: (optional extension) an ordered list of nics to be
added to this server, with information about
connected networks, fixed ips, etc.
:param scheduler_hints: (optional extension) arbitrary key-value pairs
specified by the client to help boot an instance.
:param config_drive: (optional extension) value for config drive
either boolean, or volume-id
:param admin_pass: admin password for the server.
"""
body = {"server": {
"name": name,
"imageRef": image,
"flavor": {},
}}
# Add the flavor information to PowerVC for booting VM
body["server"]["flavor"]['ram'] = flavor['memory_mb']
body["server"]["flavor"]['vcpus'] = flavor['vcpus']
body["server"]["flavor"]['disk'] = flavor['root_gb']
body["server"]["flavor"]['OS-FLV-EXT-DATA:ephemeral'] = \
flavor.get('OS-FLV-EXT-DATA:ephemeral', 0)
body["server"]["flavor"]['extra_specs'] = flavor['extra_specs']
# If hypervisor ID specified:
if kwargs.get("hypervisor", None):
body["server"]['hypervisor_hostname'] = kwargs["hypervisor"]
if userdata:
# RTC/172018 -- start
# comment out the following, already done by local OS nova client
# if hasattr(userdata, 'read'):
# userdata = userdata.read()
# userdata = strutils.safe_encode(userdata)
# body["server"]["user_data"] = base64.b64encode(userdata)
body["server"]["user_data"] = userdata
# RTC/172018 -- end
if meta:
body["server"]["metadata"] = meta
if reservation_id:
body["server"]["reservation_id"] = reservation_id
if key_name:
body["server"]["key_name"] = key_name
if scheduler_hints:
body['os:scheduler_hints'] = scheduler_hints
if config_drive:
body["server"]["config_drive"] = config_drive
if admin_pass:
body["server"]["adminPass"] = admin_pass
if not min_count:
min_count = 1
if not max_count:
max_count = min_count
body["server"]["min_count"] = min_count
body["server"]["max_count"] = max_count
if security_groups:
body["server"]["security_groups"] = ([{'name': sg}
for sg in security_groups])
# Files are a slight bit tricky. They're passed in a "personality"
# list to the POST. Each item is a dict giving a file name and the
# base64-encoded contents of the file. We want to allow passing
# either an open file *or* some contents as files here.
if files:
personality = body['server']['personality'] = []
# RTC/172018 -- start
# comment out the following, already done by local OS nova client
# for filepath, file_or_string in files.items():
# if hasattr(file_or_string, 'read'):
# data = file_or_string.read()
# else:
# data = file_or_string
for file in files:
personality.append({
'path': file[0],
'contents': file[1].encode('base64'),
})
# RTC/172018 -- end
if availability_zone:
body["server"]["availability_zone"] = availability_zone
# Block device mappings are passed as a list of dictionaries
if block_device_mapping:
bdm = body['server']['block_device_mapping'] = []
for device_name, mapping in block_device_mapping.items():
#
# The mapping is in the format:
# <id>:[<type>]:[<size(GB)>]:[<delete_on_terminate>]
#
bdm_dict = {'device_name': device_name}
mapping_parts = mapping.split(':')
id_ = mapping_parts[0]
if len(mapping_parts) == 1:
bdm_dict['volume_id'] = id_
if len(mapping_parts) > 1:
type_ = mapping_parts[1]
if type_.startswith('snap'):
bdm_dict['snapshot_id'] = id_
else:
bdm_dict['volume_id'] = id_
if len(mapping_parts) > 2:
bdm_dict['volume_size'] = mapping_parts[2]
if len(mapping_parts) > 3:
bdm_dict['delete_on_termination'] = mapping_parts[3]
bdm.append(bdm_dict)
if nics is not None:
# NOTE(tr3buchet): nics can be an empty list
all_net_data = []
for nic_info in nics:
net_data = {}
# if value is empty string, do not send value in body
if nic_info.get('net-id'):
net_data['uuid'] = nic_info['net-id']
if nic_info.get('v4-fixed-ip'):
net_data['fixed_ip'] = nic_info['v4-fixed-ip']
if nic_info.get('port-id'):
net_data['port'] = nic_info['port-id']
all_net_data.append(net_data)
body['server']['networks'] = all_net_data
return self._create(resource_url, body, response_key,
return_raw=return_raw, **kwargs)
def _resize_pvc(self, server, info, **kwargs):
"""
This method is used to overwrite the resize in the
class ServerManager
"""
return self._action('resize', server, info=info, **kwargs)
def list_instance_storage_viable_hosts(self, server):
"""
Get a list of hosts compatible with this server.
Used for getting candidate host hypervisors from powervc for
live migration. We need to do things a bit different
since there not a common schema apperently for the content
returned. See below..
{
"8233E8B_100008P":{
"host":"8233E8B_100008P"
},
"8233E8B_100043P":{
"host":"8233E8B_100043P"
}
}
:param server: ID of the :class:`Server` to get.
:rtype: dict
"""
url = "/storage-viable-hosts?instance_uuid=%s"\
% (client_base.getid(server))
_resp, body = self.api.client.get(url)
return body
def set_host_maintenance_mode(self, host, mode):
url = "/ego/prs/hypervisor_maintenance/%s" % host
if mode:
status = "enable"
else:
status = "disable"
body = {"status": status,
"migrate": "none"}
return self._update(url, body)
class StorageConnectivityGroup(client_base.Resource):
"""
Entity class for StorageConnectivityGroup
"""
def __repr__(self):
return ("<StorageConnectivityGroup: %s, displayname: %s>" %
(self.id, self.display_name))
def list_all_volumes(self):
"""
Get a list of accessible volume for this SCG.
:rtype: list of :class:`Volume`
"""
return self.manager.list_all_volumes(self.id)
def list_all_volume_types(self):
"""
Get a list of accessible volume types for this SCG.
:rtype: list of :class:`VolumeType`
"""
return self.manager.list_all_volume_types(self.id)
class StorageConnectivityGroupManager(client_base.Manager):
"""
Manager class for StorageConnectivityGroup
Currently get and list functions for StorageConnectivityGroup
are implemented.
"""
resource_class = StorageConnectivityGroup
def get(self, scgUUID):
"""
Get a StorageConnectivityGroup.
:param server: UUID `StorageConnectivityGroup` to get.
:rtype: :class:`Server`
"""
try:
return self._get("/storage-connectivity-groups/%s" % scgUUID,
"storage_connectivity_group")
except Exception as e:
# If PowerVC Express installations in IVM mode
# would receive BadRequest
LOG.error('A problem was encountered while getting the '
' Storage Connectivity Group %s: %s '
% (scgUUID, str(e)))
return None
def list_for_image(self, imageUUID):
"""
Get a list of StorageConnectivityGroups for the specified image. If
an error occurs getting the SCGs for an image, an exception is logged
and raised.
:param: imageUUID The image UUID:
:rtype: list of :class:`StorageConnectivityGroup`
"""
try:
return self._list("/images/%s/storage-connectivity-groups" %
imageUUID, "storage_connectivity_groups")
except Exception as e:
LOG.error('A problem was encountered while getting a list of '
'Storage Connectivity Groups for image %s: %s '
% (imageUUID, str(e)))
raise e
def list_all_volumes(self, scgUUID):
"""
Get a list of accessible volume for this SCG.
:rtype: list of :class:`Volume`
"""
try:
return self._list("/storage-connectivity-groups/%s/volumes"
% scgUUID, "volumes", volumes.Volume)
except Exception as e:
LOG.error('A problem was encountered while getting a list of '
'accessible volumes for scg %s: %s '
% (scgUUID, str(e)))
raise e
def list_all_volume_types(self, scgUUID):
"""
Get a list of accessible volume types for this SCG.
:rtype: list of :class:`VolumeType`
"""
try:
return self._list("/storage-connectivity-groups/%s/volume-types"
% scgUUID, "volume-types", VolumeType)
except Exception as e:
LOG.error('A problem was encountered while getting a list of '
'accessible volume types for scg %s: %s '
% (scgUUID, str(e)))
raise e
def list(self, detailed=True, search_opts=None):
"""
Get a list of StorageConnectivityGroups.
Optional detailed returns details StorageConnectivityGroup info.
:rtype: list of :class:`StorageConnectivityGroup`
"""
if search_opts is None:
search_opts = {}
qparams = {}
for opt, val in six.iteritems(search_opts):
if val:
qparams[opt] = val
query_string = "?%s" % urllib.urlencode(qparams) if qparams else ""
detail = ""
if detailed:
detail = "/detail"
try:
return self._list("/storage-connectivity-groups%s%s" %
(detail, query_string),
"storage_connectivity_groups")
except Exception as e:
# If PowerVC Express installations in IVM mode
# would receive BadRequest
LOG.error('A problem was encountered while getting a list'
' of Storage Connectivity Groups: %s '
% str(e))
return []
class SCGImage(client_base.Resource):
"""
Entity class for SCGImage
"""
def __repr__(self):
return ("<SCGImage: %s, name: %s>" %
(self.id, self.name))
class SCGImageManager(client_base.Manager):
"""
Manager class for SCGImage
Currently the list function for SCGImages in a StorageConnectivityGroup,
and the image identifiers of SCGImages in a StorageConnectivityGroup is
implemented.
"""
resource_class = SCGImage
def list(self, scgUUID):
"""
Get a list of SCGImages for the specified StorageConnectivityGroup. If
an error occurs getting the SCGImages, and exception is logged and
raised.
:param: scgUUID The StorageConnectivityGroup UUID:
:rtype: list of :class:`SCGImage`
"""
try:
return self._list("/storage-connectivity-groups/%s/images" %
scgUUID, "images")
except Exception as e:
LOG.error('A problem was encountered while getting a list of '
'images for Storage Connectivity Group \'%s\': %s '
% (scgUUID, str(e)))
raise e
def list_ids(self, scgUUID):
"""
Get a list of SCGImage identifiers for the specified
StorageConnectivityGroup. If an error occurs getting the SCGImage ids,
and exception is logged and raised.
:param: scgUUID The StorageConnectivityGroup UUID:
:rtype: list of :class:`SCGImage` identifiers
"""
ids = []
SCGImages = self.list(scgUUID)
if SCGImages:
for image in SCGImages:
ids.append(image.id)
return ids