Files
python-troveclient/troveclient/v1/instances.py
Morgan Jones ea0e472edd Fix troveclient to support Mistral
Mistral gets confused by Trove's (aguably wrong) inclusion
of a member called 'items' in the Pagenated object that
Trove returns as the result of 'list' client methods.

This change changes Pagenated to inherit from the 'list'
class so that the items method is not required (and has
the additional benefit of just generally being a better
implementation of a list type result).

Change-Id: I683120451f69f07f131e6fa422c082f85735b196
Closes-bug: 1585705
Depends-On: Id674ae57bfcdc5e09bde1e323a614b3a03a7cad3
2016-05-26 16:14:37 -04:00

491 lines
19 KiB
Python

# Copyright 2011 OpenStack Foundation
# Copyright 2013 Rackspace Hosting
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# 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.
import os
import warnings
from troveclient import base
from troveclient import common
from troveclient import exceptions
from troveclient.i18n import _LW
from troveclient import utils
from troveclient.v1 import modules as core_modules
from swiftclient import client as swift_client
REBOOT_SOFT = 'SOFT'
REBOOT_HARD = 'HARD'
class Instance(base.Resource):
"""An Instance is an opaque instance used to store Database instances."""
def list_databases(self):
return self.manager.databases.list(self)
def delete(self):
"""Delete the instance."""
self.manager.delete(self)
def restart(self):
"""Restart the database instance."""
self.manager.restart(self.id)
def detach_replica(self):
"""Stops the replica database from being replicated to."""
self.manager.edit(self.id, detach_replica_source=True)
class DatastoreLog(base.Resource):
"""A DatastoreLog is a log on the database guest instance."""
def __repr__(self):
return "<DatastoreLog: %s>" % self.name
class Instances(base.ManagerWithFind):
"""Manage :class:`Instance` resources."""
resource_class = Instance
def _get_swift_client(self):
if hasattr(self.api.client, 'auth'):
auth_url = self.api.client.auth.auth_url
user = self.api.client.auth._username
key = self.api.client.auth._password
tenant_name = self.api.client.auth._project_name
else:
auth_url = self.api.client.auth_url
user = self.api.client.username
key = self.api.client.password
tenant_name = self.api.client.tenant
# remove '/tokens' from the end of auth_url so it works for swift
token_str = "/tokens"
if auth_url.endswith(token_str):
auth_url = auth_url[:-len(token_str)]
region_name = self.api.client.region_name
os_options = {'tenant_name': tenant_name, 'region_name': region_name}
return swift_client.Connection(
auth_url, user, key, auth_version="2.0", os_options=os_options)
# TODO(mriedem): Remove slave_of after liberty-eol for Trove.
def create(self, name, flavor_id, volume=None, databases=None, users=None,
restorePoint=None, availability_zone=None, datastore=None,
datastore_version=None, nics=None, configuration=None,
replica_of=None, slave_of=None, replica_count=None,
modules=None):
"""Create (boot) a new instance."""
body = {"instance": {
"name": name,
"flavorRef": flavor_id
}}
datastore_obj = {}
if volume:
body["instance"]["volume"] = volume
if databases:
body["instance"]["databases"] = databases
if users:
body["instance"]["users"] = users
if restorePoint:
body["instance"]["restorePoint"] = restorePoint
if availability_zone:
body["instance"]["availability_zone"] = availability_zone
if datastore:
datastore_obj["type"] = datastore
if datastore_version:
datastore_obj["version"] = datastore_version
if datastore_obj:
body["instance"]["datastore"] = datastore_obj
if nics:
body["instance"]["nics"] = nics
if configuration:
body["instance"]["configuration"] = base.getid(configuration)
if replica_of or slave_of:
if slave_of:
warnings.warn(_LW("The 'slave_of' argument is deprecated in "
"favor of 'replica_of' and will be removed "
"after the Trove liberty series is end of "
"life."),
category=DeprecationWarning)
body["instance"]["replica_of"] = base.getid(replica_of) or slave_of
if replica_count:
body["instance"]["replica_count"] = replica_count
if modules:
body["instance"]["modules"] = self._get_module_list(modules)
return self._create("/instances", body, "instance")
def modify(self, instance, configuration=None):
body = {
"instance": {
}
}
if configuration is not None:
body["instance"]["configuration"] = base.getid(configuration)
url = "/instances/%s" % base.getid(instance)
resp, body = self.api.client.put(url, body=body)
common.check_for_exceptions(resp, body, url)
def edit(self, instance, configuration=None, name=None,
detach_replica_source=False, remove_configuration=False):
body = {
"instance": {
}
}
if configuration and remove_configuration:
raise Exception("Cannot attach and detach configuration "
"simultaneously.")
if remove_configuration:
body["instance"]["configuration"] = None
if configuration is not None:
body["instance"]["configuration"] = base.getid(configuration)
if name is not None:
body["instance"]["name"] = name
if detach_replica_source:
# NOTE(mriedem): We don't send slave_of since it was removed from
# the trove API in mitaka.
body["instance"]["replica_of"] = None
url = "/instances/%s" % base.getid(instance)
resp, body = self.api.client.patch(url, body=body)
common.check_for_exceptions(resp, body, url)
def list(self, limit=None, marker=None, include_clustered=False):
"""Get a list of all instances.
:rtype: list of :class:`Instance`.
"""
return self._paginated("/instances", "instances", limit, marker,
{"include_clustered": include_clustered})
def get(self, instance):
"""Get a specific instances.
:rtype: :class:`Instance`
"""
return self._get("/instances/%s" % base.getid(instance),
"instance")
def backups(self, instance, limit=None, marker=None):
"""Get the list of backups for a specific instance.
:param instance: instance for which to list backups
:param limit: max items to return
:param marker: marker start point
:rtype: list of :class:`Backups`.
"""
url = "/instances/%s/backups" % base.getid(instance)
return self._paginated(url, "backups", limit, marker)
def delete(self, instance):
"""Delete the specified instance.
:param instance: A reference to the instance to delete
"""
url = "/instances/%s" % base.getid(instance)
resp, body = self.api.client.delete(url)
common.check_for_exceptions(resp, body, url)
def _action(self, instance, body):
"""Perform a server "action" -- reboot/rebuild/resize/etc."""
url = "/instances/%s/action" % base.getid(instance)
resp, body = self.api.client.post(url, body=body)
common.check_for_exceptions(resp, body, url)
if body:
return self.resource_class(self, body, loaded=True)
return body
def resize_volume(self, instance, volume_size):
"""Resize the volume on an existing instances."""
body = {"resize": {"volume": {"size": volume_size}}}
self._action(instance, body)
def resize_instance(self, instance, flavor_id):
"""Resizes an instance with a new flavor."""
body = {"resize": {"flavorRef": flavor_id}}
self._action(instance, body)
def restart(self, instance):
"""Restart the database instance.
:param instance: The :class:`Instance` (or its ID) of the database
instance to restart.
"""
body = {'restart': {}}
self._action(instance, body)
def configuration(self, instance):
"""Get a configuration on instances.
:rtype: :class:`Instance`
"""
return self._get("/instances/%s/configuration" % base.getid(instance),
"instance")
def promote_to_replica_source(self, instance):
"""Promote a replica to be the new replica_source of its set
:param instance: The :class:`Instance` (or its ID) of the database
instance to promote.
"""
body = {'promote_to_replica_source': {}}
self._action(instance, body)
def eject_replica_source(self, instance):
"""Eject a replica source from its set
:param instance: The :class:`Instance` (or its ID) of the database
instance to eject.
"""
body = {'eject_replica_source': {}}
self._action(instance, body)
def modules(self, instance):
"""Get the list of modules for a specific instance."""
return self._modules_get(instance)
def module_query(self, instance):
"""Query an instance about installed modules."""
return self._modules_get(instance, from_guest=True)
def module_retrieve(self, instance, directory=None, prefix=None):
"""Retrieve the module data file from an instance. This includes
the contents of the module data file.
"""
if directory:
try:
os.makedirs(directory, exist_ok=True)
except TypeError:
# py27
try:
os.makedirs(directory)
except OSError:
if not os.path.isdir(directory):
raise
else:
directory = '.'
prefix = prefix or ''
if prefix and not prefix.endswith('_'):
prefix += '_'
module_list = self._modules_get(
instance, from_guest=True, include_contents=True)
saved_modules = {}
for module in module_list:
filename = '%s%s_%s_%s.dat' % (prefix, module.name,
module.datastore,
module.datastore_version)
full_filename = os.path.expanduser(
os.path.join(directory, filename))
with open(full_filename, 'wb') as fh:
fh.write(utils.decode_data(module.contents))
saved_modules[module.name] = full_filename
return saved_modules
def _modules_get(self, instance, from_guest=None, include_contents=None):
url = "/instances/%s/modules" % base.getid(instance)
query_strings = {}
if from_guest is not None:
query_strings["from_guest"] = from_guest
if include_contents is not None:
query_strings["include_contents"] = include_contents
url = common.append_query_strings(url, **query_strings)
resp, body = self.api.client.get(url)
common.check_for_exceptions(resp, body, url)
return [core_modules.Module(self, module, loaded=True)
for module in body['modules']]
def module_apply(self, instance, modules):
"""Apply modules to an instance."""
url = "/instances/%s/modules" % base.getid(instance)
body = {"modules": self._get_module_list(modules)}
resp, body = self.api.client.post(url, body=body)
common.check_for_exceptions(resp, body, url)
return [core_modules.Module(self, module, loaded=True)
for module in body['modules']]
def _get_module_list(self, modules):
"""Build a list of module ids."""
module_list = []
for module in modules:
module_info = {'id': base.getid(module)}
module_list.append(module_info)
return module_list
def module_remove(self, instance, module):
"""Remove a module from an instance.
"""
url = "/instances/%s/modules/%s" % (base.getid(instance),
base.getid(module))
resp, body = self.api.client.delete(url)
common.check_for_exceptions(resp, body, url)
def log_list(self, instance):
"""Get a list of all guest logs.
:param instance: The :class:`Instance` (or its ID) of the database
instance to get the log for.
:rtype: list of :class:`DatastoreLog`.
"""
url = '/instances/%s/log' % base.getid(instance)
resp, body = self.api.client.get(url)
common.check_for_exceptions(resp, body, url)
return [DatastoreLog(self, log, loaded=True) for log in body['logs']]
def log_show(self, instance, log_name):
return self._log_action(instance, log_name)
def log_enable(self, instance, log_name):
return self._log_action(instance, log_name, enable=True)
def log_disable(self, instance, log_name, discard=None):
return self._log_action(instance, log_name,
disable=True, discard=discard)
def log_publish(self, instance, log_name, disable=None, discard=None):
return self._log_action(instance, log_name, disable=disable,
publish=True, discard=discard)
def log_discard(self, instance, log_name):
return self._log_action(instance, log_name, discard=True)
def _log_action(self, instance, log_name, enable=None, disable=None,
publish=None, discard=None):
"""Perform action on guest log.
:param instance: The :class:`Instance` (or its ID) of the database
instance to get the log for.
:param log_name: The name of <log> to publish
:param enable: Turn on <log>
:param disable: Turn off <log>
:param publish: Publish log to associated container
:param discard: Delete the associated container
:rtype: List of :class:`DatastoreLog`.
"""
body = {"name": log_name}
if enable:
body.update({'enable': int(enable)})
if disable:
body.update({'disable': int(disable)})
if publish:
body.update({'publish': int(publish)})
if discard:
body.update({'discard': int(discard)})
url = "/instances/%s/log" % base.getid(instance)
resp, body = self.api.client.post(url, body=body)
common.check_for_exceptions(resp, body, url)
return DatastoreLog(self, body['log'], loaded=True)
def _get_container_info(self, instance, log_name, publish):
try:
log_info = self._log_action(instance, log_name, publish=publish)
container = log_info.container
prefix = log_info.prefix
metadata_file = log_info.metafile
return container, prefix, metadata_file
except swift_client.ClientException as ex:
if ex.http_status == 404:
raise exceptions.GuestLogNotFoundError()
raise
def log_generator(self, instance, log_name, publish=None, lines=50,
swift=None):
"""Return generator to yield the last <lines> lines of guest log.
:param instance: The :class:`Instance` (or its ID) of the database
instance to get the log for.
:param log_name: The name of <log> to publish
:param publish: Publish updates before displaying log
:param lines: Display last <lines> lines of log (0 for all lines)
:param swift: Connection to swift
:rtype: generator function to yield log as chunks.
"""
if not swift:
swift = self._get_swift_client()
def _log_generator(instance, log_name, publish, lines, swift):
try:
container, prefix, metadata_file = self._get_container_info(
instance, log_name, publish)
head, body = swift.get_container(container, prefix=prefix)
log_obj_to_display = []
if lines:
total_lines = lines
partial_results = False
parts = sorted(body, key=lambda obj: obj['last_modified'],
reverse=True)
for part in parts:
obj_hdrs = swift.head_object(container, part['name'])
obj_lines = int(obj_hdrs['x-object-meta-lines'])
log_obj_to_display.insert(0, part)
if obj_lines >= lines:
partial_results = True
break
lines -= obj_lines
if not partial_results:
lines = total_lines
part = log_obj_to_display.pop(0)
hdrs, log_obj = swift.get_object(container, part['name'])
log_by_lines = log_obj.splitlines()
yield "\n".join(log_by_lines[-1 * lines:]) + "\n"
else:
log_obj_to_display = sorted(
body, key=lambda obj: obj['last_modified'])
for log_part in log_obj_to_display:
headers, log_obj = swift.get_object(container,
log_part['name'])
yield log_obj
except swift_client.ClientException as ex:
if ex.http_status == 404:
raise exceptions.GuestLogNotFoundError()
raise
return lambda: _log_generator(instance, log_name, publish,
lines, swift)
def log_save(self, instance, log_name, publish=None, filename=None):
"""Saves a guest log to a file.
:param instance: The :class:`Instance` (or its ID) of the database
instance to get the log for.
:param log_name: The name of <log> to publish
:param publish: Publish updates before displaying log
:rtype: Filename to which log was saved
"""
written_file = filename or (instance.name + '-' + log_name + ".log")
log_gen = self.log_generator(instance, log_name, publish, 0)
with open(written_file, 'w') as f:
for log_obj in log_gen():
f.write(log_obj)
return written_file
class InstanceStatus(object):
ACTIVE = "ACTIVE"
BLOCKED = "BLOCKED"
BUILD = "BUILD"
FAILED = "FAILED"
REBOOT = "REBOOT"
RESIZE = "RESIZE"
SHUTDOWN = "SHUTDOWN"
RESTART_REQUIRED = "RESTART_REQUIRED"
PROMOTING = "PROMOTING"
EJECTING = "EJECTING"
LOGGING = "LOGGING"