models.py 11.9 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 acl.models import AclBase
15
from .tasks import local_tasks, remote_tasks
16
from common.models import ActivityModel, activitycontextimpl, WorkerNotFound
17 18 19 20

logger = logging.getLogger(__name__)


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

23 24
    """Collection of virtual disks.
    """
25 26 27 28
    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
29

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

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

38
    def get_remote_queue_name(self, queue_id):
39 40 41 42 43 44
        logger.debug("Checking for storage queue %s.%s",
                     self.hostname, queue_id)
        if local_tasks.check_queue(self.hostname, queue_id):
            return self.hostname + '.' + queue_id
        else:
            raise WorkerNotFound()
45

46

47
class Disk(AclBase, TimeStampedModel):
Guba Sándor committed
48

49 50
    """A virtual disk.
    """
51 52 53 54 55
    ACL_LEVELS = (
        ('user', _('user')),          # see all details
        ('operator', _('operator')),
        ('owner', _('owner')),        # superuser, can delete, delegate perms
    )
56 57
    TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'),
             ('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')]
58 59 60 61 62
    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)
63
    size = FileSizeField()
64 65
    base = ForeignKey('self', blank=True, null=True,
                      related_name='derivatives')
66 67
    ready = BooleanField(default=False,
                         help_text=_("The associated resource is ready."))
68
    dev_num = CharField(default='a', max_length=1,
69
                        verbose_name=_("device number"))
70
    destroyed = DateTimeField(blank=True, default=None, null=True)
71 72 73 74 75 76

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

77 78
    class WrongDiskTypeError(Exception):

79 80 81 82 83 84 85 86
        def __init__(self, type, message=None):
            if message is None:
                message = ("Operation can't be invoked on a disk of type '%s'."
                           % type)

            Exception.__init__(self, message)

            self.type = type
87

88 89
    class DiskInUseError(Exception):

90 91 92 93
        def __init__(self, disk, message=None):
            if message is None:
                message = ("The requested operation can't be performed on "
                           "disk '%s (%s)' because it is in use." %
Dudás Ádám committed
94
                           (disk.name, disk.filename))
95 96 97 98

            Exception.__init__(self, message)

            self.disk = disk
99

100 101 102 103 104 105 106 107 108
    @property
    def path(self):
        return self.datastore.path + '/' + self.filename

    @property
    def format(self):
        return {
            'qcow2-norm': 'qcow2',
            'qcow2-snap': 'qcow2',
109
            'iso': 'raw',
110 111 112 113
            'raw-ro': 'raw',
            'raw-rw': 'raw',
        }[self.type]

114 115 116
    @property
    def device_type(self):
        return {
117 118
            'qcow2-norm': 'vd',
            'qcow2-snap': 'vd',
119
            'iso': 'hd',
120 121 122
            'raw-ro': 'vd',
            'raw-rw': 'vd',
        }[self.type]
123

124
    def is_in_use(self):
125
        return any([i.state != 'STOPPED' for i in self.instance_set.all()])
126

127 128
    def get_exclusive(self):
        """Get an instance of the disk for exclusive usage.
129

130 131 132
        This method manipulates the database only.
        """
        type_mapping = {
133 134 135
            'qcow2-norm': 'qcow2-snap',
            'iso': 'iso',
            'raw-ro': 'raw-rw',
136 137 138 139 140 141 142
        }

        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]
143

144 145 146
        return Disk.objects.create(base=self, datastore=self.datastore,
                                   filename=filename, name=self.name,
                                   size=self.size, type=new_type)
147 148 149

    def get_vmdisk_desc(self):
        return {
150
            'source': self.path,
151 152
            'driver_type': self.format,
            'driver_cache': 'default',
153
            'target_device': self.device_type + self.dev_num,
154
            'disk_device': 'cdrom' if self.type == 'iso' else 'disk'
155 156
        }

157 158 159 160 161 162 163 164 165 166
    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'
        }

167 168 169 170 171 172
    def get_remote_queue_name(self, queue_id):
        if self.datastore:
            return self.datastore.get_remote_queue_name(queue_id)
        else:
            return None

173 174 175
    def __unicode__(self):
        return u"%s (#%d)" % (self.name, self.id)

176 177 178 179 180
    def clean(self, *args, **kwargs):
        if self.size == "" and self.base:
            self.size = self.base.size
        super(Disk, self).clean(*args, **kwargs)

181
    def deploy(self, user=None, task_uuid=None):
182 183 184 185 186
        """Reify the disk model on the associated data store.

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

187 188 189 190 191 192 193
        :param user: The user who's issuing the command.
        :type user: django.contrib.auth.models.User

        :param task_uuid: The task's UUID, if the command is being executed
                          asynchronously.
        :type task_uuid: str

194 195 196 197
        :return: True if a new reification of the disk has been created;
                 otherwise, False.
        :rtype: bool
        """
198 199 200 201
        if self.destroyed:
            self.destroyed = None
            self.save()

202
        if self.ready:
203
            return False
204

205 206 207 208
        with disk_activity(code_suffix='deploy', disk=self,
                           task_uuid=task_uuid, user=user) as act:

            # Delegate create / snapshot jobs
209
            queue_name = self.get_remote_queue_name('storage')
210 211 212 213 214 215 216 217 218 219 220 221
            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()
222

223
            return True
224

225
    def deploy_async(self, user=None):
226 227
        """Execute deploy asynchronously.
        """
228 229
        return local_tasks.deploy.apply_async(args=[self, user],
                                              queue="localhost.man")
230

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
    @classmethod
    def create_empty(cls, params={}, user=None):
        disk = cls()
        disk.__dict__.update(params)
        disk.save()
        return disk

    @classmethod
    def create_from_url_async(cls, url, params, user=None):
        return local_tasks.create_from_url.apply_async(args=[cls, url, params,
                                                             user],
                                                       queue='localhost.man')

    @classmethod
    def create_from_url(cls, url, params={}, user=None, task_uuid=None):
        disk = cls()
        disk.filename = str(uuid.uuid4())
        disk.type = "iso"
        disk.size = 1
        disk.datastore = DataStore.objects.all()[0]
        disk.__dict__.update(params)
        disk.save()
        queue_name = disk.get_remote_queue_name('storage')

        def __onabort(activity, error):
            activity.disk.delete()
            raise error

        with disk_activity(code_suffix='download', disk=disk,
                           task_uuid=task_uuid, user=user):
            size = remote_tasks.download.apply_async(
                kwargs={'url': url, 'disk': disk.get_disk_desc()},
                queue=queue_name).get()
            disk.size = size
            disk.save()

267
    def destroy(self, user=None, task_uuid=None):
268 269 270
        if self.destroyed:
            return False

271 272 273 274
        with disk_activity(code_suffix='destroy', disk=self,
                           task_uuid=task_uuid, user=user):
            self.destroyed = timezone.now()
            self.save()
275

276
            return True
277

278
    def destroy_async(self, user=None):
279 280
        """Execute destroy asynchronously.
        """
281 282
        return local_tasks.destroy.apply_async(args=[self, user],
                                               queue='localhost.man')
283

284
    def restore(self, user=None, task_uuid=None):
285
        """Restore destroyed disk.
286 287 288 289 290 291 292 293
        """
        # TODO
        pass

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

294
    def save_as(self, user=None, task_uuid=None):
295 296 297 298 299 300 301 302 303 304 305 306
        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

307 308 309 310 311 312 313
        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,
314
                                       filename=filename, name=self.name,
315 316
                                       size=self.size, type=new_type)

317
            queue_name = self.get_remote_queue_name('storage')
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
            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
334
    def create(cls, code_suffix, disk, task_uuid=None, user=None):
335
        act = cls(activity_code='storage.Disk.' + code_suffix,
336
                  disk=disk, parent=None, started=timezone.now(),
337
                  task_uuid=task_uuid, user=user)
338
        act.save()
339
        return act
340

341 342 343
    def create_sub(self, code_suffix, task_uuid=None):
        act = DiskActivity(
            activity_code=self.activity_code + '.' + code_suffix,
344
            disk=self.disk, parent=self, started=timezone.now(),
345 346 347
            task_uuid=task_uuid, user=self.user)
        act.save()
        return act
348

349 350 351
    @contextmanager
    def sub_activity(self, code_suffix, task_uuid=None):
        act = self.create_sub(code_suffix, task_uuid)
352
        return activitycontextimpl(act)
353

354

355
@contextmanager
356 357
def disk_activity(code_suffix, disk, task_uuid=None, user=None):
    act = DiskActivity.create(code_suffix, disk, task_uuid, user)
358
    return activitycontextimpl(act)