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:
parent
9b5ad6c5ef
commit
0177cd8841
@ -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')
|
||||
|
0
climate/plugins/instances/__init__.py
Normal file
0
climate/plugins/instances/__init__.py
Normal file
108
climate/plugins/instances/vm_plugin.py
Normal file
108
climate/plugins/instances/vm_plugin.py
Normal 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()
|
0
climate/tests/plugins/instances/__init__.py
Normal file
0
climate/tests/plugins/instances/__init__.py
Normal file
93
climate/tests/plugins/instances/test_vm_plugin.py
Normal file
93
climate/tests/plugins/instances/test_vm_plugin.py
Normal 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
|
@ -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/')
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user