# coding=utf-8 from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.core import signing from django.db import models from django.db import transaction from django.db.models.signals import post_save from django import forms from django.utils.translation import ugettext_lazy as _ from firewall.models import Host, Rule, Vlan, settings from firewall.tasks import reload_firewall_lock from one.util import keygen from school.models import Person import subprocess, tempfile, os, stat, re pwgen = User.objects.make_random_password """ User creation hook: create cloud details object """ def create_user_profile(sender, instance, created, **kwargs): if created: d = UserCloudDetails(user=instance) d.clean() d.save() post_save.connect(create_user_profile, sender=User) """ Cloud related details of a user """ class UserCloudDetails(models.Model): user = models.ForeignKey(User, null=False, blank=False, unique=True, verbose_name=_('user')) smb_password = models.CharField(max_length=20, verbose_name=_('Samba password'), help_text=_('Generated password for accessing store from Windows.')) ssh_key = models.ForeignKey('SshKey', null=True, verbose_name=_('SSH key (public)'), help_text=_('Generated SSH public key for accessing store from Linux.')) ssh_private_key = models.TextField(verbose_name=_('SSH key (private)'), help_text=_('Generated SSH private key for accessing store from Linux.')) """ Delete old SSH key pair and generate new one. """ def reset_keys(self): pri, pub = keygen() self.ssh_private_key = pri try: self.ssh_key.key = pub except: self.ssh_key = SshKey(user=self.user, key=pub) self.ssh_key.save() """ Generate new Samba password. """ def reset_smb(self): self.smb_password = pwgen() """ Generate key pair and Samba password if needed. """ def clean(self): super(UserCloudDetails, self).clean() if not self.ssh_key: self.reset_keys() if not self.smb_password or len(self.smb_password) == 0: self.reset_smb() """ Validate OpenSSH keys (length and type). """ class OpenSshKeyValidator(object): valid_types = ['ssh-rsa', 'ssh-dsa'] def __init__(self, types=None): if types is not None: self.valid_types = types def __call__(self, value): try: value = "%s comment" % value type, key_string, comment = value.split(None, 2) if type not in self.valid_types: raise ValidationError(_('OpenSSH key type %s is not supported.') % type) data = base64.decodestring(key_string) int_len = 4 str_len = struct.unpack('>I', data[:int_len])[0] if not data[int_len:int_len+str_len] == type: raise except ValidationError: raise except: raise ValidationError(_('Invalid OpenSSH public key.')) """ SSH public key (in OpenSSH format). """ class SshKey(models.Model): user = models.ForeignKey(User, null=False, blank=False) key = models.CharField(max_length=2000, verbose_name=_('SSH key'), help_text=_('<a href="/info/ssh/">SSH public key in OpenSSH format</a> used for shell login ' '(2048+ bit RSA preferred). Example: <code>ssh-rsa AAAAB...QtQ== ' 'john</code>.'), validators=[OpenSshKeyValidator()]) def __unicode__(self): try: keycomment = self.key.split(None, 2)[2] except: keycomment = _("unnamed") return u"%s (%s)" % (keycomment, self.user) """ Virtual disks automatically synchronized with OpenNebula. """ class Disk(models.Model): name = models.CharField(max_length=100, unique=True, verbose_name=_('name')) """ Get and register virtual disks from OpenNebula. """ @classmethod def update(cls): import subprocess proc = subprocess.Popen(["/opt/occi.sh", "storage", "list"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() from xml.dom.minidom import parse, parseString x = parseString(out) with transaction.commit_on_success(): l = [] for d in x.getElementsByTagName("STORAGE"): id = int(d.getAttributeNode('href').nodeValue.split('/')[-1]) name=d.getAttributeNode('name').nodeValue try: d = Disk.objects.get(id=id) d.name=name d.save() except: Disk(id=id, name=name).save() l.append(id) Disk.objects.exclude(id__in=l).delete() def __unicode__(self): return u"%s (#%d)" % (self.name, self.id) class Meta: ordering = ['name'] """ Virtual networks automatically synchronized with OpenNebula. """ class Network(models.Model): name = models.CharField(max_length=100, unique=True, verbose_name=_('name')) nat = models.BooleanField(verbose_name=_('NAT'), help_text=_('If network address translation is done.')) public = models.BooleanField(verbose_name=_('public'), help_text=_('If internet gateway is available.')) """ Get and register virtual networks from OpenNebula. """ @classmethod def update(cls): import subprocess proc = subprocess.Popen(["/opt/occi.sh", "network", "list"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() from xml.dom.minidom import parse, parseString x = parseString(out) with transaction.commit_on_success(): l = [] for d in x.getElementsByTagName("NETWORK"): id = int(d.getAttributeNode('href').nodeValue.split('/')[-1]) name=d.getAttributeNode('name').nodeValue try: n = Network.objects.get(id=id) n.name = name n.save() except: Network(id=id, name=name).save() l.append(id) cls.objects.exclude(id__in=l).delete() def __unicode__(self): return self.name class Meta: ordering = ['name'] """ Instance types in OCCI configuration (manually synchronized). """ class InstanceType(models.Model): name = models.CharField(max_length=100, unique=True, verbose_name=_('name')) CPU = models.IntegerField(help_text=_('CPU cores.')) RAM = models.IntegerField(help_text=_('Mebibytes of memory.')) def __unicode__(self): return u"%s" % self.name """ Virtual machine template specifying OS, disk, type and network. """ class Template(models.Model): name = models.CharField(max_length=100, unique=True, verbose_name=_('name')) access_type = models.CharField(max_length=10, choices=[('rdp', 'rdp'), ('nx', 'nx'), ('ssh', 'ssh')], verbose_name=_('access method')) disk = models.ForeignKey(Disk, verbose_name=_('disk')) instance_type = models.ForeignKey(InstanceType, verbose_name=_('instance type')) network = models.ForeignKey(Network, verbose_name=_('network')) owner = models.ForeignKey(User, verbose_name=_('owner')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) def __unicode__(self): return self.name class Meta: verbose_name = _('template') verbose_name_plural = _('templates') """ Virtual machine instance. """ class Instance(models.Model): name = models.CharField(max_length=100, unique=True, verbose_name=_('név'), null=True, blank=True) ip = models.IPAddressField(blank=True, null=True, verbose_name=_('IP address')) template = models.ForeignKey(Template, verbose_name=_('template')) owner = models.ForeignKey(User, verbose_name=_('owner')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created_at')) state = models.CharField(max_length=20, choices=[('DEPLOYABLE', _('deployable')), ('PENDING', _('pending')), ('DONE', _('done')), ('ACTIVE', _('active')), ('UNKNOWN', _('unknown')), ('SUSPENDED', _('suspended')), ('FAILED', _('failed'))], default='DEPLOYABLE') active_since = models.DateTimeField(null=True, blank=True, verbose_name=_('active since'), help_text=_('Time stamp of successful boot report.')) firewall_host = models.ForeignKey(Host, blank=True, null=True, verbose_name=_('host in firewall')) pw = models.CharField(max_length=20, verbose_name=_('password'), help_text=_('Original password of instance')) one_id = models.IntegerField(unique=True, blank=True, null=True, verbose_name=_('OpenNebula ID')) """ Get public port number for default access method. """ def get_port(self): proto = self.template.access_type if self.template.network.nat: return {"rdp": 23000, "nx": 22000, "ssh": 22000}[proto] + int(self.ip.split('.')[3]) else: return {"rdp": 3389, "nx": 22, "ssh": 22}[proto] """ Get public hostname. """ def get_connect_host(self): if self.template.network.nat: return 'cloud' else: return self.ip """ Get access parameters in URI format. """ def get_connect_uri(self): try: proto = self.template.access_type port = self.get_port() host = self.get_connect_host() pw = self.pw return "%(proto)s:cloud:%(pw)s:%(host)s:%(port)d" % {"port": port, "proto": proto, "host": self.firewall_host.pub_ipv4, "pw": pw} except: return def __unicode__(self): return self.name """ Get and update VM state from OpenNebula. """ def update_state(self): import subprocess if not self.one_id: return proc = subprocess.Popen(["/opt/occi.sh", "compute", "show", "%d"%self.one_id], stdout=subprocess.PIPE) (out, err) = proc.communicate() x = None try: from xml.dom.minidom import parse, parseString x = parseString(out) self.vnet_ip = x.getElementsByTagName("IP")[0].childNodes[0].nodeValue.split('.')[3] state = x.getElementsByTagName("STATE")[0].childNodes[0].nodeValue if self.state == 'PENDING' and state == 'ACTIVE': from datetime import datetime self.active_since = datetime.now() self.state = state except: self.state = 'UNKNOWN' self.save() return x """ Get age of VM in seconds. """ def get_age(self): from datetime import datetime age = 0 try: age = (datetime.now().replace(tzinfo=None) - self.active_since.replace(tzinfo=None)).seconds except: pass return age @models.permalink def get_absolute_url(self): return ('vm_show', None, {'iid':self.id}) """ Submit a new instance to OpenNebula. """ @classmethod def submit(cls, template, owner): from django.template.defaultfilters import escape out = "" inst = Instance(pw=pwgen(), template=template, owner=owner) inst.save() with tempfile.NamedTemporaryFile(delete=False) as f: os.chmod(f.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IROTH) token = signing.dumps(inst.id, salt='activate') try: details = owner.userclouddetails_set.all()[0] except: details = UserCloudDetails(user=owner) details.save() tpl = u""" <COMPUTE> <NAME>%(name)s</NAME> <INSTANCE_TYPE href="http://www.opennebula.org/instance_type/%(instance)s"/> <DISK> <STORAGE href="http://www.opennebula.org/storage/%(disk)d"/> </DISK> <NIC> <NETWORK href="http://www.opennebula.org/network/%(net)d"/> </NIC> <CONTEXT> <SOURCE>web</SOURCE> <HOSTNAME>cloud-$VMID</HOSTNAME> <NEPTUN>%(neptun)s</NEPTUN> <USERPW>%(pw)s</USERPW> <SMBPW>%(smbpw)s</SMBPW> <SSHPRIV>%(sshkey)s</SSHPRIV> <BOOTURL>%(booturl)s</BOOTURL> <SERVER>store.cloud.ik.bme.hu</SERVER> </CONTEXT> </COMPUTE>""" % {"name": u"%s %d" % (owner.username, inst.id), "instance": template.instance_type, "disk": template.disk.id, "net": template.network.id, "pw": escape(inst.pw), "smbpw": escape(details.smb_password), "sshkey": escape(details.ssh_private_key), "neptun": escape(owner.username), "booturl": "http://cloud.ik.bme.hu/b/%s/" % token, } f.write(tpl) f.close() import subprocess proc = subprocess.Popen(["/opt/occi.sh", "compute", "create", f.name], stdout=subprocess.PIPE) (out, err) = proc.communicate() os.unlink(f.name) from xml.dom.minidom import parse, parseString try: x = parseString(out) except: raise Exception("Unable to create VM instance.") inst.one_id = int(x.getElementsByTagName("ID")[0].childNodes[0].nodeValue) inst.ip = x.getElementsByTagName("IP")[0].childNodes[0].nodeValue inst.name = "%(neptun)s %(template)s (%(id)d)" % {'neptun': owner.username, 'template': template.name, 'id': inst.one_id} inst.save() inst.update_state() host = Host(vlan=Vlan.objects.get(name=template.network.name), owner=owner, shared_ip=True) host.hostname = u"id-%d_user-%s" % (inst.id, owner.username) host.mac = x.getElementsByTagName("MAC")[0].childNodes[0].nodeValue host.ipv4 = inst.ip host.pub_ipv4 = Vlan.objects.get(name=template.network.name).snat_ip host.save() host.enable_net() host.add_port("tcp", inst.get_port(), {"rdp": 3389, "nx": 22, "ssh": 22}[inst.template.access_type]) inst.firewall_host=host inst.save() reload_firewall_lock() return inst """ Delete host in OpenNebula. """ def delete(self): proc = subprocess.Popen(["/opt/occi.sh", "compute", "delete", "%d"%self.one_id], stdout=subprocess.PIPE) (out, err) = proc.communicate() self.firewall_host.delete() reload_firewall_lock() """ Change host state in OpenNebula. """ def _change_state(self, new_state): from django.template.defaultfilters import escape out = "" with tempfile.NamedTemporaryFile(delete=False) as f: os.chmod(f.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IROTH) tpl = u""" <COMPUTE> <ID>%(id)d</ID> <STATE>%(state)s</STATE> </COMPUTE>""" % {"id": self.one_id, "state": new_state} f.write(tpl) f.close() import subprocess proc = subprocess.Popen(["/opt/occi.sh", "compute", "update", f.name], stdout=subprocess.PIPE) (out, err) = proc.communicate() os.unlink(f.name) print "out: " + out def stop(self): self._change_state("STOPPED") def resume(self): self._change_state("RESUME") def poweroff(self): self._change_state("POWEROFF") def restart(self): self._change_state("RESTART") class Meta: verbose_name = _('instance') verbose_name_plural = _('instances')