Nova driver implementation

Added nova_driver for amphora creation through nova
Added amphora types list and an entry for Virtual Machine type to constants
Added nova version list and available versions to constants
Added amphora status list and UP/DOWN statuses to constants
Added to amphora data_model for reuse in response from nova_driver
Added testing for nova_driver

Change-Id: I6c45dae5dbdd39515f9db02e8765d68871da2762
Partially-Implements: blueprint nova-compute-driver
This commit is contained in:
Trevor Vardeman 2014-11-06 12:54:03 -06:00
parent 9b989e2f8a
commit 0053509489
16 changed files with 435 additions and 16 deletions

View File

@ -36,3 +36,7 @@
# ca_private_key_passphrase =
# signing_digest = sha265
# storage_path = /var/lib/octavia/certificates/
[networking]
# Network to communicate with amphora
# lb_network_id =

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from wsme import types as wtypes
from octavia.api.v1.types import base

View File

@ -82,11 +82,16 @@ keystone_authtoken_opts = [
cfg.StrOpt('admin_project_id'),
]
networking_opts = [
cfg.StrOpt('lb_network_id', help=_('ID of amphora internal network')),
]
core_cli_opts = []
# Register the configuration options
cfg.CONF.register_opts(core_opts)
cfg.CONF.register_opts(keystone_authtoken_opts, group='keystone_authtoken')
cfg.CONF.register_opts(networking_opts, group='networking')
cfg.CONF.register_cli_opts(core_cli_opts)
# Ensure that the control exchange is set correctly

View File

@ -53,4 +53,13 @@ ERROR = 'ERROR'
SUPPORTED_OPERATING_STATUSES = (ONLINE, OFFLINE, DEGRADED, ERROR)
AMPHORA_VM = 'VM'
AMPHORA_TYPES = (AMPHORA_VM, )
SUPPORTED_AMPHORA_TYPES = (AMPHORA_VM,)
AMPHORA_UP = 'UP'
AMPHORA_DOWN = 'DOWN'
SUPPORTED_AMPHORA_STATUSES = (AMPHORA_UP, AMPHORA_DOWN)
NOVA_1 = '1.1'
NOVA_2 = '2'
NOVA_3 = '3'
NOVA_VERSIONS = (NOVA_1, NOVA_2, NOVA_3)

View File

@ -193,9 +193,10 @@ class SNI(BaseDataModel):
class Amphora(BaseDataModel):
def __init__(self, id=None, load_balancer_id=None, compute_id=None,
status=None, load_balancer=None):
status=None, lb_network_ip=None, load_balancer=None):
self.id = id
self.load_balancer_id = load_balancer_id
self.compute_id = compute_id
self.status = status
self.lb_network_ip = lb_network_ip
self.load_balancer = load_balancer

View File

@ -20,6 +20,8 @@ Octavia base exception handling.
from oslo.utils import excutils
from webob import exc
from octavia.i18n import _LE
class OctaviaException(Exception):
"""Base Octavia Exception.
@ -112,3 +114,19 @@ class DuplicatePoolEntry(APIException):
class ImmutableObject(APIException):
msg = _("%(resource)s %(id)s is immutable and cannot be updated.")
code = 409
class ComputeBuildException(OctaviaException):
message = _LE('Failed to build nova instance.')
class ComputeDeleteException(OctaviaException):
message = _LE('Failed to delete nova instance.')
class ComputeGetException(OctaviaException):
message = _LE('Failed to retrieve nova instance.')
class ComputeStatusException(OctaviaException):
message = _LE('Failed to retrieve nova instance status.')

View File

@ -16,8 +16,6 @@ import abc
import six
import octavia.common.constants as constants
@six.add_metaclass(abc.ABCMeta)
class ComputeBase(object):
@ -31,16 +29,15 @@ class ComputeBase(object):
pass
@abc.abstractmethod
def build(self, amphora_type=constants.AMPHORA_VM, amphora_flavor=None,
image_id=None, keys=None, sec_groups=None, network_ids=None):
def build(self, name="amphora_name", amphora_flavor=None, image_id=None,
key_name=None, sec_groups=None, network_ids=None):
"""Build a new amphora.
:param amphora_type: The type of amphora to create (ex: VM)
:param name: Optional name for Amphora
:param amphora_flavor: Optionally specify a flavor
:param image_id: ID of the base image for the amphora instance
:param keys: Optionally specify a list of ssh public keys
:param sec_groups: Optionally specify list of security
groups
:param key_name: Optionally specify a keypair
:param sec_groups: Optionally specify list of security groups
:param network_ids: A list of network IDs to attach to the amphora
:returns: The id of the new instance.
"""

View File

View File

@ -0,0 +1,172 @@
# Copyright 2014 Rackspace
#
# 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.
from keystoneclient.auth.identity import v3 as keystone_client
from keystoneclient import session
from novaclient import client as nova_client
from oslo.config import cfg
from oslo.utils import excutils
from octavia.common import constants
from octavia.common import data_models as models
from octavia.common import exceptions
from octavia.compute import compute_base
from octavia.i18n import _LE
from octavia.openstack.common import log
LOG = log.getLogger(__name__)
CONF = cfg.CONF
CONF.import_group('keystone_authtoken', 'octavia.common.config')
CONF.import_group('networking', 'octavia.common.config')
class VirtualMachineManager(compute_base.ComputeBase):
'''Compute implementation of virtual machines via nova.'''
def __init__(self):
super(VirtualMachineManager, self).__init__()
# Must initialize nova api
self._nova_client = NovaKeystoneAuth.get_nova_client()
self.manager = self._nova_client.servers
def get_logger(self):
'''Retrieve a custom logger.'''
pass
def build(self, name="amphora_name", amphora_flavor=None, image_id=None,
key_name=None, sec_groups=None, network_ids=None):
'''Create a new virtual machine.
:param name: optional name for amphora
:param amphora_flavor: image flavor for virtual machine
:param image_id: image ID for virtual machine
:param key_name: keypair to add to the virtual machine
:param sec_groups: Security group IDs for virtual machine
:param network_ids: Network IDs to include on virtual machine
:raises NovaBuildException: if nova failed to build virtual machine
:returns: UUID of amphora
'''
try:
amphora = self.manager.create(
name=name, image=image_id, flavor=amphora_flavor,
key_name=key_name, security_groups=sec_groups,
nics=network_ids)
return amphora.get('id')
except Exception:
LOG.exception(_LE("Error building nova virtual machine."))
raise exceptions.ComputeBuildException()
def delete(self, amphora_id):
'''Delete a virtual machine.
:param amphora_id: virtual machine UUID
'''
try:
self.manager.delete(server=amphora_id)
except Exception:
LOG.exception(_LE("Error deleting nova virtual machine."))
raise exceptions.ComputeDeleteException()
def status(self, amphora_id):
'''Retrieve the status of a virtual machine.
:param amphora_id: virtual machine UUID
:returns: constant of amphora status
'''
try:
if self.get_amphora(amphora_id=amphora_id):
return constants.AMPHORA_UP
except Exception:
LOG.exception(_LE("Error retrieving nova virtual machine status."))
raise exceptions.ComputeStatusException()
return constants.AMPHORA_DOWN
def get_amphora(self, amphora_id):
'''Retrieve the information in nova of a virtual machine.
:param amphora_id: virtual machine UUID
:returns: an amphora object
'''
# utilize nova client ServerManager 'get' method to retrieve info
try:
amphora = self.manager.get(amphora_id)
except Exception:
LOG.exception(_LE("Error retrieving nova virtual machine."))
raise exceptions.ComputeGetException()
return self._translate_amphora(amphora)
def _translate_amphora(self, nova_response):
'''Convert a nova virtual machine into an amphora object.
:param nova_response: JSON response from nova
:returns: an amphora object
'''
# Extract information from nova response to populate desired amphora
# fields
lb_network_ip = None
for interface in nova_response.get('interface_list'):
if interface.get('net_id') is CONF.networking.lb_network_id:
lb_network_ip = interface.get('fixed_ips')[0].get('ip_address')
response = models.Amphora(
compute_id=nova_response.get('id'),
status=nova_response.get('status'),
lb_network_ip=lb_network_ip
)
return response
class NovaKeystoneAuth(object):
_keystone_session = None
_nova_client = None
# TODO(rm_you): refactor for common availability
@classmethod
def _get_keystone_session(cls):
"""Initializes a Keystone session.
:return: a Keystone Session object
:raises Exception: if the session cannot be established
"""
if not cls._keystone_session:
try:
kc = keystone_client.Password(
auth_url=CONF.keystone_authtoken.auth_uri,
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password,
project_id=CONF.keystone_authtoken.admin_project_id
)
cls._keystone_session = session.Session(auth=kc)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Error creating Keystone session."))
LOG.info()
return cls._keystone_session
@classmethod
def get_nova_client(cls):
"""Create nova client object.
:return: a Nova Client object.
:raises Exception: if the client cannot be created
"""
if not cls._nova_client:
try:
cls._nova_client = nova_client.Client(
constants.NOVA_3, session=cls._get_keystone_session()
)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Error creating Nova client."))
return cls._nova_client

View File

@ -15,14 +15,14 @@
"""update vip
Revision ID: 14892634e228
Revises: 13500e2e978d
Revises: 3a1e1cdb7b27
Create Date: 2015-01-10 00:53:57.798213
"""
# revision identifiers, used by Alembic.
revision = '14892634e228'
down_revision = '13500e2e978d'
down_revision = '3a1e1cdb7b27'
from alembic import op
import sqlalchemy as sa

View File

@ -0,0 +1,37 @@
# Copyright 2015 Rackspace
#
# 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.
"""add lb_network_ip to amphora
Revision ID: 256852d5ff7c
Revises: 14892634e228
Create Date: 2015-01-13 16:18:57.359290
"""
# revision identifiers, used by Alembic.
revision = '256852d5ff7c'
down_revision = '14892634e228'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column(u'amphora', sa.Column(u'lb_network_ip', sa.String(64),
nullable=True))
def downgrade():
op.drop_column(u'amphora', u'lb_network_ip')

View File

@ -318,6 +318,7 @@ class Amphora(base_models.BASE):
name="fk_amphora_load_balancer_id"),
nullable=True)
compute_id = sa.Column(sa.String(36), nullable=True)
lb_network_ip = sa.Column(sa.String(64), nullable=True)
status = sa.Column(
sa.String(36),
sa.ForeignKey("provisioning_status.name",

View File

@ -21,6 +21,7 @@ from octavia.tests.functional.db import base
class ModelTestMixin(object):
FAKE_IP = '10.0.0.1'
FAKE_UUID_1 = uuidutils.generate_uuid()
FAKE_UUID_2 = uuidutils.generate_uuid()
@ -114,7 +115,8 @@ class ModelTestMixin(object):
def create_amphora(self, session, **overrides):
kwargs = {'id': self.FAKE_UUID_1,
'compute_id': self.FAKE_UUID_1,
'status': constants.ACTIVE}
'status': constants.ACTIVE,
'lb_network_ip': self.FAKE_IP}
kwargs.update(overrides)
return self._insert(session, models.Amphora, kwargs)

View File

@ -21,6 +21,7 @@ from octavia.tests.functional.db import base
class BaseRepositoryTest(base.OctaviaDBTestBase):
FAKE_IP = "10.0.0.1"
FAKE_UUID_1 = uuidutils.generate_uuid()
FAKE_UUID_2 = uuidutils.generate_uuid()
FAKE_UUID_3 = uuidutils.generate_uuid()
@ -797,7 +798,8 @@ class LoadBalancerRepositoryTest(BaseRepositoryTest):
amphora = self.amphora_repo.create(self.session, id=self.FAKE_UUID_1,
load_balancer_id=lb.id,
compute_id=self.FAKE_UUID_3,
status=constants.ACTIVE)
status=constants.ACTIVE,
lb_network_ip=self.FAKE_IP)
new_lb = self.lb_repo.get(self.session, id=lb.id)
self.assertIsNotNone(new_lb)
self.assertEqual(1, len(new_lb.amphorae))
@ -817,6 +819,7 @@ class LoadBalancerRepositoryTest(BaseRepositoryTest):
amphora_2 = self.amphora_repo.create(self.session, id=self.FAKE_UUID_3,
load_balancer_id=lb.id,
compute_id=self.FAKE_UUID_3,
lb_network_ip=self.FAKE_IP,
status=constants.ACTIVE)
new_lb = self.lb_repo.get(self.session, id=lb.id)
self.assertIsNotNone(new_lb)
@ -895,6 +898,7 @@ class LoadBalancerRepositoryTest(BaseRepositoryTest):
amphora = self.amphora_repo.create(self.session, id=self.FAKE_UUID_1,
load_balancer_id=lb.id,
compute_id=self.FAKE_UUID_3,
lb_network_ip=self.FAKE_IP,
status=constants.ACTIVE)
vip = self.vip_repo.create(self.session, load_balancer_id=lb.id,
ip_address="10.0.0.1")
@ -1055,7 +1059,8 @@ class AmphoraRepositoryTest(BaseRepositoryTest):
def create_amphora(self, amphora_id):
amphora = self.amphora_repo.create(self.session, id=amphora_id,
compute_id=self.FAKE_UUID_3,
status=constants.ACTIVE)
status=constants.ACTIVE,
lb_network_ip=self.FAKE_IP)
return amphora
def test_get(self):

View File

@ -0,0 +1,167 @@
# Copyright 2014 Rackspace
#
# 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.
from keystoneclient import session
import mock
import novaclient
from oslo.config import cfg
from octavia.common import constants
from octavia.common import data_models as models
from octavia.common import exceptions
import octavia.compute.drivers.nova_driver as nova_common
from octavia.openstack.common import uuidutils
import octavia.tests.unit.base as base
CONF = cfg.CONF
CONF.import_group('networking', 'octavia.common.config')
class TestNovaClient(base.TestCase):
def setUp(self):
net_id = uuidutils.generate_uuid()
CONF.set_override(group='networking', name='lb_network_id',
override=net_id)
self.amphora = models.Amphora(
compute_id=uuidutils.generate_uuid(),
status='ONLINE',
lb_network_ip='10.0.0.1'
)
self.nova_response = {'id': self.amphora.compute_id,
'type': constants.AMPHORA_VM,
'image': uuidutils.generate_uuid(),
'flavor': 1,
'keys': [uuidutils.generate_uuid()],
'security_groups': ['admin'],
'nics': [uuidutils.generate_uuid()],
'status': 'ONLINE',
'interface_list': [{
"fixed_ips": [
{'ip_address': '10.0.0.1',
'subnet_id': uuidutils.generate_uuid()}
],
'net_id': net_id}]}
self.manager = nova_common.VirtualMachineManager()
self.manager.manager = mock.MagicMock()
self.manager.manager.get.return_value = self.nova_response
self.manager.manager.create.return_value = self.nova_response
super(TestNovaClient, self).setUp()
def test_build(self):
amphora_id = self.manager.build(amphora_flavor=1, image_id=1,
key_name=1, sec_groups=1,
network_ids=1)
self.assertEqual(self.amphora.compute_id, amphora_id)
self.manager.manager.create.assert_called_with(
name="amphora_name", image=1, flavor=1, key_name=1,
security_groups=1, nics=1
)
def test_bad_build(self):
self.manager.manager.create.side_effect = Exception
self.assertRaises(exceptions.ComputeBuildException, self.manager.build)
def test_delete(self):
amphora_id = self.manager.build(amphora_flavor=1, image_id=1,
key_name=1, sec_groups=1,
network_ids=1)
self.manager.delete(amphora_id)
self.manager.manager.delete.assert_called_with(server=amphora_id)
def test_bad_delete(self):
self.manager.manager.delete.side_effect = Exception
amphora_id = self.manager.build(amphora_flavor=1, image_id=1,
key_name=1, sec_groups=1,
network_ids=1)
self.assertRaises(exceptions.ComputeDeleteException,
self.manager.delete, amphora_id)
def test_status(self):
status = self.manager.status(self.amphora.id)
self.assertEqual(constants.AMPHORA_UP, status)
def test_bad_status(self):
self.manager.manager.get.side_effect = Exception
self.assertRaises(exceptions.ComputeStatusException,
self.manager.status, self.amphora.id)
def test_get_amphora(self):
amphora = self.manager.get_amphora(self.amphora.compute_id)
self.assertEqual(self.amphora, amphora)
self.manager.manager.get.called_with(server=amphora.id)
def test_bad_get_amphora(self):
self.manager.manager.get.side_effect = Exception
self.assertRaises(exceptions.ComputeGetException,
self.manager.get_amphora, self.amphora.id)
class TestNovaAuth(base.TestCase):
def setUp(self):
# Reset the session and client
nova_common.NovaKeystoneAuth._keystone_session = None
nova_common.NovaKeystoneAuth._nova_client = None
super(TestNovaAuth, self).setUp()
def test_get_keystone_client(self):
# There should be no existing session
self.assertIsNone(
nova_common.NovaKeystoneAuth._keystone_session
)
# Get us a session
ks1 = nova_common.NovaKeystoneAuth._get_keystone_session()
# Our returned session should also be the saved session
self.assertIsInstance(
nova_common.NovaKeystoneAuth._keystone_session,
session.Session
)
self.assertIs(
nova_common.NovaKeystoneAuth._keystone_session,
ks1
)
# Getting the session again should return the same object
ks2 = nova_common.NovaKeystoneAuth._get_keystone_session()
self.assertIs(ks1, ks2)
def test_get_nova_client(self):
# There should be no existing client
self.assertIsNone(
nova_common.NovaKeystoneAuth._nova_client
)
# Mock out the keystone session and get the client
nova_common.NovaKeystoneAuth._keystone_session = (
mock.MagicMock()
)
bc1 = nova_common.NovaKeystoneAuth.get_nova_client()
# Our returned client should also be the saved client
self.assertIsInstance(
nova_common.NovaKeystoneAuth._nova_client,
novaclient.v3.client.Client
)
self.assertIs(
nova_common.NovaKeystoneAuth._nova_client,
bc1
)
# Getting the session again should return the same object
bc2 = nova_common.NovaKeystoneAuth.get_nova_client()
self.assertIs(bc1, bc2)

View File

@ -1,5 +1,5 @@
actdiag
alembic>=0.6.4
alembic>=0.7.1
blockdiag
docutils==0.11
nwdiag