Add volume_connectors table to save connector information

This patch introduces a new "volume_connectors" table in order to save
the volume connector information of physical nodes. With this patch,
Ironic can put/get volume connector information to/from the database.

Co-Authored-By: Tomoki Sekiyama <tomoki.sekiyama.qu@hitachi.com>
Co-Authored-By: Stephane Miller <stephane@alum.mit.edu>
Co-Authored-By: Julia Kreger <juliaashleykreger@gmail.com>
Change-Id: I8237ab671a04ac9b01c8bdb2936cf0a661b9b1de
Partial-Bug: 1526231
This commit is contained in:
Satoru Moriya 2016-02-24 18:54:43 +09:00 committed by Ruby Loo
parent ab79b3eff2
commit a82a0ceba1
9 changed files with 498 additions and 0 deletions

View File

@ -157,6 +157,15 @@ class DuplicateName(Conflict):
_msg_fmt = _("A node with name %(name)s already exists.")
class VolumeConnectorAlreadyExists(Conflict):
_msg_fmt = _("A volume connector with UUID %(uuid)s already exists.")
class VolumeConnectorTypeAndIdAlreadyExists(Conflict):
_msg_fmt = _("A volume connector with type %(type)s and connector ID "
"%(connector_id)s already exists.")
class InvalidUUID(Invalid):
_msg_fmt = _("Expected a UUID but received %(uuid)s.")
@ -302,6 +311,10 @@ class ChassisNotFound(NotFound):
_msg_fmt = _("Chassis %(chassis)s could not be found.")
class VolumeConnectorNotFound(NotFound):
_msg_fmt = _("Volume connector %(connector)s could not be found.")
class NoDriversLoaded(IronicException):
_msg_fmt = _("Conductor %(conductor)s cannot be started "
"because no drivers were loaded.")

View File

@ -614,3 +614,101 @@ class Connection(object):
:returns: Node object.
:raises: NodeNotFound if none or several nodes are found.
"""
@abc.abstractmethod
def get_volume_connector_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of volume connectors.
:param limit: Maximum number of volume connectors to return.
:param marker: The last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:returns: A list of volume connectors.
:raises: InvalidParameterValue If sort_key does not exist.
"""
@abc.abstractmethod
def get_volume_connector_by_id(self, id):
"""Return a volume connector representation.
:param id: The ID of a volume connector.
:returns: A volume connector with the specified ID.
:raises: VolumeConnectorNotFound If a volume connector
with the specified ID is not found.
"""
@abc.abstractmethod
def get_volume_connector_by_uuid(self, connector_uuid):
"""Return a volume connector representation.
:param connector_uuid: The UUID of a connector.
:returns: A volume connector with the specified UUID.
:raises: VolumeConnectorNotFound If a volume connector
with the specified UUID is not found.
"""
@abc.abstractmethod
def get_volume_connectors_by_node_id(self, node_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
"""List all the volume connectors for a given node.
:param node_id: The integer node ID.
:param limit: Maximum number of volume connectors to return.
:param marker: The last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted
:param sort_dir: Direction in which results should be sorted
(asc, desc)
:returns: A list of volume connectors.
:raises: InvalidParameterValue If sort_key does not exist.
"""
@abc.abstractmethod
def create_volume_connector(self, connector_info):
"""Create a new volume connector.
:param connector_info: Dictionary containing information about the
connector. Example::
{
'uuid': '000000-..',
'type': 'wwnn',
'connector_id': '00:01:02:03:04:05:06',
'node_id': 2
}
:returns: A volume connector.
:raises: VolumeConnectorTypeAndIdAlreadyExists If a connector
already exists with a matching type and connector_id.
:raises: VolumeConnectorAlreadyExists If a volume connector with
the same UUID already exists.
"""
@abc.abstractmethod
def update_volume_connector(self, ident, connector_info):
"""Update properties of a volume connector.
:param ident: The UUID or integer ID of a volume connector.
:param connector_info: Dictionary containing the information about
connector to update.
:returns: A volume connector.
:raises: VolumeConnectorTypeAndIdAlreadyExists If another
connector already exists with a matching type and
connector_id field.
:raises: VolumeConnectorNotFound If a volume connector
with the specified ident does not exist.
:raises: InvalidParameterValue When a UUID is included in
connector_info.
"""
@abc.abstractmethod
def destroy_volume_connector(self, ident):
"""Destroy a volume connector.
:param ident: The UUID or integer ID of a volume connector.
:raises: VolumeConnectorNotFound If a volume connector
with the specified ident does not exist.
"""

View File

@ -0,0 +1,48 @@
# 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 volume_connectors table
Revision ID: daa1ba02d98
Revises: c14cef6dfedf
Create Date: 2015-11-26 17:19:22.074989
"""
# revision identifiers, used by Alembic.
revision = 'daa1ba02d98'
down_revision = 'bcdd431ba0bf'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('volume_connectors',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('node_id', sa.Integer(), nullable=True),
sa.Column('type', sa.String(length=32), nullable=True),
sa.Column('connector_id', sa.String(length=255),
nullable=True),
sa.Column('extra', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('type', 'connector_id',
name='uniq_volumeconnectors0type0'
'connector_id'),
sa.UniqueConstraint('uuid',
name='uniq_volumeconnectors0uuid'),
mysql_charset='utf8',
mysql_engine='InnoDB')

View File

@ -382,6 +382,10 @@ class Connection(api.Connection):
tag_query = model_query(models.NodeTag).filter_by(node_id=node_id)
tag_query.delete()
volume_connector_query = model_query(
models.VolumeConnector).filter_by(node_id=node_id)
volume_connector_query.delete()
query.delete()
def update_node(self, node_id, values):
@ -871,3 +875,80 @@ class Connection(api.Connection):
raise exception.NodeNotFound(
_('Multiple nodes with port addresses %s were found')
% addresses)
def get_volume_connector_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
return _paginate_query(models.VolumeConnector, limit, marker,
sort_key, sort_dir)
def get_volume_connector_by_id(self, id):
query = model_query(models.VolumeConnector).filter_by(id=id)
try:
return query.one()
except NoResultFound:
raise exception.VolumeConnectorNotFound(connector=id)
def get_volume_connector_by_uuid(self, connector_uuid):
query = model_query(models.VolumeConnector).filter_by(
uuid=connector_uuid)
try:
return query.one()
except NoResultFound:
raise exception.VolumeConnectorNotFound(connector=connector_uuid)
def get_volume_connectors_by_node_id(self, node_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
query = model_query(models.VolumeConnector).filter_by(node_id=node_id)
return _paginate_query(models.VolumeConnector, limit, marker,
sort_key, sort_dir, query)
def create_volume_connector(self, connector_info):
if 'uuid' not in connector_info:
connector_info['uuid'] = uuidutils.generate_uuid()
connector = models.VolumeConnector()
connector.update(connector_info)
with _session_for_write() as session:
try:
session.add(connector)
session.flush()
except db_exc.DBDuplicateEntry as exc:
if 'type' in exc.columns:
raise exception.VolumeConnectorTypeAndIdAlreadyExists(
type=connector_info['type'],
connector_id=connector_info['connector_id'])
raise exception.VolumeConnectorAlreadyExists(
uuid=connector_info['uuid'])
return connector
def update_volume_connector(self, ident, connector_info):
if 'uuid' in connector_info:
msg = _("Cannot overwrite UUID for an existing Volume Connector.")
raise exception.InvalidParameterValue(err=msg)
try:
with _session_for_write() as session:
query = model_query(models.VolumeConnector)
query = add_identity_filter(query, ident)
ref = query.one()
orig_type = ref['type']
orig_connector_id = ref['connector_id']
ref.update(connector_info)
session.flush()
except db_exc.DBDuplicateEntry:
raise exception.VolumeConnectorTypeAndIdAlreadyExists(
type=connector_info.get('type', orig_type),
connector_id=connector_info.get('connector_id',
orig_connector_id))
except NoResultFound:
raise exception.VolumeConnectorNotFound(connector=ident)
return ref
def destroy_volume_connector(self, ident):
with _session_for_write():
query = model_query(models.VolumeConnector)
query = add_identity_filter(query, ident)
count = query.delete()
if count == 0:
raise exception.VolumeConnectorNotFound(connector=ident)

View File

@ -210,3 +210,22 @@ class NodeTag(Base):
primaryjoin='and_(NodeTag.node_id == Node.id)',
foreign_keys=node_id
)
class VolumeConnector(Base):
"""Represents a volume connector of a bare metal node."""
__tablename__ = 'volume_connectors'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_volumeconnectors0uuid'),
schema.UniqueConstraint(
'type',
'connector_id',
name='uniq_volumeconnectors0type0connector_id'),
table_args())
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
type = Column(String(32))
connector_id = Column(String(255))
extra = Column(db_types.JsonEncodedDict)

View File

@ -547,6 +547,43 @@ class MigrationCheckersMixin(object):
self.assertIsInstance(getattr(nodes.c, name).type,
sqlalchemy.types.String)
def _check_daa1ba02d98(self, engine, data):
connectors = db_utils.get_table(engine, 'volume_connectors')
col_names = [column.name for column in connectors.c]
expected_names = ['created_at', 'updated_at', 'id', 'uuid', 'node_id',
'type', 'connector_id', 'extra']
self.assertEqual(sorted(expected_names), sorted(col_names))
self.assertIsInstance(connectors.c.created_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(connectors.c.updated_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(connectors.c.id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(connectors.c.uuid.type,
sqlalchemy.types.String)
self.assertIsInstance(connectors.c.node_id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(connectors.c.type.type,
sqlalchemy.types.String)
self.assertIsInstance(connectors.c.connector_id.type,
sqlalchemy.types.String)
self.assertIsInstance(connectors.c.extra.type,
sqlalchemy.types.TEXT)
typestring = 'a' * 32
connector_idstring = 'a' * 255
uuid = uuidutils.generate_uuid()
data = {'uuid': uuid, 'node_id': 1, 'type': typestring,
'connector_id': connector_idstring, 'extra': '{}'}
connectors.insert().execute(data)
connector = connectors.select(
connectors.c.uuid == uuid).execute().first()
self.assertEqual(typestring, connector['type'])
self.assertEqual(connector_idstring, connector['connector_id'])
self.assertEqual(1, connector['node_id'])
self.assertEqual('{}', connector['extra'])
def test_upgrade_and_version(self):
with patch_with_engine(self.engine):
self.migration_api.upgrade('head')

View File

@ -359,6 +359,26 @@ class DbNodeTestCase(base.DbTestCase):
self.dbapi.destroy_node(node.uuid)
self.assertFalse(self.dbapi.node_tag_exists(node.id, tag.tag))
def test_volume_connector_get_destroyed_after_destroying_a_node(self):
node = utils.create_test_node()
connector = utils.create_test_volume_connector(node_id=node.id)
self.dbapi.destroy_node(node.id)
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.get_volume_connector_by_id, connector.id)
def test_volume_connector_get_destroyed_after_destroying_a_node_uuid(self):
node = utils.create_test_node()
connector = utils.create_test_volume_connector(node_id=node.id)
self.dbapi.destroy_node(node.uuid)
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.get_volume_connector_by_id, connector.id)
def test_update_node(self):
node = utils.create_test_node()

View File

@ -0,0 +1,151 @@
# Copyright 2015 Hitachi Data Systems
#
# 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.
"""Tests for manipulating VolumeConnectors via the DB API"""
from oslo_utils import uuidutils
import six
from ironic.common import exception
from ironic.tests.unit.db import base
from ironic.tests.unit.db import utils as db_utils
class DbVolumeConnectorTestCase(base.DbTestCase):
def setUp(self):
# This method creates a volume_connector for every test and
# replaces a test for creating a volume_connector.
super(DbVolumeConnectorTestCase, self).setUp()
self.node = db_utils.create_test_node()
self.connector = db_utils.create_test_volume_connector(
node_id=self.node.id, type='test',
connector_id='test-connector_id')
def test_create_volume_connector_duplicated_type_connector_id(self):
self.assertRaises(exception.VolumeConnectorTypeAndIdAlreadyExists,
db_utils.create_test_volume_connector,
uuid=uuidutils.generate_uuid(),
node_id=self.node.id,
type=self.connector.type,
connector_id=self.connector.connector_id)
def test_create_volume_connector_duplicated_uuid(self):
self.assertRaises(exception.VolumeConnectorAlreadyExists,
db_utils.create_test_volume_connector,
uuid=self.connector.uuid,
node_id=self.node.id,
type='test',
connector_id='test-connector_id-2')
def test_get_volume_connector_by_id(self):
res = self.dbapi.get_volume_connector_by_id(self.connector.id)
self.assertEqual(self.connector.type, res.type)
self.assertEqual(self.connector.connector_id, res.connector_id)
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.get_volume_connector_by_id,
-1)
def test_get_volume_connector_by_uuid(self):
res = self.dbapi.get_volume_connector_by_uuid(self.connector.uuid)
self.assertEqual(self.connector.id, res.id)
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.get_volume_connector_by_uuid,
-1)
def _connector_list_preparation(self):
uuids = [six.text_type(self.connector.uuid)]
for i in range(1, 6):
volume_connector = db_utils.create_test_volume_connector(
uuid=uuidutils.generate_uuid(),
type='iqn',
connector_id='iqn.test-%s' % i)
uuids.append(six.text_type(volume_connector.uuid))
return uuids
def test_get_volume_connector_list(self):
uuids = self._connector_list_preparation()
res = self.dbapi.get_volume_connector_list()
res_uuids = [r.uuid for r in res]
six.assertCountEqual(self, uuids, res_uuids)
def test_get_volume_connector_list_sorted(self):
uuids = self._connector_list_preparation()
res = self.dbapi.get_volume_connector_list(sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.get_volume_connector_list, sort_key='foo')
def test_get_volume_connectors_by_node_id(self):
res = self.dbapi.get_volume_connectors_by_node_id(self.node.id)
self.assertEqual(self.connector.type, res[0].type)
self.assertEqual(self.connector.connector_id, res[0].connector_id)
def test_get_volume_connectors_by_node_id_that_does_not_exist(self):
self.assertEqual([], self.dbapi.get_volume_connectors_by_node_id(99))
def test_update_volume_connector(self):
old_connector_id = self.connector.connector_id
new_connector_id = 'test-connector_id-2'
self.assertNotEqual(old_connector_id, new_connector_id)
res = self.dbapi.update_volume_connector(
self.connector.id,
{'connector_id': new_connector_id})
self.assertEqual(new_connector_id, res.connector_id)
res = self.dbapi.update_volume_connector(
self.connector.uuid,
{'connector_id': old_connector_id})
self.assertEqual(old_connector_id, res.connector_id)
def test_update_volume_connector_uuid(self):
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.update_volume_connector,
self.connector.id,
{'uuid': ''})
def test_update_volume_connector_fails_invalid_id(self):
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.update_volume_connector,
-1,
{'node_id': ''})
def test_update_volume_connector_duplicated_type_connector_id(self):
type = self.connector.type
connector_id1 = self.connector.connector_id
connector_id2 = 'test-connector_id-2'
volume_connector2 = db_utils.create_test_volume_connector(
uuid=uuidutils.generate_uuid(),
node_id=self.node.id,
type=type,
connector_id=connector_id2)
self.assertRaises(exception.VolumeConnectorTypeAndIdAlreadyExists,
self.dbapi.update_volume_connector,
volume_connector2.id,
{'connector_id': connector_id1})
def test_destroy_volume_connector(self):
self.dbapi.destroy_volume_connector(self.connector.id)
# Attempt to retrieve the volume to verify it is gone.
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.get_volume_connector_by_id,
self.connector.id)
# Ensure that the destroy_volume_connector returns the
# expected exception.
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.destroy_volume_connector,
self.connector.id)

View File

@ -315,6 +315,37 @@ def create_test_port(**kw):
return dbapi.create_port(port)
def get_test_volume_connector(**kw):
return {
'id': kw.get('id', 789),
'uuid': kw.get('uuid', '1be26c0b-03f2-4d2e-ae87-c02d7f33c781'),
'node_id': kw.get('node_id', 123),
'type': kw.get('type', 'iqn'),
'connector_id': kw.get('connector_id',
'iqn.2012-06.com.example:initiator'),
'extra': kw.get('extra', {}),
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at'),
}
def create_test_volume_connector(**kw):
"""Create test connector entry in DB and return VolumeConnector DB object.
Function to be used to create test VolumeConnector objects in the database.
:param kw: kwargs with overriding values for connector's attributes.
:returns: Test VolumeConnector DB object.
"""
connector = get_test_volume_connector(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del connector['id']
dbapi = db_api.get_instance()
return dbapi.create_volume_connector(connector)
def get_test_chassis(**kw):
return {
'id': kw.get('id', 42),