Revert "Revert "Add support of backing store volumes from templates""

Migration works correctly, DB should be prepared.

This reverts commit 466f235371.

Change-Id: Ida4e40c4cb2e7f954861465c100b46cf8196c2e5
This commit is contained in:
Dennis Dmitriev 2016-07-21 12:05:15 +00:00
parent 466f235371
commit b248474542
10 changed files with 222 additions and 29 deletions

View File

@ -676,7 +676,7 @@ class LibvirtVolume(Volume):
"""Note: This class is imported as Volume at .__init__.py """ """Note: This class is imported as Volume at .__init__.py """
uuid = ParamField() uuid = ParamField()
capacity = ParamField(default=None) capacity = ParamField(default=None) # in gigabytes
format = ParamField(default='qcow2', choices=('qcow2', 'raw')) format = ParamField(default='qcow2', choices=('qcow2', 'raw'))
source_image = ParamField(default=None) source_image = ParamField(default=None)
serial = ParamField() serial = ParamField()
@ -695,24 +695,49 @@ class LibvirtVolume(Volume):
@retry(libvirt.libvirtError) @retry(libvirt.libvirtError)
def define(self): def define(self):
name = underscored( # Generate libvirt volume name
deepgetattr(self, 'node.group.environment.name'), if self.node:
deepgetattr(self, 'node.name'), name = underscored(
self.name, deepgetattr(self, 'node.group.environment.name'),
) deepgetattr(self, 'node.name'),
self.name,
)
elif self.group:
name = underscored(
deepgetattr(self, 'group.environment.name'),
deepgetattr(self, 'group.name'),
self.name,
)
else:
raise DevopsError("Can't craete volume that is not "
"associated with any node or group")
# Find backing store format and path
backing_store_path = None backing_store_path = None
backing_store_format = None backing_store_format = None
if self.backing_store is not None: if self.backing_store:
if not self.backing_store.exists():
raise DevopsError(
"Can't create volume {!r}. backing_store volume {!r} does "
"not exists.".format(self.name, self.backing_store.name))
backing_store_path = self.backing_store.get_path() backing_store_path = self.backing_store.get_path()
backing_store_format = self.backing_store.format backing_store_format = self.backing_store.format
capacity = int((self.capacity or 0) * 1024 ** 3) # Select capacity
if self.source_image is not None: if self.capacity:
file_size = get_file_size(self.source_image) # if capacity specified, use it first
if file_size > capacity: capacity = int(self.capacity * 1024 ** 3)
capacity = file_size elif self.source_image is not None:
# limit capacity to the sorse image file size
capacity = get_file_size(self.source_image)
elif self.backing_store:
# limit capacity to backing_store capacity
capacity = self.backing_store.get_capacity()
else:
raise DevopsError("Can't create volume {!r}: no capacity or "
"source_image specified".format(self.name))
# Generate xml
pool_name = self.driver.storage_pool_name pool_name = self.driver.storage_pool_name
pool = self.driver.conn.storagePoolLookupByName(pool_name) pool = self.driver.conn.storagePoolLookupByName(pool_name)
xml = LibvirtXMLBuilder.build_volume_xml( xml = LibvirtXMLBuilder.build_volume_xml(
@ -722,12 +747,19 @@ class LibvirtVolume(Volume):
backing_store_path=backing_store_path, backing_store_path=backing_store_path,
backing_store_format=backing_store_format, backing_store_format=backing_store_format,
) )
# Define volume
libvirt_volume = pool.createXML(xml, 0) libvirt_volume = pool.createXML(xml, 0)
# Save uuid
self.uuid = libvirt_volume.key() self.uuid = libvirt_volume.key()
# Set serial and wwn
if not self.serial: if not self.serial:
self.serial = uuid.uuid4().hex self.serial = uuid.uuid4().hex
if not self.wwn: if not self.wwn:
self.wwn = '0' + ''.join(uuid.uuid4().hex)[:15] self.wwn = '0' + ''.join(uuid.uuid4().hex)[:15]
super(LibvirtVolume, self).define() super(LibvirtVolume, self).define()
# Upload predefined image to the volume # Upload predefined image to the volume
@ -742,7 +774,7 @@ class LibvirtVolume(Volume):
super(LibvirtVolume, self).remove() super(LibvirtVolume, self).remove()
def get_capacity(self): def get_capacity(self):
"""Get volume capacity""" """Get volume capacity in bytes"""
return self._libvirt_volume.info()[1] return self._libvirt_volume.info()[1]
def get_format(self): def get_format(self):
@ -753,8 +785,9 @@ class LibvirtVolume(Volume):
return self._libvirt_volume.path() return self._libvirt_volume.path()
def fill_from_exist(self): def fill_from_exist(self):
self.capacity = self.get_capacity() msg = 'LibvirtVolume.fill_from_exist() is deprecated and do nothing'
self.format = self.get_format() warn(msg, DeprecationWarning)
logger.debug(msg)
@retry(libvirt.libvirtError, count=2) @retry(libvirt.libvirtError, count=2)
def upload(self, path, capacity=0): def upload(self, path, capacity=0):
@ -812,25 +845,31 @@ class LibvirtVolume(Volume):
cls = self.driver.get_model_class('Volume') cls = self.driver.get_model_class('Volume')
return cls.objects.create( return cls.objects.create(
name=name, name=name,
capacity=self.capacity,
node=self.node, node=self.node,
format=self.format, format=self.format,
backing_store=self, backing_store=self,
) )
# TO REWRITE, LEGACY, for fuel-qa compatibility # LEGACY, for fuel-qa compatibility
# Used for EXTERNAL SNAPSHOTS
@classmethod @classmethod
def volume_get_predefined(cls, uuid): def volume_get_predefined(cls, uuid):
"""Get predefined volume """Get predefined volume
:rtype : Volume :rtype : Volume
""" """
msg = ('LibvirtVolume.volume_get_predefined() is deprecated. '
'Please use Volumes associated with Groups')
warn(msg, DeprecationWarning)
logger.debug(msg)
try: try:
volume = cls.objects.get(uuid=uuid) volume = cls.objects.get(uuid=uuid)
except cls.DoesNotExist: except cls.DoesNotExist:
volume = cls(uuid=uuid) volume = cls(uuid=uuid)
volume.fill_from_exist() if not volume.exists():
raise DevopsError(
'Predefined volume {!r} not found'.format(uuid))
volume.format = volume.get_format()
volume.save() volume.save()
return volume return volume

View File

@ -159,6 +159,7 @@ class Migration(migrations.Migration):
('params', jsonfield.fields.JSONField(default={})), ('params', jsonfield.fields.JSONField(default={})),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('backing_store', models.ForeignKey(to='devops.Volume', null=True)), ('backing_store', models.ForeignKey(to='devops.Volume', null=True)),
('group', models.ForeignKey(to='devops.Group', null=True)),
('node', models.ForeignKey(to='devops.Node', null=True)), ('node', models.ForeignKey(to='devops.Node', null=True)),
], ],
options={ options={
@ -202,7 +203,7 @@ class Migration(migrations.Migration):
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='volume', name='volume',
unique_together=set([('name', 'node')]), unique_together=set([('name', 'group'), ('name', 'node')]),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='node', name='node',

View File

@ -158,7 +158,7 @@ class ParamField(ParamFieldBase):
* to set default value. * to set default value.
* to limit values using a list of allowed values. * to limit values using a list of allowed values.
Examples of ussage:: Examples of usage::
class A(ParamedModel): class A(ParamedModel):
foo = ParamField(default=10) foo = ParamField(default=10)
@ -229,9 +229,9 @@ class ParamMultiField(ParamFieldBase):
self.subfields = [] self.subfields = []
for name, field in subfields.items(): for name, field in subfields.items():
if not isinstance(field, (ParamField, ParamMultiField)): if not isinstance(field, ParamFieldBase):
raise DevopsError('field "{}" has wrong type;' raise DevopsError('field "{}" has wrong type;'
' should be ParamField or ParamMultiField' ' should be ParamFieldBase subclass instance'
''.format(name)) ''.format(name))
field.set_param_key(name) field.set_param_key(name)
self.subfields.append(field) self.subfields.append(field)
@ -322,6 +322,10 @@ class ParamedModelQuerySet(query.QuerySet):
# NOTE(astudenov): no support for 'gt', 'lt', 'in' # NOTE(astudenov): no support for 'gt', 'lt', 'in'
# and other django's filter stuff # and other django's filter stuff
if not isinstance(item, self.model):
# skip other classes
continue
item_val = deepgetattr(item, key, splitter='__', item_val = deepgetattr(item, key, splitter='__',
do_raise=True) do_raise=True)
if item_val != value: if item_val != value:

View File

@ -186,6 +186,8 @@ class Environment(BaseModel):
def define(self): def define(self):
for group in self.get_groups(): for group in self.get_groups():
group.define_networks() group.define_networks()
for group in self.get_groups():
group.define_volumes()
for group in self.get_groups(): for group in self.get_groups():
group.define_nodes() group.define_nodes()
@ -344,6 +346,11 @@ class Environment(BaseModel):
# Connect nodes to already created networks # Connect nodes to already created networks
for group_data in groups: for group_data in groups:
group = environment.get_group(name=group_data['name']) group = environment.get_group(name=group_data['name'])
# add group volumes
group.add_volumes(
group_data.get('group_volumes', []))
# add nodes # add nodes
group.add_nodes( group.add_nodes(
group_data.get('nodes', [])) group_data.get('nodes', []))

View File

@ -22,6 +22,7 @@ from devops.models.base import BaseModel
from devops.models.network import L2NetworkDevice from devops.models.network import L2NetworkDevice
from devops.models.network import NetworkPool from devops.models.network import NetworkPool
from devops.models.node import Node from devops.models.node import Node
from devops.models.node import Volume
class Group(BaseModel): class Group(BaseModel):
@ -87,6 +88,10 @@ class Group(BaseModel):
def has_snapshot(self, name): def has_snapshot(self, name):
return all(n.has_snapshot(name) for n in self.get_nodes()) return all(n.has_snapshot(name) for n in self.get_nodes())
def define_volumes(self):
for volume in self.get_volumes():
volume.define()
def define_networks(self): def define_networks(self):
for l2_network_device in self.get_l2_network_devices(): for l2_network_device in self.get_l2_network_devices():
l2_network_device.define() l2_network_device.define()
@ -113,6 +118,9 @@ class Group(BaseModel):
for node in self.get_nodes(): for node in self.get_nodes():
node.erase() node.erase()
for volume in self.get_volumes():
volume.erase()
for l2_network_device in self.get_l2_network_devices(): for l2_network_device in self.get_l2_network_devices():
l2_network_device.erase() l2_network_device.erase()
self.delete() self.delete()
@ -214,3 +222,26 @@ class Group(BaseModel):
name=name, name=name,
address_pool=address_pool, address_pool=address_pool,
) )
def add_volumes(self, volumes):
for vol_params in volumes:
self.add_volume(
**vol_params
)
def add_volume(self, name, **params):
cls = self.driver.get_model_class('Volume')
return cls.objects.create(
group=self,
name=name,
**params
)
def get_volume(self, **kwargs):
try:
return self.volume_set.get(**kwargs)
except Volume.DoesNotExist:
raise DevopsObjNotFound(Volume, **kwargs)
def get_volumes(self, **kwargs):
return self.volume_set.filter(**kwargs)

View File

@ -373,6 +373,12 @@ class Node(six.with_metaclass(ExtendableNodeType, ParamedModel, BaseModel)):
# NEW # NEW
def add_volume(self, name, device='disk', bus='virtio', **params): def add_volume(self, name, device='disk', bus='virtio', **params):
cls = self.driver.get_model_class('Volume') cls = self.driver.get_model_class('Volume')
if 'backing_store' in params:
# Backing storage volume have to be defined in group
params['backing_store'] = self.group.get_volume(
name=params['backing_store'])
volume = cls.objects.create( volume = cls.objects.create(
node=self, node=self,
name=name, name=name,

View File

@ -21,17 +21,18 @@ from devops.models.base import ParamField
class Volume(ParamedModel, BaseModel): class Volume(ParamedModel, BaseModel):
class Meta(object): class Meta(object):
unique_together = ('name', 'node') unique_together = (('name', 'node'), ('name', 'group'))
db_table = 'devops_volume' db_table = 'devops_volume'
app_label = 'devops' app_label = 'devops'
backing_store = models.ForeignKey('self', null=True) backing_store = models.ForeignKey('self', null=True)
name = models.CharField(max_length=255, unique=False, null=False) name = models.CharField(max_length=255, unique=False, null=False)
node = models.ForeignKey('Node', null=True) node = models.ForeignKey('Node', null=True)
group = models.ForeignKey('Group', null=True)
@property @property
def driver(self): def driver(self):
return self.node.driver return self.node.driver if self.node else self.group.driver
def define(self, *args, **kwargs): def define(self, *args, **kwargs):
self.save() self.save()

View File

@ -76,8 +76,8 @@ class TestCloudImage(LibvirtTestCase):
cloud_init_volume_name='iso', cloud_init_volume_name='iso',
cloud_init_iface_up='enp0s3') cloud_init_iface_up='enp0s3')
self.system_volume = self.node.add_volume(name='system') self.system_volume = self.node.add_volume(name='system', capacity=10)
self.iso_volume = self.node.add_volume(name='iso') self.iso_volume = self.node.add_volume(name='iso', capacity=5)
self.adm_iface = self.node.add_interface( self.adm_iface = self.node.add_interface(
label='enp0s3', label='enp0s3',

View File

@ -0,0 +1,104 @@
# Copyright 2016 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 collections
import mock
from devops.models import Environment
from devops.tests.driver.libvirt.base import LibvirtTestCase
class TestLibvirtVolumeBackingStore(LibvirtTestCase):
def setUp(self):
super(TestLibvirtVolumeBackingStore, self).setUp()
self.sleep_mock = self.patch('devops.helpers.retry.sleep')
self.open_mock = mock.mock_open(read_data='image_data')
self.patch('devops.driver.libvirt.libvirt_driver.open',
self.open_mock, create=True)
self.os_mock = self.patch('devops.helpers.helpers.os')
Size = collections.namedtuple('Size', ['st_size'])
self.file_sizes = {
'/tmp/admin.iso': Size(st_size=500),
}
self.os_mock.stat.side_effect = self.file_sizes.get
self.env = Environment.create('test_env')
self.group1 = self.env.add_group(
group_name='test_group1',
driver_name='devops.driver.libvirt',
connection_string='test:///default',
storage_pool_name='default-pool')
self.group2 = self.env.add_group(
group_name='test_group2',
driver_name='devops.driver.libvirt',
connection_string='test:///default',
storage_pool_name='default-pool')
self.node1 = self.group1.add_node(
name='test_node1',
role='default',
architecture='i686',
hypervisor='test',
)
self.node2 = self.group1.add_node(
name='test_node2',
role='default',
architecture='i686',
hypervisor='test',
)
def test_backing_store(self):
parent_vol1 = self.group1.add_volume(
name='parent_volume',
format='qcow2',
capacity=10,
source_image='/tmp/admin.iso',
)
parent_vol1.define()
parent_vol2 = self.group2.add_volume(
name='parent_volume',
format='qcow2',
capacity=10,
source_image='/tmp/admin.iso',
)
parent_vol2.define()
child_volume1 = self.node1.add_volume(
name='test_volume1',
backing_store='parent_volume',
capacity=20,
)
child_volume1.define()
assert child_volume1.capacity == 20
assert child_volume1.backing_store is not None
assert child_volume1.backing_store.pk == parent_vol1.pk
child_volume2 = self.node2.add_volume(
name='test_volume2',
backing_store='parent_volume',
capacity=20,
)
child_volume2.define()
assert child_volume2.capacity == 20
assert child_volume2.backing_store is not None
assert child_volume2.backing_store.pk == parent_vol1.pk

View File

@ -65,8 +65,8 @@ class TestCentosMasterExt(LibvirtTestCase):
architecture='x86_64', architecture='x86_64',
hypervisor='test') hypervisor='test')
self.system_volume = self.node.add_volume(name='system') self.system_volume = self.node.add_volume(name='system', capacity=10)
self.iso_volume = self.node.add_volume(name='iso') self.iso_volume = self.node.add_volume(name='iso', capacity=5)
self.adm_iface = self.node.add_interface( self.adm_iface = self.node.add_interface(
label='enp0s3', label='enp0s3',