Commit 9ab17d5f by Sulyok Gábor

Commit previous changes for Setty

parent 39a7ffc9
...@@ -17,13 +17,21 @@ ...@@ -17,13 +17,21 @@
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
Element, Service,
ElementCategory,
ElementTemplate, ElementTemplate,
ElementConnection, ElementConnection,
Service, Machine,
NginxNode,
MySQLNode,
PostgreSQLNode
) )
admin.site.register(Element) admin.site.register(ElementCategory)
admin.site.register(ElementTemplate) admin.site.register(ElementTemplate)
admin.site.register(ElementConnection) admin.site.register(ElementConnection)
admin.site.register(Service) admin.site.register(Service)
admin.site.register(Machine)
admin.site.register(NginxNode)
admin.site.register(MySQLNode)
admin.site.register(PostgreSQLNode)
from .models import *
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.db.models.loading import get_model
from saltstackhelper import *
import os
class SettyController:
salthelper = SaltStackHelper()
@staticmethod
def saveService( serviceId, serviceName, serviceNodes, machines, elementConnections ):
service = None
try:
service = Service.objects.get(id=serviceId)
except Service.DoesNotExist:
return JsonResponse( {'error': 'Service not found'})
service.name = serviceName
service.save()
#first check machine names
#validMachineNames = self.salthelper.getAllMinionsUngrouped()
Machine.objects.filter(service=service).delete()
for machineData in machines:
# if machineData["hostname"] in validMachineNames:
machineSaved = Machine(service=service)
machineSaved.fromDataDictionary( machineData )
machineSaved.save()
ServiceNode.objects.filter(service=service).delete()
for node in serviceNodes:
elementTemplateId = node["displayId"].split("_")[0]
elementTemplate = ElementTemplate.objects.get(id=elementTemplateId)
newNode = get_model('setty', elementTemplate.prototype ).clone()
newNode.service = service
newNode.fromDataDictionary( node )
newNode.save()
for elementConnection in elementConnections:
sourceId = elementConnection['sourceId']
targetId = elementConnection['targetId']
sourceEndpoint = elementConnection['sourceEndpoint']
targetEndpoint = elementConnection['targetEndpoint']
connectionParameters = elementConnection['parameters']
targetObject = Element.objects.get(
display_id=targetId)
sourceObject = Element.objects.get(
display_id=sourceId)
connectionObject = ElementConnection(
target=targetObject,
source=sourceObject,
target_endpoint=targetEndpoint,
source_endpoint=sourceEndpoint,
parameters=connectionParameters
)
connectionObject.save()
return {"serviceName": serviceName}
@staticmethod
def loadService(serviceId):
service = None
try:
service = Service.objects.get(id=serviceId)
except Service.DoesNotExist:
return JsonResponse({'error': 'Service not found'})
machineList = Machine.objects.filter(service=service)
serviceNodes = []
elementConnections = []
machines = []
for machine in machineList:
machines.append(machine.getDataDictionary())
serviveNodeList = ServiceNode.objects.filter(service=service)
elementConnectionList = ElementConnection.objects.filter(
Q(target__in=serviveNodeList) | Q(source__in=serviveNodeList))
for servideNode in serviveNodeList:
serviceNodes.append( servideNode.cast().getDataDictionary() )
for elementConnection in elementConnectionList:
elementConnections.append( elementConnection.getDataDictionary() )
return {'serviceName': service.name,
'elementConnections': elementConnections,
'serviceNodes': serviceNodes,
'machines': machines}
@staticmethod
def getInformation(elementTemplateId, hostname):
if elementTemplateId:
try:
elementTemplate = ElementTemplate.objects.get(
id=elementTemplateId)
model = get_model('setty', elementTemplate.prototype)
return model.getInformation()
except ElementTemplate.DoesNotExist:
return
except LookupError:
return
elif hostname:
return Machine.getInformation()
elif hostname and elementTemplateId:
raise PermissionDenied # TODO: something more meaningful
else:
raise PermissionDenied # TODO: something more meaningful
@staticmethod
def getMachineAvailableList(service_id, used_hostnames):
all_minions = SettyController.salthelper.getAllMinionsGrouped()
result = []
#TODO: filter out used ones
for item in all_minions["up"]:
result.append( {'hostname': item,
'hardware-info': SettyController.salthelper.getMinionBasicHardwareInfo( item ),
'status': 'up'} )
for item in all_minions["down"]:
result.append( {'hostname': item, 'status': 'down' })
return { 'machinedata': result }
@staticmethod
def addMachine(hostname):
try:
Machine.objects.get(hostname=hostname)
return {'error': 'already added or doesnt exists'}
except:
pass
if SettyController.salthelper.checkMinionExists(hostname):
machine = Machine.clone()
machine.hostname = hostname
return machine.getDataDictionary()
else:
return {'error': 'already added or doesnt exists'}
@staticmethod
def addServiceNode(elementTemplateId):
if elementTemplateId:
try:
elementTemplate = ElementTemplate.objects.get(id=elementTemplateId)
model = get_model('setty', elementTemplate.prototype )
return model.clone().getDataDictionary()
except ElementTemplate.DoesNotExist:
return {'error': 'lofaszka' }
except:
return {'error': 'valami nagyon el lett baszva'}
else:
return {'error': 'templateid'}
@staticmethod
def deploy(serviceId):
service = Service.objects.get(id=serviceId)
machines = Machine.objects.filter(service=service)
elementConnections = ElementConnection.objects.filter(
Q(target__in=machines) | Q(source__in=machines) )
firstLevelServiceNodes = []
#phase one: set the machine ptr in serviceNodes which can be accessed by
# connections from machines
for machine in machines:
for connection in elementConnections:
serviceNode = None
if connection.target.cast() == machine:
serviceNode = connection.source.cast()
serviceNode.setMachineForDeploy( machine )
elif connection.source.cast() == machine:
serviceNode = connection.target.cast()
serviceNode.setMachineForDeploy( machine )
else:
raise PermissionDenied
firstLevelServiceNodes.append( serviceNode )
#phase two: let the nodes create configurations recursively
configuratedNodes = list()
for serviceNode in firstLevelServiceNodes:
generatedNodes = serviceNode.generateConfigurationRecursively()
if isinstance( generatedNodes, list ):
configuratedNodes = configuratedNodes + generatedNodes
else:
configuratedNodes.append( generatedNodes )
#phase three: sort the nodes by deployment priority(lower the prio, later in the deployement)
configuratedNodes.sort(reverse=True)
#deploy the nodes
for node in configuratedNodes:
SettyController.salthelper.deploy( node.machine.hostname, node.generatedConfig )
return {'status': 'deployed'}
#cleanup the temporary data
''' for node in configuratedNodes:
node.deployCleanUp()'''
\ No newline at end of file
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import setty.storage
class Migration(migrations.Migration):
dependencies = [
('setty', '0012_auto_20160308_1432'),
]
operations = [
migrations.CreateModel(
name='ElementCategory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=50)),
('parent_category', models.ForeignKey(to='setty.ElementCategory', null=True)),
],
),
migrations.CreateModel(
name='Machine',
fields=[
('element_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='setty.Element')),
('hostname', models.TextField()),
('alias', models.CharField(max_length=50)),
('config_file', models.FileField(default=None, storage=setty.storage.OverwriteStorage(), upload_to=b'setty/machine_configs/')),
('description', models.TextField(default=b'')),
('status', models.CharField(max_length=1, choices=[(1, b'Running'), (2, b'Unreachable')])),
],
options={
'abstract': False,
},
bases=('setty.element',),
),
migrations.CreateModel(
name='ServiceNode',
fields=[
('element_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='setty.Element')),
('name', models.CharField(max_length=50)),
('config_file', models.FileField(default=None, storage=setty.storage.OverwriteStorage(), upload_to=b'setty/node_configs/')),
('description', models.TextField(default=b'')),
('machine', models.ForeignKey(to='setty.Machine')),
],
bases=('setty.element',),
),
migrations.RemoveField(
model_name='element',
name='parameters',
),
migrations.RemoveField(
model_name='element',
name='service',
),
migrations.RemoveField(
model_name='elementtemplate',
name='parameters',
),
migrations.AlterField(
model_name='service',
name='status',
field=models.CharField(default=1, max_length=1, choices=[(1, b'Draft'), (2, b'Deployed')]),
),
migrations.AddField(
model_name='machine',
name='service',
field=models.ForeignKey(related_name='service_id', to='setty.Service'),
),
migrations.AddField(
model_name='elementtemplate',
name='category',
field=models.ForeignKey(to='setty.ElementCategory', null=True),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('setty', '0013_saltstack_changes'),
]
operations = [
migrations.CreateModel(
name='NginxNode',
fields=[
('servicenode_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='setty.ServiceNode')),
('worker_connections', models.PositiveIntegerField()),
],
bases=('setty.servicenode',),
),
migrations.CreateModel(
name='WebServerNode',
fields=[
('servicenode_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='setty.ServiceNode')),
('useSSL', models.BooleanField(default=False)),
('listeningPort', models.PositiveIntegerField()),
],
bases=('setty.servicenode',),
),
migrations.RemoveField(
model_name='elementtemplate',
name='tags',
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('setty', '0014_auto_20160320_1724'),
]
operations = [
migrations.AlterField(
model_name='elementcategory',
name='parent_category',
field=models.ForeignKey(blank=True, to='setty.ElementCategory', null=True),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import setty.storage
class Migration(migrations.Migration):
dependencies = [
('setty', '0015_allow_blank_elementcategory_parent'),
]
operations = [
migrations.AddField(
model_name='elementtemplate',
name='prototype',
field=models.TextField(default=b'<SYNTAX ERROR>'),
),
migrations.AlterField(
model_name='elementtemplate',
name='compatibles',
field=models.ManyToManyField(related_name='compatibles_rel_+', to='setty.ElementTemplate', blank=True),
),
migrations.AlterField(
model_name='elementtemplate',
name='logo',
field=models.FileField(storage=setty.storage.OverwriteStorage(), upload_to=b'setty/', blank=True),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import setty.storage
class Migration(migrations.Migration):
dependencies = [
('setty', '0016_auto_20160320_1753'),
]
operations = [
migrations.RemoveField(
model_name='nginxnode',
name='servicenode_ptr',
),
migrations.AddField(
model_name='nginxnode',
name='webservernode_ptr',
field=models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, default=None, serialize=False, to='setty.WebServerNode'),
preserve_default=False,
),
migrations.AlterField(
model_name='elementtemplate',
name='logo',
field=models.FileField(storage=setty.storage.OverwriteStorage(), upload_to=b'setty/'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('setty', '0017_auto_20160320_1828'),
]
operations = [
migrations.RemoveField(
model_name='servicenode',
name='machine',
),
migrations.AddField(
model_name='servicenode',
name='service',
field=models.ForeignKey(default=None, to='setty.Service'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('setty', '0018_auto_20160420_1728'),
]
operations = [
migrations.AlterField(
model_name='servicenode',
name='service',
field=models.ForeignKey(related_name='node_service_id', default=None, to='setty.Service'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('setty', '0019_auto_20160420_2043'),
]
operations = [
migrations.AlterField(
model_name='servicenode',
name='service',
field=models.ForeignKey(default=None, to='setty.Service'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('setty', '0020_auto_20160420_2132'),
]
operations = [
migrations.AddField(
model_name='element',
name='real_type',
field=models.ForeignKey(default=None, editable=False, to='contenttypes.ContentType'),
),
]
import salt.loader
import salt.config
import salt.runner
import salt.client
SALTSTACK_STATE_FOLDER = "/srv/salt"
class SaltStackHelper:
def __init__(self):
self.master_opts = salt.config.client_config('/etc/salt/master')
self.salt_runner = salt.runner.RunnerClient(self.master_opts)
self.salt_localclient = salt.client.LocalClient()
self.salt_caller = salt.client.Caller()
def getAllMinionsGrouped(self):
query_result = self.salt_runner.cmd('manage.status', []);
return query_result
def getAllMinionsUngrouped(self):
query_result = self.salt_runner.cmd('manage.status', []);
return query_result["up"] + query_result["down"]
def getRunningMinions(self):
return self.salt_runner.cmd('manage.up', []);
def getUnavailableMinions(self):
return self.salt_runner.cmd('manage.down', []);
def getMinionBasicHardwareInfo(self, hostname):
query_res = self.salt_localclient.cmd( hostname,'grains.items' );
if query_res:
return {
'CpuModel': query_res[hostname]['cpu_model'],
'CpuArch': query_res[hostname]['cpuarch'],
'TotalMemory': query_res[hostname]['mem_total'],
'OSDescription': query_res[hostname]['lsb_distrib_description'] }
return query_res
def checkMinionExists(self, hostname):
query_res = self.salt_localclient.cmd( hostname,'network.get_hostname' );
return query_res != None
def deploy(self, hostname, configFilePath ):
print configFilePath
self.salt_localclient.cmd(hostname, 'state.apply', [configFilePath.split('.')[0]] )
\ No newline at end of file

15.9 KB | W: | H:

15.9 KB | W: | H:

circle/setty/static/setty/apache.jpg
circle/setty/static/setty/apache.jpg
circle/setty/static/setty/apache.jpg
circle/setty/static/setty/apache.jpg
  • 2-up
  • Swipe
  • Onion skin

18.4 KB | W: | H:

18.4 KB | W: | H:

circle/setty/static/setty/lighttpd.jpg
circle/setty/static/setty/lighttpd.jpg
circle/setty/static/setty/lighttpd.jpg
circle/setty/static/setty/lighttpd.jpg
  • 2-up
  • Swipe
  • Onion skin

6.02 KB | W: | H:

6.02 KB | W: | H:

circle/setty/static/setty/nginx.jpg
circle/setty/static/setty/nginx.jpg
circle/setty/static/setty/nginx.jpg
circle/setty/static/setty/nginx.jpg
  • 2-up
  • Swipe
  • Onion skin

9.06 KB | W: | H:

9.06 KB | W: | H:

circle/setty/static/setty/ubuntu.jpg
circle/setty/static/setty/ubuntu.jpg
circle/setty/static/setty/ubuntu.jpg
circle/setty/static/setty/ubuntu.jpg
  • 2-up
  • Swipe
  • Onion skin

19.9 KB | W: | H:

19.9 KB | W: | H:

circle/setty/static/setty/wordpress.jpg
circle/setty/static/setty/wordpress.jpg
circle/setty/static/setty/wordpress.jpg
circle/setty/static/setty/wordpress.jpg
  • 2-up
  • Swipe
  • Onion skin
# Copyright 2014 Budapest University of Technology and Economics (BME IK) # Copyright 2014 Budapest University of Technology and Economics (BME IK)
# #
# This file is part of CIRCLE Cloud. # This file is part of CIRCLE Cloud.
# #
# CIRCLE is free software: you can redistribute it and/or modify it under # CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free # the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) # Software Foundation, either version 3 of the License, or (at your option)
# any later version. # any later version.
# #
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY # CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details. # details.
# #
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django_tables2 import Table from django_tables2 import Table
from django_tables2.columns import TemplateColumn from django_tables2.columns import TemplateColumn
from setty.models import Service from setty.models import Service
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
class ServiceListTable(Table): class ServiceListTable(Table):
name = TemplateColumn( name = TemplateColumn(
template_name="setty/tables/column-name.html", template_name="setty/tables/column-name.html",
attrs={'th': {'data-sort': "string"}} attrs={'th': {'data-sort': "string"}}
) )
owner = TemplateColumn( owner = TemplateColumn(
template_name="setty/tables/column-owner.html", template_name="setty/tables/column-owner.html",
verbose_name=_("Owner"), verbose_name=_("Owner"),
attrs={'th': {'data-sort': "string"}} attrs={'th': {'data-sort': "string"}}
) )
running = TemplateColumn( running = TemplateColumn(
template_name="setty/tables/column-running.html", template_name="setty/tables/column-running.html",
verbose_name=_("Running"), verbose_name=_("Running"),
attrs={'th': {'data-sort': "string"}}, attrs={'th': {'data-sort': "string"}},
) )
class Meta: class Meta:
model = Service model = Service
attrs = {'class': ('table table-bordered table-striped table-hover' attrs = {'class': ('table table-bordered table-striped table-hover'
' template-list-table')} ' template-list-table')}
fields = ('name', 'owner', 'running', ) fields = ('name', 'owner', 'running', )
prefix = "service-" prefix = "service-"
...@@ -24,7 +24,8 @@ from django.shortcuts import redirect ...@@ -24,7 +24,8 @@ from django.shortcuts import redirect
from braces.views import LoginRequiredMixin from braces.views import LoginRequiredMixin
from django.views.generic import TemplateView, DeleteView from django.views.generic import TemplateView, DeleteView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from .models import Element, ElementTemplate, ElementConnection, Service from saltstackhelper import *
from controller import *
from dashboard.views.util import FilterMixin from dashboard.views.util import FilterMixin
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import json import json
...@@ -39,6 +40,7 @@ logger = logging.getLogger(__name__) ...@@ -39,6 +40,7 @@ logger = logging.getLogger(__name__)
class DetailView(LoginRequiredMixin, TemplateView): class DetailView(LoginRequiredMixin, TemplateView):
template_name = "setty/index.html" template_name = "setty/index.html"
salthelper = SaltStackHelper()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
logger.debug('DetailView.get_context_data() called. User: %s', logger.debug('DetailView.get_context_data() called. User: %s',
...@@ -54,88 +56,37 @@ class DetailView(LoginRequiredMixin, TemplateView): ...@@ -54,88 +56,37 @@ class DetailView(LoginRequiredMixin, TemplateView):
raise PermissionDenied raise PermissionDenied
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
logger.debug('DetailView.post() called. User: %s',
unicode(self.request.user))
service = Service.objects.get(id=kwargs['pk']) service = Service.objects.get(id=kwargs['pk'])
if self.request.user != service.user or not self.request.user.is_superuser:
if self.request.user == service.user or self.request.user.is_superuser:
if self.request.POST.get('event') == "saveService":
data = json.loads(self.request.POST.get('data'))
service = Service.objects.get(id=kwargs['pk'])
service.name = data['serviceName']
service.save()
Element.objects.filter(service=service).delete()
for element in data['elements']:
elementObject = Element(
service=service,
parameters=element['parameters'],
display_id=element['displayId'],
position_left=element['positionLeft'],
position_top=element['positionTop'],
anchor_number=element['anchorNumber']
)
elementObject.save()
for elementConnection in data['elementConnections']:
sourceId = elementConnection['sourceId']
targetId = elementConnection['targetId']
sourceEndpoint = elementConnection['sourceEndpoint']
targetEndpoint = elementConnection['targetEndpoint']
connectionParameters = elementConnection['parameters']
targetObject = Element.objects.get(
display_id=targetId,
service=service)
sourceObject = Element.objects.get(
display_id=sourceId,
service=service)
connectionObject = ElementConnection(
target=targetObject,
source=sourceObject,
target_endpoint=targetEndpoint,
source_endpoint=sourceEndpoint,
parameters=connectionParameters
)
connectionObject.save()
return JsonResponse({'serviceName': service.name})
elif self.request.POST.get('event') == "loadService":
service = Service.objects.get(id=kwargs['pk'])
elementList = Element.objects.filter(service=service)
elementConnectionList = ElementConnection.objects.filter(
Q(target__in=elementList) | Q(source__in=elementList))
elements = []
elementConnections = []
for item in elementList:
elements.append({
'parameters': item.parameters,
'displayId': item.display_id,
'positionLeft': item.position_left,
'positionTop': item.position_top,
'anchorNumber': item.anchor_number})
for item in elementConnectionList:
elementConnections.append({
'targetEndpoint': item.target_endpoint,
'sourceEndpoint': item.source_endpoint,
'parameters': item.parameters})
return JsonResponse(
{'elements': elements,
'elementConnections': elementConnections,
'serviceName': service.name})
else:
raise PermissionDenied
else:
raise PermissionDenied raise PermissionDenied
result = {}
eventName = self.request.POST.get('event')
serviceId = kwargs['pk']
if eventName == 'loadService':
result = SettyController.loadService(serviceId)
elif eventName == "deploy":
result = SettyController.deploy(serviceId)
data = json.loads(self.request.POST.get('data'))
if eventName == "saveService":
result = SettyController.saveService(serviceId, data['serviceName'], data[
'serviceNodes'], data['machines'], data['elementConnections'])
elif eventName == "getMachineAvailableList":
result = SettyController.getMachineAvailableList(
serviceId, data["usedHostnames"])
elif eventName == "addServiceNode":
result = SettyController.addServiceNode(
data["elementTemplateId"])
elif eventName == "addMachine":
result = SettyController.addMachine(data["hostname"])
elif eventName == "getInformation":
result = SettyController.getInformation(
data['elementTemplateId'], data['hostname'])
return JsonResponse(result)
class DeleteView(LoginRequiredMixin, DeleteView): class DeleteView(LoginRequiredMixin, DeleteView):
...@@ -180,11 +131,16 @@ class CreateView(LoginRequiredMixin, TemplateView): ...@@ -180,11 +131,16 @@ class CreateView(LoginRequiredMixin, TemplateView):
if not service_name: if not service_name:
service_name = "Noname" service_name = "Noname"
service = Service( try:
name=service_name, serviceNameAvailable = Service.objects.get(name=service_name)
status="stopped", raise PermissionDenied
user=self.request.user except Service.DoesNotExist:
) pass
service = Service(name=service_name,
status=1,
user=self.request.user)
service.save() service.save()
return redirect('setty.views.service-detail', pk=service.pk) return redirect('setty.views.service-detail', pk=service.pk)
......
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