Allow updates for OS::Trove::Instance

Change-Id: I83b1550557424aaa48bf275ddd6e4c39984dc1e2
Closes-Bug: #1524463
This commit is contained in:
Randall Burt 2015-12-17 16:18:20 -06:00
parent 6caa70c00b
commit 68d12529d3
2 changed files with 449 additions and 1 deletions

View File

@ -12,6 +12,7 @@
# under the License.
from oslo_log import log as logging
import six
from heat.common import exception
from heat.common.i18n import _
@ -91,6 +92,7 @@ class OSDBInstance(resource.Resource):
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the DB instance to create.'),
update_allowed=True,
constraints=[
constraints.Length(max=255),
]
@ -99,6 +101,7 @@ class OSDBInstance(resource.Resource):
properties.Schema.STRING,
_('Reference to a flavor for creating DB instance.'),
required=True,
update_allowed=True,
constraints=[
constraints.CustomConstraint('trove.flavor')
]
@ -123,6 +126,7 @@ class OSDBInstance(resource.Resource):
properties.Schema.INTEGER,
_('Database volume size in GB.'),
required=True,
update_allowed=True,
constraints=[
constraints.Range(1, 150),
]
@ -167,6 +171,7 @@ class OSDBInstance(resource.Resource):
properties.Schema.LIST,
_('List of databases to be created on DB instance creation.'),
default=[],
update_allowed=True,
schema=properties.Schema(
properties.Schema.MAP,
schema={
@ -200,6 +205,7 @@ class OSDBInstance(resource.Resource):
properties.Schema.LIST,
_('List of users to be created on DB instance creation.'),
default=[],
update_allowed=True,
schema=properties.Schema(
properties.Schema.MAP,
schema={
@ -208,6 +214,7 @@ class OSDBInstance(resource.Resource):
_('User name to create a user on instance '
'creation.'),
required=True,
update_allowed=True,
constraints=[
constraints.Length(max=16),
constraints.AllowedPattern(r'[a-zA-Z0-9_]+'
@ -220,6 +227,7 @@ class OSDBInstance(resource.Resource):
_('Password for those users on instance '
'creation.'),
required=True,
update_allowed=True,
constraints=[
constraints.AllowedPattern(r'[a-zA-Z0-9_]+'
r'[a-zA-Z0-9_@?#\s]*'
@ -230,7 +238,8 @@ class OSDBInstance(resource.Resource):
properties.Schema.STRING,
_('The host from which a user is allowed to '
'connect to the database.'),
default='%'
default='%',
update_allowed=True
),
USER_DATABASES: properties.Schema(
properties.Schema.LIST,
@ -240,6 +249,7 @@ class OSDBInstance(resource.Resource):
properties.Schema.STRING,
),
required=True,
update_allowed=True,
constraints=[
constraints.Length(min=1),
]
@ -414,6 +424,170 @@ class OSDBInstance(resource.Resource):
]
self._verify_check_conditions(checks)
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
updates = {}
if prop_diff:
instance = self.client().instances.get(self.resource_id)
if self.NAME in prop_diff:
updates.update({self.NAME: prop_diff[self.NAME]})
if self.FLAVOR in prop_diff:
flvid = prop_diff[self.FLAVOR]
flv = self.client_plugin().get_flavor_id(flvid)
updates.update({self.FLAVOR: flv})
if self.SIZE in prop_diff:
updates.update({self.SIZE: prop_diff[self.SIZE]})
if self.DATABASES in prop_diff:
current = [d.name
for d in self.client().databases.list(instance)]
desired = [d[self.DATABASE_NAME]
for d in prop_diff[self.DATABASES]]
for db in prop_diff[self.DATABASES]:
dbname = db[self.DATABASE_NAME]
if dbname not in current:
db['ACTION'] = self.CREATE
for dbname in current:
if dbname not in desired:
deleted = {self.DATABASE_NAME: dbname,
'ACTION': self.DELETE}
prop_diff[self.DATABASES].append(deleted)
updates.update({self.DATABASES: prop_diff[self.DATABASES]})
if self.USERS in prop_diff:
current = [u.name
for u in self.client().users.list(instance)]
desired = [u[self.USER_NAME] for u in prop_diff[self.USERS]]
for usr in prop_diff[self.USERS]:
if usr[self.USER_NAME] not in current:
usr['ACTION'] = self.CREATE
for usr in current:
if usr not in desired:
prop_diff[self.USERS].append({self.USER_NAME: usr,
'ACTION': self.DELETE})
updates.update({self.USERS: prop_diff[self.USERS]})
return updates
def check_update_complete(self, updates):
instance = self.client().instances.get(self.resource_id)
if instance.status in self.BAD_STATUSES:
raise exception.ResourceInError(
resource_status=instance.status,
status_reason=self.TROVE_STATUS_REASON.get(instance.status,
_("Unknown")))
if updates:
if instance.status != self.ACTIVE:
dmsg = ("Instance is in status %(now)s. Waiting on status"
" %(stat)s")
LOG.debug(dmsg % {"now": instance.status,
"stat": self.ACTIVE})
return False
try:
return (
self._update_name(instance, updates.get(self.NAME)) and
self._update_flavor(instance, updates.get(self.FLAVOR)) and
self._update_size(instance, updates.get(self.SIZE)) and
self._update_databases(instance,
updates.get(self.DATABASES)) and
self._update_users(instance, updates.get(self.USERS))
)
except Exception as exc:
if self.client_plugin().is_client_exception(exc):
# the instance could have updated between the time
# we retrieve it and try to update it so check again
if self.client_plugin().is_over_limit(exc):
LOG.debug("API rate limit: %(ex)s. Retrying." %
{'ex': six.text_type(exc)})
return False
if "No change was requested" in six.text_type(exc):
LOG.warn(_LW("Unexpected instance state change "
"during update. Retrying."))
return False
raise exc
return True
def _update_name(self, instance, name):
if name and instance.name != name:
self.client().instances.edit(instance, name=name)
return False
return True
def _update_flavor(self, instance, new_flavor):
if new_flavor:
current_flav = six.text_type(instance.flavor['id'])
new_flav = six.text_type(new_flavor)
if new_flav != current_flav:
dmsg = "Resizing instance flavor from %(old)s to %(new)s"
LOG.debug(dmsg % {"old": current_flav, "new": new_flav})
self.client().instances.resize_instance(instance, new_flavor)
return False
return True
def _update_size(self, instance, new_size):
if new_size and instance.volume['size'] != new_size:
dmsg = "Resizing instance storage from %(old)s to %(new)s"
LOG.debug(dmsg % {"old": instance.volume['size'],
"new": new_size})
self.client().instances.resize_volume(instance, new_size)
return False
return True
def _update_databases(self, instance, databases):
if databases:
for db in databases:
if db.get("ACTION") == self.CREATE:
db.pop("ACTION", None)
dmsg = "Adding new database %(db)s to instance"
LOG.debug(dmsg % {"db": db})
self.client().databases.create(instance, [db])
elif db.get("ACTION") == self.DELETE:
dmsg = ("Deleting existing database %(db)s from "
"instance")
LOG.debug(dmsg % {"db": db['name']})
self.client().databases.delete(instance, db['name'])
return True
def _update_users(self, instance, users):
if users:
for usr in users:
dbs = [{'name': db} for db in usr.get(self.USER_DATABASES,
[])]
usr[self.USER_DATABASES] = dbs
if usr.get("ACTION") == self.CREATE:
usr.pop("ACTION", None)
dmsg = "Adding new user %(u)s to instance"
LOG.debug(dmsg % {"u": usr})
self.client().users.create(instance, [usr])
elif usr.get("ACTION") == self.DELETE:
dmsg = ("Deleting existing user %(u)s from "
"instance")
LOG.debug(dmsg % {"u": usr['name']})
self.client().users.delete(instance, usr['name'])
else:
newattrs = {}
if usr.get(self.USER_HOST):
newattrs[self.USER_HOST] = usr[self.USER_HOST]
if usr.get(self.USER_PASSWORD):
newattrs[self.USER_PASSWORD] = usr[self.USER_PASSWORD]
if newattrs:
self.client().users.update_attributes(
instance,
usr['name'], newuserattr=newattrs,
hostname=instance.hostname)
current = self.client().users.get(instance,
usr[self.USER_NAME])
dbs = [db['name'] for db in current.databases]
desired = [db['name'] for db in
usr.get(self.USER_DATABASES, [])]
grants = [db for db in desired if db not in dbs]
revokes = [db for db in dbs if db not in desired]
if grants:
self.client().users.grant(instance,
usr[self.USER_NAME],
grants)
if revokes:
self.client().users.revoke(instance,
usr[self.USER_NAME],
revokes)
return True
def handle_delete(self):
"""Delete a cloud database instance."""
if not self.resource_id:

View File

@ -11,17 +11,20 @@
# License for the specific language governing permissions and limitations
# under the License.
import testtools
import uuid
import mock
import six
from troveclient.openstack.common.apiclient import exceptions as troveexc
from troveclient.v1 import users
from heat.common import exception
from heat.common import template_format
from heat.engine.clients.os import neutron
from heat.engine.clients.os import nova
from heat.engine.clients.os import trove
from heat.engine import resource
from heat.engine.resources.openstack.trove import os_database
from heat.engine import rsrc_defn
from heat.engine import scheduler
@ -742,3 +745,274 @@ class OSDBInstanceTest(common.HeatTestCase):
scheduler.TaskRunner(instance.create)()
self.assertEqual((instance.CREATE, instance.COMPLETE), instance.state)
self.m.VerifyAll()
@mock.patch.object(resource.Resource, "client_plugin")
@mock.patch.object(resource.Resource, "client")
class InstanceUpdateTests(testtools.TestCase):
def setUp(self):
super(InstanceUpdateTests, self).setUp()
resource._register_class("OS::Trove::Instance",
os_database.OSDBInstance)
self._stack = mock.Mock()
self._stack.has_cache_data.return_value = False
self._stack.db_resource_get.return_value = None
testprops = {
"name": "testinstance",
"flavor": "foo",
"datastore_type": "database",
"datastore_version": "1",
"size": 10,
"databases": [
{"name": "bar"},
{"name": "biff"}
],
"users": [
{
"name": "baz",
"password": "password",
"databases": ["bar"]
},
{
"name": "deleted",
"password": "password",
"databases": ["biff"]
}
]
}
self._rdef = rsrc_defn.ResourceDefinition('test',
os_database.OSDBInstance,
properties=testprops)
def test_handle_no_update(self, mock_client, mock_plugin):
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertEqual({}, trove.handle_update(None, None, {}))
def test_handle_update_name(self, mock_client, mock_plugin):
prop_diff = {
"name": "changed"
}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertEqual(prop_diff, trove.handle_update(None, None, prop_diff))
def test_handle_update_databases(self, mock_client, mock_plugin):
prop_diff = {
"databases": [
{"name": "bar",
"character_set": "ascii"},
{'name': "baz"}
]
}
mget = mock_client().databases.list
mbar = mock.Mock(name='bar')
mbar.name = 'bar'
mbiff = mock.Mock(name='biff')
mbiff.name = 'biff'
mget.return_value = [mbar, mbiff]
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
expected = {
'databases': [
{'character_set': 'ascii', 'name': 'bar'},
{'ACTION': 'CREATE', 'name': 'baz'},
{'ACTION': 'DELETE', 'name': 'biff'}
]}
self.assertEqual(expected, trove.handle_update(None, None, prop_diff))
def test_handle_update_users(self, mock_client, mock_plugin):
prop_diff = {
"users": [
{"name": "baz",
"password": "changed",
"databases": ["bar", "biff"]},
{'name': "user2",
"password": "password",
"databases": ["biff", "bar"]}
]
}
uget = mock_client().users
mbaz = mock.Mock(name='baz')
mbaz.name = 'baz'
mdel = mock.Mock(name='deleted')
mdel.name = 'deleted'
uget.list.return_value = [mbaz, mdel]
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
expected = {
'users': [{
'databases': ['bar', 'biff'],
'name': 'baz',
'password': 'changed'
}, {
'ACTION': 'CREATE',
'databases': ['biff', 'bar'],
'name': 'user2',
'password': 'password'
}, {
'ACTION': 'DELETE',
'name': 'deleted'
}]}
self.assertEqual(expected, trove.handle_update(None, None, prop_diff))
def test_handle_update_flavor(self, mock_client, mock_plugin):
prop_diff = {
"flavor": "changed"
}
mock_plugin().get_flavor_id.return_value = 1234
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
expected = {
"flavor": 1234
}
self.assertEqual(expected, trove.handle_update(None, None, prop_diff))
def test_handle_update_size(self, mock_client, mock_plugin):
prop_diff = {
"size": 42
}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
expected = {
"size": 42
}
self.assertEqual(expected, trove.handle_update(None, None, prop_diff))
def test_check_complete_none(self, mock_client, mock_plugin):
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertTrue(trove.check_update_complete({}))
def test_check_complete_error(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="ERROR")
mock_client().instances.get.return_value = mock_instance
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
exc = self.assertRaises(exception.ResourceInError,
trove.check_update_complete,
{"foo": "bar"})
msg = "The last operation for the database instance failed"
self.assertIn(msg, six.text_type(exc))
def test_check_client_exceptions(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="ACTIVE")
mock_client().instances.get.return_value = mock_instance
mock_plugin().is_client_exception.return_value = True
mock_plugin().is_over_limit.side_effect = [True, False]
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
with mock.patch.object(trove, "_update_flavor") as mupdate:
mupdate.side_effect = [Exception("test"),
Exception("No change was requested "
"because I'm testing")]
self.assertFalse(trove.check_update_complete({"foo": "bar"}))
self.assertFalse(trove.check_update_complete({"foo": "bar"}))
self.assertEqual(2, mupdate.call_count)
self.assertEqual(2, mock_plugin().is_client_exception.call_count)
def test_check_complete_status(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="RESIZING")
mock_client().instances.get.return_value = mock_instance
updates = {"foo": "bar"}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertFalse(trove.check_update_complete(updates))
def test_check_complete_name(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="ACTIVE", name="mock_instance")
mock_client().instances.get.return_value = mock_instance
updates = {"name": "changed"}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertFalse(trove.check_update_complete(updates))
mock_instance.name = "changed"
self.assertTrue(trove.check_update_complete(updates))
mock_client().instances.edit.assert_called_once_with(mock_instance,
name="changed")
def test_check_complete_databases(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="ACTIVE", name="mock_instance")
mock_client().instances.get.return_value = mock_instance
updates = {
'databases': [
{'name': 'bar', "character_set": "ascii"},
{'ACTION': 'CREATE', 'name': 'baz'},
{'ACTION': 'DELETE', 'name': 'biff'}
]}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertTrue(trove.check_update_complete(updates))
mcreate = mock_client().databases.create
mdelete = mock_client().databases.delete
mcreate.assert_called_once_with(mock_instance, [{'name': 'baz'}])
mdelete.assert_called_once_with(mock_instance, 'biff')
def test_check_complete_users(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="ACTIVE", name="mock_instance")
mock_client().instances.get.return_value = mock_instance
mock_plugin().is_client_exception.return_value = False
mock_client().users.get.return_value = users.User(None, {
"databases": [{
"name": "bar"
}, {
"name": "buzz"
}],
"name": "baz"
}, loaded=True)
updates = {
'users': [{
'databases': ['bar', 'biff'],
'name': 'baz',
'password': 'changed'
}, {
'ACTION': 'CREATE',
'databases': ['biff', 'bar'],
'name': 'user2',
'password': 'password'
}, {
'ACTION': 'DELETE',
'name': 'deleted'
}]}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertTrue(trove.check_update_complete(updates))
create_calls = [
mock.call(mock_instance, [{'password': 'password',
'databases': [{'name': 'biff'},
{'name': 'bar'}],
'name': 'user2'}])
]
delete_calls = [
mock.call(mock_instance, 'deleted')
]
mock_client().users.create.assert_has_calls(create_calls)
mock_client().users.delete.assert_has_calls(delete_calls)
self.assertEqual(1, mock_client().users.create.call_count)
self.assertEqual(1, mock_client().users.delete.call_count)
updateattr = mock_client().users.update_attributes
updateattr.assert_called_once_with(
mock_instance, 'baz', newuserattr={'password': 'changed'},
hostname=mock.ANY)
mock_client().users.grant.assert_called_once_with(
mock_instance, 'baz', ['biff'])
mock_client().users.revoke.assert_called_once_with(
mock_instance, 'baz', ['buzz'])
def test_check_complete_flavor(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="ACTIVE", flavor={'id': 4567},
name="mock_instance")
mock_client().instances.get.return_value = mock_instance
updates = {
"flavor": 1234
}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertFalse(trove.check_update_complete(updates))
mock_instance.status = "RESIZING"
self.assertFalse(trove.check_update_complete(updates))
mock_instance.status = "ACTIVE"
mock_instance.flavor = {'id': 1234}
self.assertTrue(trove.check_update_complete(updates))
def test_check_complete_size(self, mock_client, mock_plugin):
mock_instance = mock.Mock(status="ACTIVE", volume={'size': 24},
name="mock_instance")
mock_client().instances.get.return_value = mock_instance
updates = {
"size": 42
}
trove = os_database.OSDBInstance('test', self._rdef, self._stack)
self.assertFalse(trove.check_update_complete(updates))
mock_instance.status = "RESIZING"
self.assertFalse(trove.check_update_complete(updates))
mock_instance.status = "ACTIVE"
mock_instance.volume = {'size': 42}
self.assertTrue(trove.check_update_complete(updates))