Add volume_targets table to database

This patch adds a "volume_targets" DB table in order to save
the volume target information of physical nodes. With this patch,
Ironic can put/get volume target information to/from the database.

Co-Authored-By: Stephane Miller <stephane@alum.mit.edu>
Co-Authored-By: Ruby Loo <ruby.loo@intel.com>
Change-Id: I79063f9d0aafd7b740785a883732536704e43b7c
Partial-Bug: 1526231
This commit is contained in:
Satoru Moriya 2016-02-26 19:49:15 +09:00 committed by Ruby Loo
parent f857a883d3
commit 07541047be
9 changed files with 504 additions and 2 deletions

View File

@ -217,6 +217,15 @@ class VolumeConnectorTypeAndIdAlreadyExists(Conflict):
"%(connector_id)s already exists.")
class VolumeTargetAlreadyExists(Conflict):
_msg_fmt = _("A volume target with UUID %(uuid)s already exists.")
class VolumeTargetBootIndexAlreadyExists(Conflict):
_msg_fmt = _("A volume target with boot index '%(boot_index)s' "
"for the same node already exists.")
class InvalidUUID(Invalid):
_msg_fmt = _("Expected a UUID but received %(uuid)s.")
@ -372,6 +381,10 @@ class VolumeConnectorNotFound(NotFound):
_msg_fmt = _("Volume connector %(connector)s could not be found.")
class VolumeTargetNotFound(NotFound):
_msg_fmt = _("Volume target %(target)s could not be found.")
class NoDriversLoaded(IronicException):
_msg_fmt = _("Conductor %(conductor)s cannot be started "
"because no drivers were loaded.")

View File

@ -183,9 +183,12 @@ class Connection(object):
@abc.abstractmethod
def destroy_node(self, node_id):
"""Destroy a node and all associated interfaces.
"""Destroy a node and its associated resources.
:param node_id: The id or uuid of a node.
Destroy a node, including any associated ports, port groups,
tags, volume connectors, and volume targets.
:param node_id: The ID or UUID of a node.
"""
@abc.abstractmethod
@ -687,6 +690,7 @@ class Connection(object):
: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.
@ -712,3 +716,100 @@ class Connection(object):
:raises: VolumeConnectorNotFound If a volume connector
with the specified ident does not exist.
"""
@abc.abstractmethod
def get_volume_target_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of volume targets.
:param limit: Maximum number of volume targets 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 targets.
:raises: InvalidParameterValue if sort_key does not exist.
"""
@abc.abstractmethod
def get_volume_target_by_id(self, db_id):
"""Return a volume target representation.
:param db_id: The database primary key (integer) ID of a volume target.
:returns: A volume target.
:raises: VolumeTargetNotFound if no volume target with this ID
exists.
"""
@abc.abstractmethod
def get_volume_target_by_uuid(self, uuid):
"""Return a volume target representation.
:param uuid: The UUID of a volume target.
:returns: A volume target.
:raises: VolumeTargetNotFound if no volume target with this UUID
exists.
"""
@abc.abstractmethod
def get_volume_targets_by_node_id(self, node_id, limit=None,
marker=None, sort_key=None,
sort_dir=None):
"""List all the volume targets for a given node.
:param node_id: The integer node ID.
:param limit: Maximum number of volume targets 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 targets.
:raises: InvalidParameterValue if sort_key does not exist.
"""
@abc.abstractmethod
def create_volume_target(self, target_info):
"""Create a new volume target.
:param target_info: Dictionary containing the information about the
volume target. Example::
{
'uuid': '000000-..',
'node_id': 2,
'boot_index': 0,
'volume_id': '12345678-...'
'volume_type': 'some type',
}
:returns: A volume target.
:raises: VolumeTargetBootIndexAlreadyExists if a volume target already
exists with the same boot index and node ID.
:raises: VolumeTargetAlreadyExists if a volume target with the same
UUID exists.
"""
@abc.abstractmethod
def update_volume_target(self, ident, target_info):
"""Update information for a volume target.
:param ident: The UUID or integer ID of a volume target.
:param target_info: Dictionary containing the information about
volume target to update.
:returns: A volume target.
:raises: InvalidParameterValue if a UUID is included in target_info.
:raises: VolumeTargetBootIndexAlreadyExists if a volume target already
exists with the same boot index and node ID.
:raises: VolumeTargetNotFound if no volume target with this ident
exists.
"""
@abc.abstractmethod
def destroy_volume_target(self, ident):
"""Destroy a volume target.
:param ident: The UUID or integer ID of a volume target.
:raises: VolumeTargetNotFound if a volume target with the specified
ident does not exist.
"""

View File

@ -0,0 +1,51 @@
# 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_targets table
Revision ID: 1a59178ebdf6
Revises: daa1ba02d98
Create Date: 2016-02-25 11:25:29.836535
"""
# revision identifiers, used by Alembic.
revision = '1a59178ebdf6'
down_revision = 'daa1ba02d98'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('volume_targets',
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('volume_type', sa.String(length=64),
nullable=True),
sa.Column('properties', sa.Text(), nullable=True),
sa.Column('boot_index', sa.Integer(), nullable=True),
sa.Column('volume_id',
sa.String(length=36), nullable=True),
sa.Column('extra', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('node_id', 'boot_index',
name='uniq_volumetargets0node_id0'
'boot_index'),
sa.UniqueConstraint('uuid',
name='uniq_volumetargets0uuid'),
mysql_charset='utf8',
mysql_engine='InnoDB')

View File

@ -386,6 +386,10 @@ class Connection(api.Connection):
models.VolumeConnector).filter_by(node_id=node_id)
volume_connector_query.delete()
volume_target_query = model_query(
models.VolumeTarget).filter_by(node_id=node_id)
volume_target_query.delete()
query.delete()
def update_node(self, node_id, values):
@ -952,3 +956,74 @@ class Connection(api.Connection):
count = query.delete()
if count == 0:
raise exception.VolumeConnectorNotFound(connector=ident)
def get_volume_target_list(self, limit=None, marker=None,
sort_key=None, sort_dir=None):
return _paginate_query(models.VolumeTarget, limit, marker,
sort_key, sort_dir)
def get_volume_target_by_id(self, db_id):
query = model_query(models.VolumeTarget).filter_by(id=db_id)
try:
return query.one()
except NoResultFound:
raise exception.VolumeTargetNotFound(target=db_id)
def get_volume_target_by_uuid(self, uuid):
query = model_query(models.VolumeTarget).filter_by(uuid=uuid)
try:
return query.one()
except NoResultFound:
raise exception.VolumeTargetNotFound(target=uuid)
def get_volume_targets_by_node_id(self, node_id, limit=None, marker=None,
sort_key=None, sort_dir=None):
query = model_query(models.VolumeTarget).filter_by(node_id=node_id)
return _paginate_query(models.VolumeTarget, limit, marker, sort_key,
sort_dir, query)
def create_volume_target(self, target_info):
if 'uuid' not in target_info:
target_info['uuid'] = uuidutils.generate_uuid()
target = models.VolumeTarget()
target.update(target_info)
with _session_for_write() as session:
try:
session.add(target)
session.flush()
except db_exc.DBDuplicateEntry as exc:
if 'boot_index' in exc.columns:
raise exception.VolumeTargetBootIndexAlreadyExists(
boot_index=target_info['boot_index'])
raise exception.VolumeTargetAlreadyExists(
uuid=target_info['uuid'])
return target
def update_volume_target(self, ident, target_info):
if 'uuid' in target_info:
msg = _("Cannot overwrite UUID for an existing Volume Target.")
raise exception.InvalidParameterValue(err=msg)
try:
with _session_for_write() as session:
query = model_query(models.VolumeTarget)
query = add_identity_filter(query, ident)
ref = query.one()
orig_boot_index = ref['boot_index']
ref.update(target_info)
session.flush()
except db_exc.DBDuplicateEntry:
raise exception.VolumeTargetBootIndexAlreadyExists(
boot_index=target_info.get('boot_index', orig_boot_index))
except NoResultFound:
raise exception.VolumeTargetNotFound(target=ident)
return ref
def destroy_volume_target(self, ident):
with _session_for_write():
query = model_query(models.VolumeTarget)
query = add_identity_filter(query, ident)
count = query.delete()
if count == 0:
raise exception.VolumeTargetNotFound(target=ident)

View File

@ -229,3 +229,23 @@ class VolumeConnector(Base):
type = Column(String(32))
connector_id = Column(String(255))
extra = Column(db_types.JsonEncodedDict)
class VolumeTarget(Base):
"""Represents a volume target of a bare metal node."""
__tablename__ = 'volume_targets'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_volumetargets0uuid'),
schema.UniqueConstraint('node_id',
'boot_index',
name='uniq_volumetargets0node_id0boot_index'),
table_args())
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
volume_type = Column(String(64))
properties = Column(db_types.JsonEncodedDict)
boot_index = Column(Integer)
volume_id = Column(String(36))
extra = Column(db_types.JsonEncodedDict)

View File

@ -585,6 +585,35 @@ class MigrationCheckersMixin(object):
self.assertEqual(1, connector['node_id'])
self.assertEqual('{}', connector['extra'])
def _check_1a59178ebdf6(self, engine, data):
targets = db_utils.get_table(engine, 'volume_targets')
col_names = [column.name for column in targets.c]
expected_names = ['created_at', 'updated_at', 'id', 'uuid', 'node_id',
'boot_index', 'extra', 'properties', 'volume_type',
'volume_id']
self.assertEqual(sorted(expected_names), sorted(col_names))
self.assertIsInstance(targets.c.created_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(targets.c.updated_at.type,
sqlalchemy.types.DateTime)
self.assertIsInstance(targets.c.id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(targets.c.uuid.type,
sqlalchemy.types.String)
self.assertIsInstance(targets.c.node_id.type,
sqlalchemy.types.Integer)
self.assertIsInstance(targets.c.boot_index.type,
sqlalchemy.types.Integer)
self.assertIsInstance(targets.c.extra.type,
sqlalchemy.types.TEXT)
self.assertIsInstance(targets.c.properties.type,
sqlalchemy.types.TEXT)
self.assertIsInstance(targets.c.volume_type.type,
sqlalchemy.types.String)
self.assertIsInstance(targets.c.volume_id.type,
sqlalchemy.types.String)
def test_upgrade_and_version(self):
with patch_with_engine(self.engine):
self.migration_api.upgrade('head')

View File

@ -379,6 +379,26 @@ class DbNodeTestCase(base.DbTestCase):
self.assertRaises(exception.VolumeConnectorNotFound,
self.dbapi.get_volume_connector_by_id, connector.id)
def test_volume_target_gets_destroyed_after_destroying_a_node(self):
node = utils.create_test_node()
target = utils.create_test_volume_target(node_id=node.id)
self.dbapi.destroy_node(node.id)
self.assertRaises(exception.VolumeTargetNotFound,
self.dbapi.get_volume_target_by_id, target.id)
def test_volume_target_gets_destroyed_after_destroying_a_node_uuid(self):
node = utils.create_test_node()
target = utils.create_test_volume_target(node_id=node.id)
self.dbapi.destroy_node(node.uuid)
self.assertRaises(exception.VolumeTargetNotFound,
self.dbapi.get_volume_target_by_id, target.id)
def test_update_node(self):
node = utils.create_test_node()

View File

@ -0,0 +1,160 @@
# Copyright 2016 Hitachi, Ltc
#
# 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 VolumeTargets 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 DbVolumeTargetTestCase(base.DbTestCase):
def setUp(self):
# This method creates a volume_target for every test.
super(DbVolumeTargetTestCase, self).setUp()
self.node = db_utils.create_test_node()
self.target = db_utils.create_test_volume_target(node_id=self.node.id)
def test_create_volume_target(self):
info = {'uuid': uuidutils.generate_uuid(),
'node_id': self.node.id,
'boot_index': 1,
'volume_type': 'iscsi',
'volume_id': '12345678'}
target = self.dbapi.create_volume_target(info)
self.assertEqual(info['uuid'], target.uuid)
self.assertEqual(info['node_id'], target.node_id)
self.assertEqual(info['boot_index'], target.boot_index)
self.assertEqual(info['volume_type'], target.volume_type)
self.assertEqual(info['volume_id'], target.volume_id)
self.assertIsNone(target.properties)
self.assertIsNone(target.extra)
def test_create_volume_target_duplicated_nodeid_and_bootindex(self):
self.assertRaises(exception.VolumeTargetBootIndexAlreadyExists,
db_utils.create_test_volume_target,
uuid=uuidutils.generate_uuid(),
node_id=self.target.node_id,
boot_index=self.target.boot_index)
def test_create_volume_target_duplicated_uuid(self):
self.assertRaises(exception.VolumeTargetAlreadyExists,
db_utils.create_test_volume_target,
uuid=self.target.uuid,
node_id=self.node.id,
boot_index=100)
def test_get_volume_target_by_id(self):
res = self.dbapi.get_volume_target_by_id(self.target.id)
self.assertEqual(self.target.volume_type, res.volume_type)
self.assertEqual(self.target.properties, res.properties)
self.assertEqual(self.target.boot_index, res.boot_index)
self.assertRaises(exception.VolumeTargetNotFound,
self.dbapi.get_volume_target_by_id,
100)
def test_get_volume_target_by_uuid(self):
res = self.dbapi.get_volume_target_by_uuid(self.target.uuid)
self.assertEqual(self.target.id, res.id)
self.assertRaises(exception.VolumeTargetNotFound,
self.dbapi.get_volume_target_by_uuid,
'11111111-2222-3333-4444-555555555555')
def _create_list_of_volume_targets(self, num):
uuids = [six.text_type(self.target.uuid)]
for i in range(1, num):
volume_target = db_utils.create_test_volume_target(
uuid=uuidutils.generate_uuid(),
properties={"target_iqn": "iqn.test-%s" % i},
boot_index=i)
uuids.append(six.text_type(volume_target.uuid))
return uuids
def test_get_volume_target_list(self):
uuids = self._create_list_of_volume_targets(6)
res = self.dbapi.get_volume_target_list()
res_uuids = [r.uuid for r in res]
six.assertCountEqual(self, uuids, res_uuids)
def test_get_volume_target_list_sorted(self):
uuids = self._create_list_of_volume_targets(5)
res = self.dbapi.get_volume_target_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_target_list, sort_key='foo')
def test_get_volume_targets_by_node_id(self):
node2 = db_utils.create_test_node(uuid=uuidutils.generate_uuid())
target2 = db_utils.create_test_volume_target(
uuid=uuidutils.generate_uuid(), node_id=node2.id)
self._create_list_of_volume_targets(2)
res = self.dbapi.get_volume_targets_by_node_id(node2.id)
self.assertEqual(1, len(res))
self.assertEqual(target2.uuid, res[0].uuid)
def test_get_volume_targets_by_node_id_that_does_not_exist(self):
self.assertEqual([], self.dbapi.get_volume_targets_by_node_id(99))
def test_update_volume_target(self):
old_boot_index = self.target.boot_index
new_boot_index = old_boot_index + 1
res = self.dbapi.update_volume_target(self.target.id,
{'boot_index': new_boot_index})
self.assertEqual(new_boot_index, res.boot_index)
res = self.dbapi.update_volume_target(self.target.id,
{'boot_index': old_boot_index})
self.assertEqual(old_boot_index, res.boot_index)
def test_update_volume_target_uuid(self):
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.update_volume_target,
self.target.id,
{'uuid': uuidutils.generate_uuid()})
def test_update_volume_target_fails_invalid_id(self):
self.assertRaises(exception.VolumeTargetNotFound,
self.dbapi.update_volume_target,
99,
{'boot_index': 6})
def test_update_volume_target_duplicated_nodeid_and_bootindex(self):
t = db_utils.create_test_volume_target(uuid=uuidutils.generate_uuid(),
boot_index=1)
self.assertRaises(exception.VolumeTargetBootIndexAlreadyExists,
self.dbapi.update_volume_target,
t.uuid,
{'boot_index': self.target.boot_index,
'node_id': self.target.node_id})
def test_destroy_volume_target(self):
self.dbapi.destroy_volume_target(self.target.id)
self.assertRaises(exception.VolumeTargetNotFound,
self.dbapi.get_volume_target_by_id,
self.target.id)
# Ensure that destroy_volume_target returns the expected exception.
self.assertRaises(exception.VolumeTargetNotFound,
self.dbapi.destroy_volume_target,
self.target.id)

View File

@ -346,6 +346,39 @@ def create_test_volume_connector(**kw):
return dbapi.create_volume_connector(connector)
def get_test_volume_target(**kw):
fake_properties = {"target_iqn": "iqn.foo"}
return {
'id': kw.get('id', 789),
'uuid': kw.get('uuid', '1be26c0b-03f2-4d2e-ae87-c02d7f33c781'),
'node_id': kw.get('node_id', 123),
'volume_type': kw.get('volume_type', 'iscsi'),
'properties': kw.get('properties', fake_properties),
'boot_index': kw.get('boot_index', 0),
'volume_id': kw.get('volume_id', '12345678'),
'extra': kw.get('extra', {}),
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at'),
}
def create_test_volume_target(**kw):
"""Create test target entry in DB and return VolumeTarget DB object.
Function to be used to create test VolumeTarget objects in the database.
:param kw: kwargs with overriding values for target's attributes.
:returns: Test VolumeTarget DB object.
"""
target = get_test_volume_target(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del target['id']
dbapi = db_api.get_instance()
return dbapi.create_volume_target(target)
def get_test_chassis(**kw):
return {
'id': kw.get('id', 42),