Commit f3aa35f9 by Dudás Ádám

storage, vm: change activity log format to hierarchical

parent 8cd8d2bf
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'DiskActivity.state'
db.delete_column(u'storage_diskactivity', 'state')
# Adding field 'DiskActivity.parent'
db.add_column(u'storage_diskactivity', 'parent',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['storage.DiskActivity'], null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Adding field 'DiskActivity.state'
db.add_column(u'storage_diskactivity', 'state',
self.gf('django.db.models.fields.CharField')(default='PENDING', max_length=50),
keep_default=False)
# Deleting field 'DiskActivity.parent'
db.delete_column(u'storage_diskactivity', 'parent_id')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'storage.diskactivity': {
'Meta': {'object_name': 'DiskActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'disk': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_log'", 'to': u"orm['storage.Disk']"}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DiskActivity']", 'null': 'True', 'blank': 'True'}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['storage']
\ No newline at end of file
# coding=utf-8 # coding=utf-8
from contextlib import contextmanager
import logging import logging
import uuid import uuid
from django.contrib.auth.models import User
from django.db.models import (Model, BooleanField, CharField, DateTimeField, from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey, TextField) ForeignKey)
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from sizefield.models import FileSizeField from sizefield.models import FileSizeField
from .tasks import local_tasks, remote_tasks from .tasks import local_tasks, remote_tasks
from common.models import ActivityModel, activitycontextimpl
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -160,30 +161,24 @@ class Disk(TimeStampedModel): ...@@ -160,30 +161,24 @@ class Disk(TimeStampedModel):
if self.ready: if self.ready:
return False return False
act = DiskActivity(activity_code='storage.Disk.deploy') with disk_activity(code_suffix='deploy', disk=self,
act.disk = self task_uuid=task_uuid, user=user) as act:
act.started = timezone.now()
act.state = 'PENDING'
act.task_uuid = task_uuid
act.user = user
act.save()
# Delegate create / snapshot jobs # Delegate create / snapshot jobs
queue_name = self.datastore.hostname + ".storage" queue_name = self.datastore.hostname + ".storage"
disk_desc = self.get_disk_desc() disk_desc = self.get_disk_desc()
if self.type == 'qcow2-snap': if self.type == 'qcow2-snap':
act.update_state('CREATING SNAPSHOT') with act.sub_activity('creating_snapshot'):
remote_tasks.snapshot.apply_async(args=[disk_desc], remote_tasks.snapshot.apply_async(args=[disk_desc],
queue=queue_name).get() queue=queue_name).get()
else: else:
act.update_state('CREATING DISK') with act.sub_activity('creating_disk'):
remote_tasks.create.apply_async(args=[disk_desc], remote_tasks.create.apply_async(args=[disk_desc],
queue=queue_name).get() queue=queue_name).get()
self.ready = True self.ready = True
self.save() self.save()
act.finish('SUCCESS')
return True return True
def deploy_async(self, user=None): def deploy_async(self, user=None):
...@@ -224,13 +219,8 @@ class Disk(TimeStampedModel): ...@@ -224,13 +219,8 @@ class Disk(TimeStampedModel):
# from this point on, the caller has to guarantee that the disk is not # from this point on, the caller has to guarantee that the disk is not
# going to be used until the operation is complete # going to be used until the operation is complete
act = DiskActivity(activity_code='storage.Disk.save_as') with disk_activity(code_suffix='save_as', disk=self,
act.disk = self task_uuid=task_uuid, user=user):
act.started = timezone.now()
act.state = 'PENDING'
act.task_uuid = task_uuid
act.user = user
act.save()
filename = str(uuid.uuid4()) filename = str(uuid.uuid4())
new_type, new_base = mapping[self.type] new_type, new_base = mapping[self.type]
...@@ -246,31 +236,38 @@ class Disk(TimeStampedModel): ...@@ -246,31 +236,38 @@ class Disk(TimeStampedModel):
disk.ready = True disk.ready = True
disk.save() disk.save()
act.finish('SUCCESS')
return disk return disk
class DiskActivity(TimeStampedModel): class DiskActivity(ActivityModel):
activity_code = CharField(verbose_name=_('activity_code'), max_length=100) disk = ForeignKey(Disk, related_name='activity_log',
task_uuid = CharField(verbose_name=_('task_uuid'), blank=True, help_text=_('Disk this activity works on.'),
max_length=50, null=True, unique=True) verbose_name=_('disk'))
disk = ForeignKey(Disk, verbose_name=_('disk'),
related_name='activity_log')
user = ForeignKey(User, verbose_name=_('user'), blank=True, null=True)
started = DateTimeField(verbose_name=_('started'), blank=True, null=True)
finished = DateTimeField(verbose_name=_('finished'), blank=True, null=True)
result = TextField(verbose_name=_('result'), blank=True, null=True)
state = CharField(verbose_name=_('state'), default='PENDING',
max_length=50)
def update_state(self, new_state):
self.state = new_state
self.save()
def finish(self, result=None): @classmethod
if not self.finished: def create(cls, code_suffix, instance, task_uuid=None, user=None):
self.finished = timezone.now() act = cls(activity_code='storage.Disk.' + code_suffix,
self.result = result instance=instance, parent=None, started=timezone.now(),
self.state = 'COMPLETED' task_uuid=task_uuid, user=user)
self.save() act.save()
return act
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
@contextmanager
def sub_activity(self, code_suffix, task_uuid=None):
act = self.create_sub(code_suffix, task_uuid)
activitycontextimpl(act)
@contextmanager
def disk_activity(code_suffix, instance, task_uuid=None, user=None):
act = DiskActivity.create(code_suffix, instance, task_uuid, user)
activitycontextimpl(act)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'InstanceActivity.state'
db.delete_column(u'vm_instanceactivity', 'state')
# Adding field 'InstanceActivity.parent'
db.add_column(u'vm_instanceactivity', 'parent',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vm.InstanceActivity'], null=True, blank=True),
keep_default=False)
# Deleting field 'NodeActivity.status'
db.delete_column(u'vm_nodeactivity', 'status')
# Adding field 'NodeActivity.parent'
db.add_column(u'vm_nodeactivity', 'parent',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vm.NodeActivity'], null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Adding field 'InstanceActivity.state'
db.add_column(u'vm_instanceactivity', 'state',
self.gf('django.db.models.fields.CharField')(default='PENDING', max_length=50),
keep_default=False)
# Deleting field 'InstanceActivity.parent'
db.delete_column(u'vm_instanceactivity', 'parent_id')
# Adding field 'NodeActivity.status'
db.add_column(u'vm_nodeactivity', 'status',
self.gf('django.db.models.fields.CharField')(default='PENDING', max_length=50),
keep_default=False)
# Deleting field 'NodeActivity.parent'
db.delete_column(u'vm_nodeactivity', 'parent_id')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'pub_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'interface': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'vm.instance': {
'Meta': {'ordering': "['pk']", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destoryed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'NOSTATE'", 'max_length': '20'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'instance_set'", 'null': 'True', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {})
},
u'vm.instanceactivity': {
'Meta': {'object_name': 'InstanceActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_log'", 'to': u"orm['vm.Instance']"}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceActivity']", 'null': 'True', 'blank': 'True'}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "['name']", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'template_set'", 'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'NEW'", 'max_length': '10'}),
'system': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
u'vm.interface': {
'Meta': {'object_name': 'Interface'},
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'interface_set'", 'to': u"orm['vm.Instance']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vm_interface'", 'to': u"orm['firewall.Vlan']"})
},
u'vm.interfacetemplate': {
'Meta': {'object_name': 'InterfaceTemplate'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'interface_set'", 'to': u"orm['vm.InstanceTemplate']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'vm.lease': {
'Meta': {'ordering': "['name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {})
},
u'vm.namedbaseresourceconfig': {
'Meta': {'object_name': 'NamedBaseResourceConfig'},
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
u'vm.node': {
'Meta': {'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {})
},
u'vm.nodeactivity': {
'Meta': {'object_name': 'NodeActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_log'", 'to': u"orm['vm.Node']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.NodeActivity']", 'null': 'True', 'blank': 'True'}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['vm']
\ No newline at end of file
from contextlib import contextmanager
from datetime import timedelta from datetime import timedelta
from importlib import import_module from importlib import import_module
import logging import logging
...@@ -13,6 +14,7 @@ from model_utils.models import TimeStampedModel ...@@ -13,6 +14,7 @@ from model_utils.models import TimeStampedModel
from netaddr import EUI, mac_unix from netaddr import EUI, mac_unix
from .tasks import local_tasks, vm_tasks, net_tasks from .tasks import local_tasks, vm_tasks, net_tasks
from common.models import ActivityModel, activitycontextimpl
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk from storage.models import Disk
...@@ -99,38 +101,37 @@ class Node(TimeStampedModel): ...@@ -99,38 +101,37 @@ class Node(TimeStampedModel):
return self.name return self.name
class NodeActivity(TimeStampedModel): class NodeActivity(ActivityModel):
activity_code = CharField(verbose_name=_('activity code'), node = ForeignKey(Node, related_name='activity_log',
max_length=100) # TODO help_text=_('Node this activity works on.'),
task_uuid = CharField(verbose_name=_('task_uuid'), blank=True, verbose_name=_('node'))
max_length=50, null=True, unique=True, help_text=_(
'Celery task unique identifier.'))
node = ForeignKey(Node, verbose_name=_('node'),
related_name='activity_log',
help_text=_('Node this activity works on.'))
user = ForeignKey(User, verbose_name=_('user'), blank=True, null=True,
help_text=_('The person who started this activity.'))
started = DateTimeField(verbose_name=_('started at'),
blank=True, null=True,
help_text=_('Time of activity initiation.'))
finished = DateTimeField(verbose_name=_('finished at'),
blank=True, null=True,
help_text=_('Time of activity finalization.'))
result = TextField(verbose_name=_('result'), blank=True, null=True,
help_text=_('Human readable result of activity.'))
status = CharField(verbose_name=_('status'), default='PENDING',
max_length=50, help_text=_('Actual state of activity'))
def update_state(self, new_state):
self.state = new_state
self.save()
def finish(self, result=None): @classmethod
if not self.finished: def create(cls, code_suffix, node, task_uuid=None, user=None):
self.finished = timezone.now() act = cls(activity_code='vm.Node.' + code_suffix,
self.result = result node=node, parent=None, started=timezone.now(),
self.state = 'COMPLETED' task_uuid=task_uuid, user=user)
self.save() act.save()
return act
def create_sub(self, code_suffix, task_uuid=None):
act = NodeActivity(
activity_code=self.activity_code + '.' + code_suffix,
node=self.node, parent=self, started=timezone.now(),
task_uuid=task_uuid, user=self.user)
act.save()
return act
@contextmanager
def sub_activity(self, code_suffix, task_uuid=None):
act = self.create_sub(code_suffix, task_uuid)
activitycontextimpl(act)
@contextmanager
def node_activity(code_suffix, node, task_uuid=None, user=None):
act = InstanceActivity.create(code_suffix, node, task_uuid, user)
activitycontextimpl(act)
class Lease(Model): class Lease(Model):
...@@ -533,41 +534,35 @@ class Instance(BaseResourceConfigModel, TimeStampedModel): ...@@ -533,41 +534,35 @@ class Instance(BaseResourceConfigModel, TimeStampedModel):
asynchronously. asynchronously.
:type task_uuid: str :type task_uuid: str
""" """
act = InstanceActivity(activity_code='vm.Instance.deploy') with instance_activity(code_suffix='deploy', instance=self,
act.instance = self task_uuid=task_uuid, user=user) as act:
act.user = user
act.started = timezone.now()
act.task_uuid = task_uuid
act.save()
# Schedule # Schedule
self.node = scheduler.get_node(self, Node.objects.all()) self.node = scheduler.get_node(self, Node.objects.all())
self.save() self.save()
# Create virtual images # Deploy virtual images
act.update_state('DEPLOYING DISKS') with act.sub_activity('deploying_disks'):
for disk in self.disks.all(): for disk in self.disks.all():
disk.deploy() disk.deploy()
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
# Deploy VM on remote machine # Deploy VM on remote machine
act.update_state('DEPLOYING VM') with act.sub_activity('deploying_vm'):
vm_tasks.create.apply_async(args=[self.get_vm_desc()], vm_tasks.create.apply_async(args=[self.get_vm_desc()],
queue=queue_name).get() queue=queue_name).get()
# Estabilish network connection (vmdriver) # Estabilish network connection (vmdriver)
act.update_state('DEPLOYING NET') with act.sub_activity('deploying_net'):
for net in self.interface_set.all(): for net in self.interface_set.all():
net.deploy() net.deploy()
# Resume vm # Resume vm
act.update_state('BOOTING') with act.sub_activity('booting'):
vm_tasks.resume.apply_async(args=[self.vm_name], vm_tasks.resume.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name).get()
act.finish(result='SUCCESS')
def deploy_async(self, user=None): def deploy_async(self, user=None):
"""Execute deploy asynchronously. """Execute deploy asynchronously.
""" """
...@@ -587,32 +582,27 @@ class Instance(BaseResourceConfigModel, TimeStampedModel): ...@@ -587,32 +582,27 @@ class Instance(BaseResourceConfigModel, TimeStampedModel):
asynchronously. asynchronously.
:type task_uuid: str :type task_uuid: str
""" """
act = InstanceActivity(activity_code='vm.Instance.destroy') with instance_activity(code_suffix='destroy', instance=self,
act.instance = self task_uuid=task_uuid, user=user) as act:
act.user = user
act.started = timezone.now()
act.task_uuid = task_uuid
act.save()
# Destroy networks # Destroy networks
act.update_state('DESTROYING NET') with act.sub_activity('destroying_net'):
for net in self.interface_set.all(): for net in self.interface_set.all():
net.destroy() net.destroy()
# Destroy virtual machine # Destroy virtual machine
act.update_state('DESTROYING VM') with act.sub_activity('destroying_vm'):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.destroy.apply_async(args=[self.vm_name], vm_tasks.destroy.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name).get()
# Destroy disks # Destroy disks
act.update_state('DESTROYING DISKS') with act.sub_activity('destroying_disks'):
for disk in self.disks.all(): for disk in self.disks.all():
disk.destroy() disk.destroy()
self.destoryed = timezone.now() self.destoryed = timezone.now()
self.save() self.save()
act.finish(result="SUCCESS")
def destroy_async(self, user=None): def destroy_async(self, user=None):
"""Execute destroy asynchronously. """Execute destroy asynchronously.
...@@ -623,19 +613,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel): ...@@ -623,19 +613,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel):
def sleep(self, user=None, task_uuid=None): def sleep(self, user=None, task_uuid=None):
"""Suspend virtual machine with memory dump. """Suspend virtual machine with memory dump.
""" """
act = InstanceActivity(activity_code='vm.Instance.sleep') with instance_activity(code_suffix='sleep', instance=self,
act.instance = self task_uuid=task_uuid, user=user):
act.user = user
act.started = timezone.now()
act.task_uuid = task_uuid
act.save()
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.sleep.apply_async(args=[self.vm_name, self.mem_dump], vm_tasks.sleep.apply_async(args=[self.vm_name, self.mem_dump],
queue=queue_name).get() queue=queue_name).get()
act.finish(result='SUCCESS')
def sleep_async(self, user=None): def sleep_async(self, user=None):
"""Execute sleep asynchronously. """Execute sleep asynchronously.
""" """
...@@ -643,19 +627,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel): ...@@ -643,19 +627,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel):
queue="localhost.man") queue="localhost.man")
def wake_up(self, user=None, task_uuid=None): def wake_up(self, user=None, task_uuid=None):
act = InstanceActivity(activity_code='vm.Instance.wake_up') with instance_activity(code_suffix='wake_up', instance=self,
act.instance = self task_uuid=task_uuid, user=user):
act.user = user
act.started = timezone.now()
act.task_uuid = task_uuid
act.save()
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.resume.apply_async(args=[self.vm_name, self.dump_mem], vm_tasks.resume.apply_async(args=[self.vm_name, self.dump_mem],
queue=queue_name).get() queue=queue_name).get()
act.finish(result='SUCCESS')
def wake_up_async(self, user=None): def wake_up_async(self, user=None):
"""Execute wake_up asynchronously. """Execute wake_up asynchronously.
""" """
...@@ -665,19 +643,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel): ...@@ -665,19 +643,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel):
def shutdown(self, user=None, task_uuid=None): def shutdown(self, user=None, task_uuid=None):
"""Shutdown virtual machine with ACPI signal. """Shutdown virtual machine with ACPI signal.
""" """
act = InstanceActivity(activity_code='vm.Instance.shutdown') with instance_activity(code_suffix='shutdown', instance=self,
act.instance = self task_uuid=task_uuid, user=user):
act.user = user
act.started = timezone.now()
act.task_uuid = task_uuid
act.save()
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.shutdown.apply_async(args=[self.vm_name], vm_tasks.shutdown.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name).get()
act.finish(result='SUCCESS')
def shutdown_async(self, user=None): def shutdown_async(self, user=None):
"""Execute shutdown asynchronously. """Execute shutdown asynchronously.
""" """
...@@ -687,19 +659,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel): ...@@ -687,19 +659,13 @@ class Instance(BaseResourceConfigModel, TimeStampedModel):
def reset(self, user=None, task_uuid=None): def reset(self, user=None, task_uuid=None):
"""Reset virtual machine (reset button) """Reset virtual machine (reset button)
""" """
act = InstanceActivity(activity_code='vm.Instance.reset') with instance_activity(code_suffix='reset', instance=self,
act.instance = self task_uuid=task_uuid, user=user):
act.user = user
act.started = timezone.now()
act.task_uuid = task_uuid
act.save()
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.restart.apply_async(args=[self.vm_name], vm_tasks.restart.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name).get()
act.finish(result='SUCCESS')
def reset_async(self, user=None): def reset_async(self, user=None):
"""Execute reset asynchronously. """Execute reset asynchronously.
""" """
...@@ -709,48 +675,51 @@ class Instance(BaseResourceConfigModel, TimeStampedModel): ...@@ -709,48 +675,51 @@ class Instance(BaseResourceConfigModel, TimeStampedModel):
def reboot(self, user=None, task_uuid=None): def reboot(self, user=None, task_uuid=None):
"""Reboot virtual machine with Ctrl+Alt+Del signal. """Reboot virtual machine with Ctrl+Alt+Del signal.
""" """
act = InstanceActivity(activity_code='vm.Instance.reboot') with instance_activity(code_suffix='reboot', instance=self,
act.instance = self task_uuid=task_uuid, user=user):
act.user = user
act.started = timezone.now()
act.task_uuid = task_uuid
act.save()
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name('vm')
vm_tasks.reboot.apply_async(args=[self.vm_name], vm_tasks.reboot.apply_async(args=[self.vm_name],
queue=queue_name).get() queue=queue_name).get()
act.finish(result='SUCCESS')
def reboot_async(self, user=None): def reboot_async(self, user=None):
"""Execute reboot asynchronously. """Execute reboot asynchronously.
""" """
return local_tasks.reboot.apply_async(args=[self, user], return local_tasks.reboot.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
class InstanceActivity(TimeStampedModel):
activity_code = CharField(verbose_name=_('activity_code'), max_length=100)
task_uuid = CharField(verbose_name=_('task_uuid'), blank=True,
max_length=50, null=True, unique=True)
instance = ForeignKey(Instance, verbose_name=_('instance'),
related_name='activity_log')
user = ForeignKey(User, verbose_name=_('user'), blank=True, null=True)
started = DateTimeField(verbose_name=_('started'), blank=True, null=True)
finished = DateTimeField(verbose_name=_('finished'), blank=True, null=True)
result = TextField(verbose_name=_('result'), blank=True, null=True)
state = CharField(verbose_name=_('state'),
default='PENDING', max_length=50)
def update_state(self, new_state):
self.state = new_state
self.save()
def finish(self, result=None): class InstanceActivity(ActivityModel):
if not self.finished: instance = ForeignKey(Instance, related_name='activity_log',
self.finished = timezone.now() help_text=_('Instance this activity works on.'),
self.result = result verbose_name=_('instance'))
self.state = 'COMPLETED'
self.save() @classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None):
act = cls(activity_code='vm.Instance.' + code_suffix,
instance=instance, parent=None, started=timezone.now(),
task_uuid=task_uuid, user=user)
act.save()
return act
def create_sub(self, code_suffix, task_uuid=None):
act = InstanceActivity(
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
@contextmanager
def sub_activity(self, code_suffix, task_uuid=None):
act = self.create_sub(code_suffix, task_uuid)
activitycontextimpl(act)
@contextmanager
def instance_activity(code_suffix, instance, task_uuid=None, user=None):
act = InstanceActivity.create(code_suffix, instance, task_uuid, user)
activitycontextimpl(act)
class Interface(Model): class Interface(Model):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment