Commit 156fc6bc by x

Merge branch 'master' of ssh://giccero.cloud.ik.bme.hu/cloud

parents 8dc2613b 81cd77d5
#!/usr/bin/python
#TODO File permission checks
from bottle import route, run, request, static_file, abort, redirect, app
import json, os, shutil
import uuid
import subprocess
from pwd import getpwnam
ROOT_WWW_FOLDER='/var/www'
ROOT_BIN_FOLDER='/opt/store-server'
SITE_URL='https://store.cloud.ik.bme.hu'
USER_MANAGER='UserManager.sh'
@route('/')
def index():
return "It works!"
#@route('/<neptun:re:[a-zA-Z0-9]{6}>', method='GET')
@route('/<neptun>', method='GET')
def neptun_GET(neptun):
home_path = '/home/'+neptun+'/home'
if os.path.exists(home_path) != True:
abort(401, 'The requested user does not exist!')
else:
statistics=getQuotaStatus(neptun)
return { 'Used' : statistics[0], 'Soft' : statistics[1], 'Hard' : statistics[2]}
@route('/<neptun>', method='POST')
def neptun_POST(neptun):
#Check if user avaiable (home folder ready)
home_path = '/home/'+neptun+'/home'
if os.path.exists(home_path) != True:
abort(401, 'The requested user does not exist!')
else:
#Parse post
#LISTING
if request.json['CMD'] == 'LIST':
list_path = home_path+request.json['PATH']
if os.path.exists(list_path) != True:
abort(404, "Path not found!")
else:
return list_directory(home_path,list_path)
#DOWNLOAD LINK GENERATOR
elif request.json['CMD'] == 'DOWNLOAD':
dl_path = home_path+'/'+request.json['PATH']
dl_path = os.path.normpath(dl_path)
if not dl_path.startswith(home_path):
abort(400, 'Invalid download path.')
if( os.path.isfile(dl_path) ):
dl_hash = str(uuid.uuid4())
os.symlink(dl_path, ROOT_WWW_FOLDER+'/'+dl_hash)
#Debug
#redirect('http://store.cloud.ik.bme.hu:8080/dl/'+dl_hash)
return json.dumps({'LINK' : SITE_URL+'/dl/'+dl_hash})
else:
abort(400, 'Can\'t download folder')
#UPLOAD
elif request.json['CMD'] == 'UPLOAD':
up_path = home_path+'/'+request.json['PATH']
up_path = os.path.normpath(up_path)
if not up_path.startswith(home_path):
abort(400, 'Invalid upload path.')
if os.path.exists(up_path) == True and os.path.isdir(up_path):
up_hash = str(uuid.uuid4())
os.symlink(up_path, ROOT_WWW_FOLDER+'/'+up_hash)
return json.dumps({ 'LINK' : SITE_URL+'/ul/'+up_hash})
else:
abort(400, 'Upload directory not exists!')
#MOVE
elif request.json['CMD'] == 'MOVE':
src_path = home_path+'/'+request.json['SOURCE']
dst_path = home_path+'/'+request.json['DESTINATION']
if not os.path.normpath(src_path).startswith(home_path):
abort(400, 'Invalid source path.')
if not os.path.normpath(dst_path).startswith(home_path):
abort(400, 'Invalid destination path.')
if os.path.exists(src_path) == True and os.path.exists(dst_path) == True and os.path.isdir(dst_path) == True:
shutil.move(src_path,dst_path)
return
else:
#TODO
abort(400, "Can not move the file.")
#RENAME
elif request.json['CMD'] == 'RENAME':
src_path = home_path+'/'+request.json['PATH']
if not os.path.normpath(src_path).startswith(home_path):
abort(400, 'Invalid source path.')
dst_path = os.path.dirname(src_path)+'/'+request.json['NEW_NAME']
if os.path.exists(src_path) == True:
os.rename(src_path, dst_path)
else:
abort(404, "File or Folder not found!")
return
#NEW FOLDER
elif request.json['CMD'] == 'NEW_FOLDER':
dir_path = home_path+'/'+request.json['PATH']
if not os.path.normpath(dir_path).startswith(home_path):
abort(400, 'Invalid directory path.')
if os.path.exists(dir_path) == True:
abort(400, "Directory already exist!")
else:
os.mkdir(dir_path, 0755)
return
#REMOVE
elif request.json['CMD'] == 'REMOVE':
remove_path = home_path+'/'+request.json['PATH']
if not os.path.normpath(remove_path).startswith(home_path):
abort(400, 'Invalid path.')
if os.path.exists(remove_path) != True:
abort(404, "Path not found!")
else:
if os.path.isdir(remove_path) == True:
shutil.rmtree(remove_path)
return
else:
os.remove(remove_path)
return
else:
abort(400, "Command not found!")
@route('/set/<neptun>', method='POST')
def set_keys(neptun):
key_list = []
smb_password = ''
try:
smbpasswd = request.json['SMBPASSWD']
for key in request.json['KEYS']:
key_list.append(key)
except:
abort(400, 'Wrong syntax!')
result = subprocess.call([ROOT_BIN_FOLDER+'/'+USER_MANAGER,'set',neptun,smbpasswd])
if result == 0:
updateSSHAuthorizedKeys(neptun,key_list)
return
elif result == 2:
abort(403, 'User does not exist!')
@route('/new/<neptun>', method='POST')
def new_user(neptun):
key_list = []
smbpasswd=''
try:
smbpasswd = request.json['SMBPASSWD']
except:
abort(400, 'Invalid syntax')
#Call user creator script
result = subprocess.call([ROOT_BIN_FOLDER+'/'+USER_MANAGER,'add',neptun,smbpasswd])
if result == 0:
try:
for key in request.json['KEYS']:
key_list.append(key)
updateSSHAuthorizedKeys(neptun,key_list)
except:
abort(400,'SSH')
return
elif result == 2:
abort(403, 'User already exist!')
else:
abort(400, 'An error occured!')
#Static file
@route('/dl/<hash_num>', method='GET')
def dl_hash(hash_num):
hash_path = ROOT_WWW_FOLDER
if os.path.exists(hash_path) != True:
abort(404, "File not found!")
else:
filename = os.path.basename(os.path.realpath(hash_path+'/'+hash_num))
return static_file(hash_num,root=hash_path,download=filename)
@route('/ul/<hash_num>', method='POST')
def upload(hash_num):
if not os.path.exists(ROOT_WWW_FOLDER+'/'+hash_num):
abort (404,'Token not found!')
try:
file_data = request.files.data
file_name = file_data.filename
except:
if os.path.exists(ROOT_WWW_FOLDER+'/'+hash_num):
os.remove(ROOT_WWW_FOLDER+'/'+hash_num)
abort(400, 'No file was specified!')
up_path = os.path.realpath(ROOT_WWW_FOLDER+'/'+hash_num+'/'+file_name)
if os.path.exists(up_path):
abort(400, 'File already exists')
#Check if upload path valid
if not os.path.normpath(up_path).startswith('/home'):
abort(400, 'Invalid path.')
os.remove(ROOT_WWW_FOLDER+'/'+hash_num)
#Get the real upload path
#Delete the hash link
#Get the username from path for proper ownership
username=up_path.split('/',3)[2]
#os.setegid(getpwnam(username).pw_gid)
#os.seteuid(getpwnam(username).pw_uid)
#TODO setuid subcommand
#Check if file exist (root can overwrite anything not safe)
f = open(up_path , 'wb')
datalength = 0
for chunk in fbuffer(file_data.file):
f.write(chunk)
datalength += len(chunk)
f.close()
os.chown(up_path,getpwnam(username).pw_uid,getpwnam(username).pw_gid)
os.chmod(up_path,0744)
return 'Upload finished: '+file_name+' - '+str(datalength)+' Byte'
#Define filebuffer for big uploads
def fbuffer(f, chunk_size=4096):
while True:
chunk = f.read(chunk_size)
if not chunk: break
yield chunk
#Update users .ssh/authorized_keys
def updateSSHAuthorizedKeys(username,key_list):
user_home_ssh = '/home/'+username+'/home/.ssh'
user_uid=getpwnam(username).pw_uid
user_gid=getpwnam(username).pw_gid
if not os.path.exists(user_home_ssh):
os.mkdir(user_home_ssh, 0700)
os.chown(user_home_ssh,user_uid,user_gid)
auth_file_name = user_home_ssh+'/authorized_keys'
auth_file = open(auth_file_name,'w')
for key in key_list:
auth_file.write(key+'\n')
auth_file.close()
os.chmod(auth_file_name,0600)
os.chown(auth_file_name,user_uid,user_gid)
return
#For debug purpose
#@route('/ul/<hash_num>', method='GET')
#def upload_get(hash_num):
# return """<form method="POST" action="/ul/{hash}" enctype="multipart/form-data">
# <input name="data" type="file" />
# <input type="submit" />
#</form>""".format(hash=hash_num)
def list_directory(home,path):
#Check for path breakout
if not os.path.normpath(path).startswith(home):
abort(400, 'Invalid path.')
#Check if path exist
if os.path.exists(path) != True:
abort(404,'No such file or directory')
else:
#If it's a file return with list
if os.path.isdir(path) != True:
return json.dumps((os.path.basename(path), 'F', os.path.getsize(path), os.path.getmtime(path)))
#List directory and return list
else:
tuplelist = []
filelist = os.listdir(path)
#Add type support
for item in filelist:
static_route = path+"/"+item
if os.path.isdir(static_route):
is_dir = 'D'
else:
is_dir = 'F'
tuplelist.append((item, is_dir, os.path.getsize(static_route)/1024 , os.path.getmtime(static_route) ))
return json.dumps(tuplelist)
def getQuotaStatus(neptun):
output=subprocess.check_output([ROOT_BIN_FOLDER+'/'+USER_MANAGER,'status',neptun], stderr=subprocess.STDOUT)
return output.split()
if __name__ == "__main__":
run(host='0.0.0.0', port=8080)
else:
application=app()
#!/bin/bash
#
# Return values:
# 0: succesfully created
# 1: invalid syntax
# 2: user already exist
#
GRP_NAME="cloudusers"
COMMAND="$1"
USER_NAME="$2"
SMB_PASSWD="$3"
umask 022
case $COMMAND in
'add')
if [ "x${USER_NAME}" == "x" ]; then
exit 1
fi
if [ "x${SMB_PASSWD}" == "x" ]; then
exit 1
fi
#Check if user already exist
id ${USER_NAME} > /dev/null 2>&1
if [ $? == '0' ]; then
exit 2
fi
HOME_DIR="/home/${USER_NAME}/home"
mkdir -p ${HOME_DIR}
useradd --no-user-group --home ${HOME_DIR} --gid ${GRP_NAME} ${USER_NAME} >/dev/null 2>&1
adduser ${USER_NAME} ${GRP_NAME} >/dev/null 2>&1
chown ${USER_NAME}:cloudusers ${HOME_DIR} >/dev/null 2>&1
chmod 0755 ${HOME_DIR} >/dev/null 2>&1
chmod 0755 "/home/${USER_NAME}" 2>&1
#Set password to SMB_PASSWD
echo -e "${SMB_PASSWD}\n${SMB_PASSWD}\n" | passwd ${USER_NAME} >/dev/null 2>&1
#Set SMBPASSWD
echo -e "${SMB_PASSWD}\n${SMB_PASSWD}" | (smbpasswd -a -s ${USER_NAME}) > /dev/null
echo "User ${USER_NAME} CREATED at `date`" >> /root/users.log
#Set quotas
# Username Soft Hard Inode Dev
setquota ${USER_NAME} 2097152 2621440 0 0 /home
;;
'set')
id ${USER_NAME} > /dev/null 2>&1
if [ $? == '0' ]; then
echo -e "${SMB_PASSWD}\n${SMB_PASSWD}\n" | passwd ${USER_NAME} >/dev/null 2>&1
echo -e "${SMB_PASSWD}\n${SMB_PASSWD}" | (smbpasswd -a -s ${USER_NAME}) > /dev/null
else
exit 2
fi
;;
'del')
id ${USER_NAME} > /dev/null 2>&1
if [ $? != '0' ]; then
exit 2
fi
smbpasswd -x ${USER_NAME} >/dev/null 2>&1
deluser --remove-home ${USER_NAME} >/dev/null 2>&1
rmdir /home/${USER_NAME} >/dev/null 2>&1
echo "User ${USER_NAME} DELETED at `date`" >> /root/users.log
;;
'stat')
stat=( $(quota -w ${USER_NAME} 2>/dev/null | tail -1 | awk '{ print $2" "$3" "$4 }') )
USED_DISK=${stat[0]}
SOFT_LIMIT=${stat[1]}
HARD_LIMIT=${stat[3]}
case $3 in
'used')
echo $USED_DISK
;;
'soft')
echo $SOFT_LIMIT
;;
'hard')
echo $HARD_LIMIT
;;
esac
;;
'status')
echo $(quota -w ${USER_NAME} 2>/dev/null | tail -1 | awk '{ print $2" "$3" "$4 }')
;;
*)
echo "Usage: UserManager.sh COMMAND USER PASSWORD"
exit 1
;;
esac
......@@ -7,16 +7,18 @@ 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 one.util import keygen
from school.models import Person
from firewall.models import Host, Rule, Vlan
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
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)
......@@ -24,13 +26,22 @@ def create_user_profile(sender, instance, created, **kwargs):
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)
smb_password = models.CharField(max_length=20)
ssh_key = models.ForeignKey('SshKey', null=True)
ssh_private_key = models.TextField()
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
......@@ -41,9 +52,15 @@ class UserCloudDetails(models.Model):
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:
......@@ -51,6 +68,9 @@ class UserCloudDetails(models.Model):
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']
......@@ -74,13 +94,16 @@ class OpenSshKeyValidator(object):
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]
......@@ -89,10 +112,15 @@ class SshKey(models.Model):
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
......@@ -121,12 +149,17 @@ class Disk(models.Model):
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()
public = models.BooleanField()
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
......@@ -154,56 +187,84 @@ class Network(models.Model):
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()
RAM = models.IntegerField()
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=_('név'))
access_type = models.CharField(max_length=10, choices=[('rdp', 'rdp'), ('nx', 'nx'), ('ssh', 'ssh')])
disk = models.ForeignKey(Disk)
instance_type = models.ForeignKey(InstanceType)
network = models.ForeignKey(Network)
owner = models.ForeignKey(User)
created_at = models.DateTimeField(auto_now_add=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 = _('sablon')
verbose_name_plural = _('sablonok')
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)
template = models.ForeignKey(Template)
owner = models.ForeignKey(User)
created_at = models.DateTimeField(auto_now_add=True)
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)
firewall_host = models.ForeignKey(Host, blank=True, null=True)
pw = models.CharField(max_length=20)
one_id = models.IntegerField(unique=True, blank=True, null=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
......@@ -217,6 +278,10 @@ class Instance(models.Model):
def __unicode__(self):
return self.name
"""
Get and update VM state from OpenNebula.
"""
def update_state(self):
import subprocess
......@@ -241,11 +306,14 @@ class Instance(models.Model):
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)
age = (datetime.now().replace(tzinfo=None)
- self.active_since.replace(tzinfo=None)).seconds
except:
pass
......@@ -253,8 +321,11 @@ class Instance(models.Model):
@models.permalink
def get_absolute_url(self):
return ('vm_show', None, {'iid':self.id,})
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
......@@ -296,10 +367,9 @@ class Instance(models.Model):
"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,
}
"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
......@@ -332,6 +402,9 @@ class Instance(models.Model):
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)
......@@ -343,8 +416,3 @@ class Instance(models.Model):
class Meta:
verbose_name = _('instance')
verbose_name_plural = _('instances')
# vim: et sw=4 ai fenc=utf8 smarttab :
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