Merge "Add shared_targets flag to Volumes"

This commit is contained in:
Zuul 2017-12-01 03:43:41 +00:00 committed by Gerrit Code Review
commit adbe9b4f02
11 changed files with 188 additions and 3 deletions

View File

@ -73,6 +73,7 @@ from cinder import context
from cinder import db
from cinder.db import migration as db_migration
from cinder.db.sqlalchemy import api as db_api
from cinder.db.sqlalchemy import models
from cinder import exception
from cinder.i18n import _
from cinder import objects
@ -85,6 +86,48 @@ from cinder.volume import utils as vutils
CONF = cfg.CONF
def _get_non_shared_target_hosts(ctxt):
hosts = []
numvols_needing_update = 0
rpc.init(CONF)
rpcapi = volume_rpcapi.VolumeAPI()
services = objects.ServiceList.get_all_by_topic(ctxt, 'cinder-volume')
for service in services:
capabilities = rpcapi.get_capabilities(ctxt, service.host, True)
if not capabilities.get('shared_targets', True):
hosts.append(service.host)
numvols_needing_update += db_api.model_query(
ctxt, models.Volume).filter_by(
shared_targets=True,
service_uuid=service.uuid).count()
return hosts, numvols_needing_update
def shared_targets_online_data_migration(ctxt, max_count):
"""Update existing volumes shared_targets flag based on capabilities."""
non_shared_hosts = []
completed = 0
non_shared_hosts, total_vols_to_update = _get_non_shared_target_hosts(ctxt)
for host in non_shared_hosts:
# We use the api call here instead of going direct to
# db query to take advantage of parsing out the host
# correctly
vrefs = db_api.volume_get_all_by_host(
ctxt, host,
filters={'shared_targets': True})
if len(vrefs) > max_count:
del vrefs[-(len(vrefs) - max_count):]
max_count -= len(vrefs)
for v in vrefs:
db.volume_update(
ctxt, v['id'],
{'shared_targets': 0})
completed += 1
return total_vols_to_update, completed
# Decorators for actions
def args(*args, **kwargs):
def _decorator(func):
@ -209,6 +252,7 @@ class DbCommands(object):
db.service_uuids_online_data_migration,
db.backup_service_online_migration,
db.volume_service_uuids_online_data_migration,
shared_targets_online_data_migration,
)
def __init__(self):

View File

@ -0,0 +1,27 @@
# 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 sqlalchemy import Boolean, Column, MetaData, Table
def upgrade(migrate_engine):
"""Add shared_targets column to Volumes."""
meta = MetaData()
meta.bind = migrate_engine
volumes = Table('volumes', meta, autoload=True)
# NOTE(jdg): We use a default of True because it's harmless for a device
# that does NOT use shared_targets to be treated as if it does
shared_targets = Column('shared_targets',
Boolean,
default=True)
volumes.create_column(shared_targets)

View File

@ -320,6 +320,7 @@ class Volume(BASE, CinderBase):
backref="volumes",
foreign_keys=service_uuid,
primaryjoin='Volume.service_uuid == Service.uuid')
shared_targets = Column(Boolean, default=True) # make an FK of service?
class VolumeMetadata(BASE, CinderBase):

View File

@ -140,6 +140,7 @@ OBJ_VERSIONS.add('1.29', {'Service': '1.6'})
OBJ_VERSIONS.add('1.30', {'RequestSpec': '1.2'})
OBJ_VERSIONS.add('1.31', {'Volume': '1.7'})
OBJ_VERSIONS.add('1.32', {'RequestSpec': '1.3'})
OBJ_VERSIONS.add('1.33', {'Volume': '1.8'})
class CinderObjectRegistry(base.VersionedObjectRegistry):

View File

@ -61,7 +61,8 @@ class Volume(cleanable.CinderCleanableObject, base.CinderObject,
# Version 1.5: Added group
# Version 1.6: This object is now cleanable (adds rows to workers table)
# Version 1.7: Added service_uuid
VERSION = '1.7'
# Version 1.8: Added shared_targets
VERSION = '1.8'
OPTIONAL_FIELDS = ('metadata', 'admin_metadata', 'glance_metadata',
'volume_type', 'volume_attachment', 'consistencygroup',
@ -126,6 +127,7 @@ class Volume(cleanable.CinderCleanableObject, base.CinderObject,
'snapshots': fields.ObjectField('SnapshotList', nullable=True),
'group': fields.ObjectField('Group', nullable=True),
'service_uuid': fields.StringField(nullable=True),
'shared_targets': fields.BooleanField(default=True, nullable=True),
}
# NOTE(thangp): obj_extra_fields is used to hold properties that are not

View File

@ -384,6 +384,11 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
self.assertEqual(sorted(['deleted', 'service_uuid']),
sorted(index_columns))
def _check_115(self, engine, data):
volumes = db_utils.get_table(engine, 'volumes')
self.assertIsInstance(volumes.c.shared_targets.type,
self.BOOL_TYPE)
def test_walk_versions(self):
self.walk_versions(False, False)
self.assert_each_foreign_key_is_part_of_an_index()

View File

@ -47,7 +47,7 @@ object_data = {
'ServiceList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'Snapshot': '1.5-ac1cdbd5b89588f6a8f44afdf6b8b201',
'SnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'Volume': '1.7-0845c5b7b826a4e9019f3684c2f9b132',
'Volume': '1.8-6cf615b72269cef48702a2a5c2940159',
'VolumeList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'VolumeAttachment': '1.2-b68b357a1756582b706006ea9de40c9a',
'VolumeAttachmentList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',

View File

@ -38,6 +38,7 @@ from cinder.cmd import scheduler as cinder_scheduler
from cinder.cmd import volume as cinder_volume
from cinder.cmd import volume_usage_audit
from cinder import context
from cinder.db.sqlalchemy import api as sqlalchemy_api
from cinder import exception
from cinder.objects import fields
from cinder import test
@ -45,7 +46,9 @@ from cinder.tests.unit import fake_cluster
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_service
from cinder.tests.unit import fake_volume
from cinder.tests.unit import utils
from cinder import version
from cinder.volume import rpcapi
CONF = cfg.CONF
@ -2004,3 +2007,97 @@ class TestCinderVolumeUsageAuditCmd(test.TestCase):
mock.call(ctxt, backup1, 'delete.end',
extra_usage_info=extra_info_backup_delete)
])
class TestVolumeSharedTargetsOnlineMigration(test.TestCase):
"""Unit tests for cinder.db.api.service_*."""
def setUp(self):
super(TestVolumeSharedTargetsOnlineMigration, self).setUp()
def _get_minimum_rpc_version_mock(ctxt, binary):
binary_map = {
'cinder-volume': rpcapi.VolumeAPI,
}
return binary_map[binary].RPC_API_VERSION
self.patch('cinder.objects.Service.get_minimum_rpc_version',
side_effect=_get_minimum_rpc_version_mock)
@mock.patch('cinder.objects.Service.get_minimum_obj_version',
return_value='1.8')
def test_shared_targets_migrations(self, mock_version):
"""Ensure we can update the column."""
ctxt = context.get_admin_context()
sqlalchemy_api.volume_create(
ctxt,
{'host': 'host1@lvm-driver1#lvm-driver1',
'service_uuid': 'f080f895-cff2-4eb3-9c61-050c060b59ad'})
# Create another one correct setting
sqlalchemy_api.volume_create(
ctxt,
{'host': 'host1@lvm-driver1#lvm-driver1',
'shared_targets': False,
'service_uuid': 'f080f895-cff2-4eb3-9c61-050c060b59ad'})
# Need a service to query
values = {
'host': 'host1@lvm-driver1',
'binary': 'cinder-volume',
'topic': 'cinder-volume',
'uuid': 'f080f895-cff2-4eb3-9c61-050c060b59ad'}
utils.create_service(ctxt, values)
# Run the migration and verify that we updated 1 entry
with mock.patch('cinder.volume.rpcapi.VolumeAPI.get_capabilities',
return_value={'shared_targets': False}):
total, updated = (
cinder_manage.shared_targets_online_data_migration(
ctxt, 10))
self.assertEqual(1, total)
self.assertEqual(1, updated)
@mock.patch('cinder.objects.Service.get_minimum_obj_version',
return_value='1.8')
def test_shared_targets_migrations_with_limit(self, mock_version):
"""Ensure we update in batches."""
ctxt = context.get_admin_context()
# default value in db for shared_targets on a volume
# is True, so don't need to set it here explicitly
sqlalchemy_api.volume_create(
ctxt,
{'host': 'host1@lvm-driver1#lvm-driver1',
'service_uuid': 'f080f895-cff2-4eb3-9c61-050c060b59ad'})
sqlalchemy_api.volume_create(
ctxt,
{'host': 'host1@lvm-driver1#lvm-driver1',
'service_uuid': 'f080f895-cff2-4eb3-9c61-050c060b59ad'})
sqlalchemy_api.volume_create(
ctxt,
{'host': 'host1@lvm-driver1#lvm-driver1',
'service_uuid': 'f080f895-cff2-4eb3-9c61-050c060b59ad'})
values = {
'host': 'host1@lvm-driver1',
'binary': 'cinder-volume',
'topic': 'cinder-volume',
'uuid': 'f080f895-cff2-4eb3-9c61-050c060b59ad'}
utils.create_service(ctxt, values)
# Run the migration and verify that we updated 1 entry
with mock.patch('cinder.volume.rpcapi.VolumeAPI.get_capabilities',
return_value={'shared_targets': False}):
total, updated = (
cinder_manage.shared_targets_online_data_migration(
ctxt, 2))
self.assertEqual(3, total)
self.assertEqual(2, updated)
total, updated = (
cinder_manage.shared_targets_online_data_migration(
ctxt, 2))
self.assertEqual(1, total)
self.assertEqual(1, updated)

View File

@ -279,6 +279,7 @@ class LVMVolumeDriver(driver.VolumeDriver):
multiattach=False
))
data["pools"].append(single_pool)
data["shared_targets"] = False
# Check availability of sparse volume copy.
data['sparse_copy_volume'] = self._sparse_copy_volume

View File

@ -1845,6 +1845,7 @@ class SolidFireDriver(san.SanISCSIDriver):
results['deDuplicationPercent'])
data['thin_provision_percent'] = (
results['thinProvisioningPercent'])
data['shared_targets'] = False
self.cluster_stats = data
def initialize_connection(self, volume, connector):

View File

@ -680,7 +680,13 @@ class VolumeManager(manager.CleanableManager,
# volume stats as these are decremented on delete.
self._update_allocated_capacity(volume)
updates = {'service_uuid': self.service_uuid}
shared_targets = (
1
if self.driver.capabilities.get('shared_targets', True)
else 0)
updates = {'service_uuid': self.service_uuid,
'shared_targets': shared_targets}
volume.update(updates)
volume.save()