models.py 9.14 KB
Newer Older
1 2
# coding=utf-8

3
from contextlib import contextmanager
4
import logging
5 6
import uuid

7
from django.db.models import (Model, BooleanField, CharField, DateTimeField,
8
                              ForeignKey)
9
from django.utils import timezone
10
from django.utils.translation import ugettext_lazy as _
11
from model_utils.models import TimeStampedModel
12
from sizefield.models import FileSizeField
13

14
from .tasks import local_tasks, remote_tasks
15
from common.models import ActivityModel, activitycontextimpl
16 17 18 19

logger = logging.getLogger(__name__)


20
class DataStore(Model):
Guba Sándor committed
21

22 23
    """Collection of virtual disks.
    """
24 25 26 27
    name = CharField(max_length=100, unique=True, verbose_name=_('name'))
    path = CharField(max_length=200, unique=True, verbose_name=_('path'))
    hostname = CharField(max_length=40, unique=True,
                         verbose_name=_('hostname'))
Guba Sándor committed
28

29 30 31 32 33 34 35 36 37 38
    class Meta:
        ordering = ['name']
        verbose_name = _('datastore')
        verbose_name_plural = _('datastores')

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.path)


class Disk(TimeStampedModel):
Guba Sándor committed
39

40 41 42 43
    """A virtual disk.
    """
    TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'),
             ('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')]
44 45 46 47 48
    name = CharField(blank=True, max_length=100, verbose_name=_("name"))
    filename = CharField(max_length=256, verbose_name=_("filename"))
    datastore = ForeignKey(DataStore, verbose_name=_("datastore"),
                           help_text=_("The datastore that holds the disk."))
    type = CharField(max_length=10, choices=TYPES)
49
    size = FileSizeField()
50 51 52 53
    base = ForeignKey('self', blank=True, null=True,
                      related_name='derivatives')
    ready = BooleanField(default=False)
    dev_num = CharField(default='a', max_length=1,
54
                        verbose_name=_("device number"))
55
    destroyed = DateTimeField(blank=True, default=None, null=True)
56 57 58 59 60 61

    class Meta:
        ordering = ['name']
        verbose_name = _('disk')
        verbose_name_plural = _('disks')

62 63 64 65 66 67 68 69
    class WrongDiskTypeError(Exception):
        def __init__(self, type):
            self.type = type

        def __str__(self):
            return ("Operation can't be invoked on a disk of type '%s'." %
                    self.type)

70 71 72 73 74 75 76 77 78
    class DiskInUseError(Exception):
        def __init__(self, disk):
            self.disk = disk

        def __str__(self):
            return ("The requested operation can't be performed on disk "
                    "'%s (%s)' because it is in use." %
                    (self.disk.name, self.disk.filename))

79 80 81 82 83 84 85 86 87
    @property
    def path(self):
        return self.datastore.path + '/' + self.filename

    @property
    def format(self):
        return {
            'qcow2-norm': 'qcow2',
            'qcow2-snap': 'qcow2',
88
            'iso': 'raw',
89 90 91 92
            'raw-ro': 'raw',
            'raw-rw': 'raw',
        }[self.type]

93 94 95
    @property
    def device_type(self):
        return {
96 97
            'qcow2-norm': 'vd',
            'qcow2-snap': 'vd',
98
            'iso': 'hd',
99 100 101
            'raw-ro': 'vd',
            'raw-rw': 'vd',
        }[self.type]
102

103 104 105
    def is_in_use(self):
        return self.instance_set.exclude(state='SHUTOFF').exists()

106 107
    def get_exclusive(self):
        """Get an instance of the disk for exclusive usage.
108

109 110 111
        This method manipulates the database only.
        """
        type_mapping = {
112 113 114
            'qcow2-norm': 'qcow2-snap',
            'iso': 'iso',
            'raw-ro': 'raw-rw',
115 116 117 118 119 120 121
        }

        if self.type not in type_mapping.keys():
            raise self.WrongDiskTypeError(self.type)

        filename = self.filename if self.type == 'iso' else str(uuid.uuid4())
        new_type = type_mapping[self.type]
122

123 124 125
        return Disk.objects.create(base=self, datastore=self.datastore,
                                   filename=filename, name=self.name,
                                   size=self.size, type=new_type)
126 127 128

    def get_vmdisk_desc(self):
        return {
129
            'source': self.path,
130 131
            'driver_type': self.format,
            'driver_cache': 'default',
132
            'target_device': self.device_type + self.dev_num,
133
            'disk_device': 'cdrom' if self.type == 'iso' else 'disk'
134 135
        }

136 137 138 139 140 141 142 143 144 145
    def get_disk_desc(self):
        return {
            'name': self.filename,
            'dir': self.datastore.path,
            'format': self.format,
            'size': self.size,
            'base_name': self.base.filename if self.base else None,
            'type': 'snapshot' if self.type == 'qcow2-snap' else 'normal'
        }

146 147 148
    def __unicode__(self):
        return u"%s (#%d)" % (self.name, self.id)

149 150 151 152 153
    def clean(self, *args, **kwargs):
        if self.size == "" and self.base:
            self.size = self.base.size
        super(Disk, self).clean(*args, **kwargs)

154
    def deploy(self, user=None, task_uuid=None):
155 156 157 158 159 160 161 162 163
        """Reify the disk model on the associated data store.

        :param self: the disk model to reify
        :type self: storage.models.Disk

        :return: True if a new reification of the disk has been created;
                 otherwise, False.
        :rtype: bool
        """
164
        if self.ready:
165
            return False
166

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
        with disk_activity(code_suffix='deploy', disk=self,
                           task_uuid=task_uuid, user=user) as act:

            # Delegate create / snapshot jobs
            queue_name = self.datastore.hostname + ".storage"
            disk_desc = self.get_disk_desc()
            if self.type == 'qcow2-snap':
                with act.sub_activity('creating_snapshot'):
                    remote_tasks.snapshot.apply_async(args=[disk_desc],
                                                      queue=queue_name).get()
            else:
                with act.sub_activity('creating_disk'):
                    remote_tasks.create.apply_async(args=[disk_desc],
                                                    queue=queue_name).get()

            self.ready = True
            self.save()
184

185
            return True
186

187
    def deploy_async(self, user=None):
188 189
        """Execute deploy asynchronously.
        """
190 191
        local_tasks.deploy.apply_async(args=[self, user],
                                       queue="localhost.man")
192

193
    def destroy(self, user=None, task_uuid=None):
194
        # TODO add activity logging
195
        self.destroyed = timezone.now()
196 197
        self.save()

198
    def destroy_async(self, user=None):
199 200
        local_tasks.destroy.apply_async(args=[self, user],
                                        queue='localhost.man')
201

202
    def restore(self, user=None, task_uuid=None):
203
        """Restore destroyed disk.
204 205 206 207 208 209 210 211
        """
        # TODO
        pass

    def restore_async(self, user=None):
        local_tasks.restore.apply_async(args=[self, user],
                                        queue='localhost.man')

212
    def save_as(self, user=None, task_uuid=None):
213 214 215 216 217 218 219 220 221 222 223 224
        mapping = {
            'qcow2-snap': ('qcow2-norm', self.base),
        }
        if self.type not in mapping.keys():
            raise self.WrongDiskTypeError(self.type)

        if self.is_in_use():
            raise self.DiskInUseError(self)

        # from this point on, the caller has to guarantee that the disk is not
        # going to be used until the operation is complete

225 226 227 228 229 230 231
        with disk_activity(code_suffix='save_as', disk=self,
                           task_uuid=task_uuid, user=user):

            filename = str(uuid.uuid4())
            new_type, new_base = mapping[self.type]

            disk = Disk.objects.create(base=new_base, datastore=self.datastore,
232
                                       filename=filename, name=self.name,
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
                                       size=self.size, type=new_type)

            queue_name = self.datastore.hostname + ".storage"
            remote_tasks.merge.apply_async(args=[self.get_disk_desc(),
                                                 disk.get_disk_desc()],
                                           queue=queue_name).get()

            disk.ready = True
            disk.save()

            return disk


class DiskActivity(ActivityModel):
    disk = ForeignKey(Disk, related_name='activity_log',
                      help_text=_('Disk this activity works on.'),
                      verbose_name=_('disk'))

    @classmethod
    def create(cls, code_suffix, instance, task_uuid=None, user=None):
        act = cls(activity_code='storage.Disk.' + code_suffix,
                  instance=instance, parent=None, started=timezone.now(),
                  task_uuid=task_uuid, user=user)
256
        act.save()
257
        return act
258

259 260 261 262 263 264 265
    def create_sub(self, code_suffix, task_uuid=None):
        act = DiskActivity(
            activity_code=self.activity_code + '.' + code_suffix,
            instance=self.instance, parent=self, started=timezone.now(),
            task_uuid=task_uuid, user=self.user)
        act.save()
        return act
266

267 268 269
    @contextmanager
    def sub_activity(self, code_suffix, task_uuid=None):
        act = self.create_sub(code_suffix, task_uuid)
270
        return activitycontextimpl(act)
271

272

273 274 275
@contextmanager
def disk_activity(code_suffix, instance, task_uuid=None, user=None):
    act = DiskActivity.create(code_suffix, instance, task_uuid, user)
276
    return activitycontextimpl(act)