Implement basic plugin for VM management

This commit implements basic VM management plugin for Climate.
As decided we use nova shelved instances to support our reservation
model. The only one thing we do for "on_start" lease action -
'unshelve' instance. As decided we support configurable opts for
"on_end" lease action, by default it set to snapshot and delete VM.

Implements bp:basic-vm-plugin

Change-Id: Ia34e16c636d1fa8d200873334f55b9868866f97f
This commit is contained in:
Nikolaj Starodubtsev 2013-10-01 15:47:31 +04:00 committed by Nikolay
parent 9b5ad6c5ef
commit 0177cd8841
9 changed files with 255 additions and 26 deletions

View File

@ -76,3 +76,15 @@ class ServiceCatalogNotFound(NotFound):
class WrongFormat(ClimateException):
msg_fmt = _("Unenxpectable object format")
class ServiceClient(ClimateException):
msg_fmt = _("Service %(service)s have some problems")
class TaskFailed(ClimateException):
msg_fmt = _('Current task failed')
class Timeout(ClimateException):
msg_fmt = _('Current task failed with timeout')

View File

View File

@ -0,0 +1,108 @@
# Copyright (c) 2013 Mirantis 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.
import eventlet
from oslo.config import cfg
from climate import exceptions as climate_exceptions
from climate.openstack.common import log as logging
from climate.plugins import base
from climate.utils.openstack import nova
LOG = logging.getLogger(__name__)
plugin_opts = [
cfg.StrOpt('on_end',
default='create_image, delete',
help='Actions which we will use in the end of the lease')
]
CONF = cfg.CONF
CONF.register_opts(plugin_opts, 'virtual:instance')
class VMPlugin(base.BasePlugin):
"""Base plugin for VM reservation."""
resource_type = 'virtual:instance'
title = "Basic VM Plugin"
description = ("This is basic plugin for VM management. "
"It can start, snapshot and suspend VMs")
def on_start(self, resource_id):
nova_client = nova.ClimateNovaClient()
try:
nova_client.servers.unshelve(resource_id)
except nova_client.exceptions.Conflict:
LOG.error("Instance have been unshelved")
def on_end(self, resource_id):
nova_client = nova.ClimateNovaClient()
actions = self._split_actions(CONF['virtual:instance'].on_end)
# actions will be processed in following order:
# - create image from VM
# - suspend VM
# - delete VM
# this order guarantees there will be no situations like
# creating snapshot or suspending already deleted instance
if 'create_image' in actions:
with eventlet.timeout.Timeout(600, climate_exceptions.Timeout):
try:
nova_client.servers.create_image(resource_id)
eventlet.sleep(5)
while not self._check_active(resource_id, nova_client):
eventlet.sleep(1)
except nova_client.exceptions.NotFound:
LOG.error('Instance %s has been already deleted. '
'Cannot create image.' % resource_id)
except climate_exceptions.Timeout:
LOG.error('Image create failed with timeout. Take a look '
'at nova.')
if 'suspend' in actions:
try:
nova_client.servers.suspend(resource_id)
except nova_client.exceptions.NotFound:
LOG.error('Instance %s has been already deleted. '
'Cannot suspend instance.' % resource_id)
if 'delete' in actions:
try:
nova_client.servers.delete(resource_id)
except nova_client.exceptions.NotFound:
LOG.error('Instance %s has been already deleted. '
'Cannot delete instance.' % resource_id)
def _check_active(self, resource_id, curr_client):
instance = curr_client.servers.get(resource_id)
task_state = getattr(instance, 'OS-EXT-STS:task_state', None)
if task_state is None:
return True
if task_state.upper() in ['IMAGE_SNAPSHOT', 'IMAGE_PENDING_UPLOAD',
'IMAGE_UPLOADING']:
return False
else:
LOG.error('Nova reported unexpected task status %s for '
'instance %s' % (task_state, resource_id))
raise climate_exceptions.TaskFailed()
def _split_actions(self, actions):
try:
return actions.replace(' ', '').split(',')
except AttributeError:
raise climate_exceptions.WrongFormat()

View File

@ -0,0 +1,93 @@
# Copyright (c) 2013 Mirantis 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.
import sys
import testtools
from climate import exceptions as climate_exceptions
from climate.openstack.common import log as logging
from climate.plugins.instances import vm_plugin
from climate import tests
from climate.utils.openstack import nova
class VMPluginTestCase(tests.TestCase):
def setUp(self):
super(VMPluginTestCase, self).setUp()
self.plugin = vm_plugin.VMPlugin()
self.nova = nova
self.exc = climate_exceptions
self.logging = logging
self.sys = sys
self.client = self.patch(self.nova, 'ClimateNovaClient')
self.fake_id = '1'
def test_on_start_ok(self):
self.plugin.on_start(self.fake_id)
self.client.return_value.servers.unshelve.assert_called_once_with('1')
@testtools.skip('Will be released later')
def test_on_start_fail(self):
self.client.side_effect =\
self.nova.ClimateNovaClient.exceptions.Conflict
self.plugin.on_start(self.fake_id)
def test_on_end_create_image_ok(self):
self.patch(self.plugin, '_split_actions').return_value =\
['create_image']
self.patch(self.plugin, '_check_active').return_value =\
True
self.plugin.on_end(self.fake_id)
self.client.return_value.servers.create_image.assert_called_once_with(
'1')
def test_on_end_suspend_ok(self):
self.patch(self.plugin, '_split_actions').return_value =\
['suspend']
self.plugin.on_end(self.fake_id)
self.client.return_value.servers.suspend.assert_called_once_with('1')
def test_on_end_delete_ok(self):
self.patch(self.plugin, '_split_actions').return_value =\
['delete']
self.plugin.on_end(self.fake_id)
self.client.return_value.servers.delete.assert_called_once_with('1')
def test_on_end_instance_deleted(self):
self.client.side_effect =\
self.nova.ClimateNovaClient.exceptions.NotFound
self.assertRaises(self.exc.TaskFailed,
self.plugin.on_end,
self.fake_id)
@testtools.skip('Will be released later')
def test_on_end_timeout(self):
self.patch(self.plugin, '_split_actions').return_value =\
['create_image']
self.assertRaises(self.exc.Timeout,
self.plugin.on_end,
self.fake_id)
@testtools.skip('Will be released later')
def test_check_active(self):
pass

View File

@ -68,7 +68,6 @@ class TestCNClient(tests.TestCase):
self.client.assert_called_once_with(version=self.version,
username=self.ctx().user_name,
api_key=None,
auth_token=self.ctx().auth_token,
project_id=self.ctx().tenant_id,
auth_url='http://fake.com/')

View File

@ -15,10 +15,10 @@
from novaclient import client as nova_client
from novaclient import exceptions as nova_exception
from novaclient.v1_1 import servers
from oslo.config import cfg
from climate import context
from climate.manager import exceptions as manager_exceptions
from climate.utils.openstack import base
@ -29,6 +29,9 @@ nova_opts = [
cfg.StrOpt('compute_service',
default='compute',
help='Nova name in keystone'),
cfg.StrOpt('image_prefix',
default='reserved_',
help='Prefix for VM images if you want to create snapshots')
]
CONF = cfg.CONF
@ -65,6 +68,8 @@ class ClimateNovaClient(object):
"""
ctx = kwargs.pop('ctx', None)
auth_token = kwargs.pop('auth_token', None)
mgmt_url = kwargs.pop('mgmt_url', None)
if ctx is None:
try:
@ -75,33 +80,45 @@ class ClimateNovaClient(object):
if ctx is not None:
kwargs.setdefault('username', ctx.user_name)
kwargs.setdefault('api_key', None)
kwargs.setdefault('auth_token', ctx.auth_token)
kwargs.setdefault('project_id', ctx.tenant_id)
if not kwargs.get('auth_url'):
kwargs['auth_url'] = base.url_for(
ctx.service_catalog, CONF.identity_service)
kwargs.setdefault('auth_url', base.url_for(
ctx.service_catalog, CONF.identity_service))
try:
mgmt_url = kwargs.pop('mgmt_url', None) or base.url_for(
ctx.service_catalog, CONF.compute_service)
except AttributeError:
raise manager_exceptions.NoManagementUrl()
auth_token = auth_token or ctx.auth_token
mgmt_url = mgmt_url or base.url_for(ctx.service_catalog,
CONF.compute_service)
self.nova = nova_client.Client(**kwargs)
self.nova.client.auth_token = auth_token
self.nova.client.management_url = mgmt_url
self.nova.servers = ServerManager(self.nova)
self.exceptions = nova_exception
def _image_create(self, instance_id):
instance = self.nova.servers.get(instance_id)
instance_name = instance.name
self.nova.servers.create_image(instance_id,
"reserved_%s" % instance_name)
def __getattr__(self, name):
if name == 'create_image':
func = self._image_create
else:
func = getattr(self.nova, name)
return getattr(self.nova, name)
return func
#todo(dbelova): remove these lines after novaclient 2.16.0 will be released
class ClimateServer(servers.Server):
def unshelve(self):
"""Unshelve -- Unshelve the server."""
self.manager.unshelve(self)
class ServerManager(servers.ServerManager):
resource_class = ClimateServer
def unshelve(self, server):
"""Unshelve the server."""
self._action('unshelve', server, None)
def create_image(self, server_id, image_name=None, metadata=None):
"""Snapshot a server."""
server_name = self.get(server_id).name
if image_name is None:
image_name = cfg.CONF.image_prefix + server_name
return super(ServerManager, self).create_image(server_id,
image_name=image_name,
metadata=metadata)

View File

@ -11,11 +11,10 @@ climate_password=<password>
climate_tenant_name=<tenant_name>
[manager]
plugins=dummy.vm.plugin,physical.host.plugin
plugins=basic.vm.plugin,physical.host.plugin
[virtual:instance]
on_start = wake_up
on_end = delete
on_end = create_image, delete
[physical:host]
on_start = wake_up

View File

@ -36,6 +36,7 @@ console_scripts =
climate.resource.plugins =
dummy.vm.plugin=climate.plugins.dummy_vm_plugin:DummyVMPlugin
physical.host.plugin=climate.plugins.oshosts.host_plugin:PhysicalHostPlugin
basic.vm.plugin=climate.plugins.instances.vm_plugin:VMPlugin
[build_sphinx]
all_files = 1