Commit 3dc93ded by Sulyok Gabor

Changes and fixes for deployment

parent 97c4dbde

15.9 KB | W: | H:

15.9 KB | W: | H:

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

9.07 KB | W: | H:

9.07 KB | W: | H:

circle/media/setty/mysql.jpg
circle/media/setty/mysql.jpg
circle/media/setty/mysql.jpg
circle/media/setty/mysql.jpg
  • 2-up
  • Swipe
  • Onion skin

19.9 KB | W: | H:

19.9 KB | W: | H:

circle/media/setty/wordpress.jpg
circle/media/setty/wordpress.jpg
circle/media/setty/wordpress.jpg
circle/media/setty/wordpress.jpg
  • 2-up
  • Swipe
  • Onion skin
from .models import *
from models import ElementTemplate, Machine, Service, ServiceNode, Element, ElementConnection
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.db.models.loading import get_model
from django.db import transaction
from saltstackhelper import *
import os
from saltstackhelper import SaltStackHelper
from vm.models import Instance
import logging
logger = logging.getLogger(__name__)
salthelper = SaltStackHelper()
class SettyController:
salthelper = SaltStackHelper()
'''
Save the service data given by the designer. It includes all
Machines, ServiceNodes and connections between them. If the
saving fails, the last saved status will not be lost.
'''
@staticmethod
@transaction.atomic
def saveService(serviceId, serviceName, serviceNodes, machines, elementConnections):
......@@ -18,7 +24,7 @@ class SettyController:
try:
service = Service.objects.get(id=serviceId)
except Service.DoesNotExist:
return JsonResponse({'error': 'Service not found'})
return {'status': 'error', 'errors': 'SERVICE_NOT_FOUND'}
service.name = serviceName
service.save()
......@@ -61,16 +67,21 @@ class SettyController:
connectionObject.save()
return {"serviceName": serviceName}
return {'status': 'success', 'serviceName': serviceName}
'''
Load all ServiceNode derivatives, ElementConnections and saved Machines
for the Service with the given id and give it back to the caller.
If the service doesn't exists, error is returned.
'''
@staticmethod
def loadService(serviceId):
service = None
try:
service = Service.objects.get(id=serviceId)
except Service.DoesNotExist:
return JsonResponse({'error': 'Service not found'})
return {'status': 'error', 'errors': 'SERVICE_NOT_FOUND'}
machineList = Machine.objects.filter(service=service)
serviceNodes = []
......@@ -95,6 +106,12 @@ class SettyController:
'serviceNodes': serviceNodes,
'machines': machines}
'''
Queiry information -- field names, type of that field -- about
an ElemenTemplate or Machine. If the ElementTemplate doesn't exist
or the prototype couldn't be retrieved
'''
@staticmethod
def getInformation(elementTemplateId, hostname):
if elementTemplateId:
......@@ -104,19 +121,24 @@ class SettyController:
model = get_model('setty', elementTemplate.prototype)
return model.getInformation()
except ElementTemplate.DoesNotExist:
return
return {'status': 'error', 'errors': 'ELEMENTTEMPLATE_DOESNT_EXISTS'}
except LookupError:
return
return {'status': 'error', 'errors': 'ELEMENTTEMPLATE_COULDNT_GET_PROTOTYPE'}
elif hostname:
return Machine.getInformation()
elif hostname and elementTemplateId:
raise PermissionDenied # TODO: something more meaningful
return {'status': 'error', 'errors': 'BOTH_ELEMENTEMPLATE_HOSTNAME_FILLED'}
else:
raise PermissionDenied # TODO: something more meaningful
return {'status': 'error', 'errors': 'UNKNOWN_ERROR'}
'''
Return the available hostnames to add to the Service
based on already used / saved hostanames and Instances
owned by current user which is also known by SaltStack
'''
@staticmethod
def getMachineAvailableList(serviceId, usedHostnames, current_user):
saltMinions = SettyController.salthelper.getAllMinionsUngrouped()
savedMachines = Machine.objects.filter(service=serviceId)
savedHostNames = []
......@@ -131,27 +153,41 @@ class SettyController:
userMachines.append(instance.vm_name)
usedHostnamesByUser = set(savedHostNames + usedHostnames)
availableInstances = set(set(userMachines) - usedHostnamesByUser)
saltMinions = SettyController.salthelper.getAllMinionsUngrouped()
if not usedHostnamesByUser:
return {'machinedata': [machineName for machineName in userMachines if machineName in saltMinions] }
return {'machinedata': [machineName for machineName in userMachines if machineName in saltMinions]}
availableInstances = list(set(userMachines) - usedHostnamesByUser)
return {'machinedata': [ machineName for machineName in availableInstances if machineName in saltMinions ]}
return {'machinedata': [machineName for machineName in availableInstances if machineName in saltMinions]}
'''
Add a machine with the given hostname to the Service. If there is a
saved Machine with the given hostname or the SaltStack doesn't know about
the machine give an error back. If it exists and not already saved give
back the new Machine instance
'''
#TODO: addMachine requires usedHostnames too for safety
@staticmethod
def addMachine(hostname):
try:
Machine.objects.get(hostname=hostname)
return {'error': 'already added or doesnt exists'}
except:
pass
return {'status': 'error', 'errors': 'MACHINE_ALREADY_ADDED'}
if SettyController.salthelper.checkMinionExists(hostname):
machine = Machine.clone()
machine.hostname = hostname
return machine.getDataDictionary()
else:
return {'error': 'already added or doesnt exists'}
return {'status': 'error', 'errors': 'MACHINE_DOESNT_EXISTS'}
'''
Add a new service node based on the given ElementTemplate ID to
the current service and return the data member of it.
If no ElementTemplate exists with the given ID or it couldn't be
institiated give an error.
'''
@staticmethod
def addServiceNode(elementTemplateId):
......@@ -162,12 +198,13 @@ class SettyController:
model = get_model('setty', elementTemplate.prototype)
return model.clone().getDataDictionary()
except ElementTemplate.DoesNotExist:
return {'error': "ElementTemplate doesn't exists"}
return {'status': 'error', 'errors': 'ELEMENTTEMPLATE_DOESNT_EXISTS'}
except:
return {'error': 'Can not get prototype'}
return {'status': 'error', 'errors': 'ELEMENTTEMPLATE_COULDNT_GET_PROTOTYPE'}
else:
return {'error': 'templateid'}
return {'status': 'error', 'errors': 'INVALID_ELEMENTTEMPLATE_ID'}
''' Deploy a service using SaltStack. The steps are described inline.'''
@staticmethod
def deploy(serviceId):
service = Service.objects.get(id=serviceId)
......@@ -177,7 +214,7 @@ class SettyController:
nodesToBeDeployed = []
for serviceNode in serviveNodeList:
castedServiceNode = serviceNode.cast()
nodesToBeDeployed.append( castedServiceNode )
nodesToBeDeployed.append(castedServiceNode)
errorMessage = castedServiceNode.checkDependenciesAndAttributes()
if errorMessage:
errorMessages.append(errorMessage)
......@@ -186,8 +223,9 @@ class SettyController:
return {'status': 'error',
'errors': errorMessages}
# phase one: ask the servicenodes to generate their needed salt commands
# phase one: ask the servicenodes to generate their needed salt
# commands
for serviceNode in nodesToBeDeployed:
serviceNode.generateSaltCommands()
......@@ -196,17 +234,19 @@ class SettyController:
nodesToBeDeployed.sort(reverse=True)
# dbgCheck = []
dbgCheck = []
# for node in nodesToBeDeployed:
# commandArray = []
#
# for command in node.generatedCommands:
# logger.error( "salt '"+ command.hostname +"' state.sls " + command.command + " pillar=\"" + str(command.parameters) + '"' )
# commandArray.append( command.toDict() )
#
# dbgCheck.append({ "nodeName": str(node.__class__.__name__),
# "hostingMachineName": str(node.hostingMachine.hostname),
# "hostingMachineName": str(node.getHostingMachine().hostname ),
# "commands": commandArray })
#
# return {"status": "error", "errors":dbgCheck}
#return {"status": "error", "errors":dbgCheck}
# phase three: deploy the nodes
for node in nodesToBeDeployed:
......@@ -215,10 +255,9 @@ class SettyController:
if errorMessages:
errorMessages.append(deployErrorMessages)
# phase four: cleanup generated commands
# phase four: cleanup generated commands
for serviceNode in nodesToBeDeployed:
serviceNode.generatedCommands = None
serviceNode.hostingMachine = None
if errorMessages:
return {'status': 'error',
......
......@@ -16,21 +16,19 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.db import models
from django.db.models import Model, Q
from django.db.models.signals import post_init
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from taggit.managers import TaggableManager
from django.utils.translation import ugettext_lazy as _
from storage import OverwriteStorage
import os
import yaml
from saltstackhelper import SaltCommand
from saltstackhelper import SaltCommand, SaltStackHelper
SALTSTACK_PILLAR_FOLDER = "/srv/pillar"
# Replacer method for configuration generation
salthelper = SaltStackHelper()
def replaceParameter(pillar, parameterToReplace, newValue):
pillarEdited = pillar.replace(parameterToReplace, str(newValue))
......@@ -49,35 +47,44 @@ class Service(models.Model):
def __unicode__(self):
return self.name
''' Defines caterogies for ElementTemplates. '''
class ElementCategory(models.Model):
name = models.CharField(max_length=50)
name = models.CharField(max_length=50) # Name of the category
# If the current category is a subcategory, store the ancestor
parent_category = models.ForeignKey(
'self', on_delete=models.CASCADE, null=True, blank=True)
def __unicode__(self):
return self.name
# Defines a model for ElementTemplates which are used to create real elements on the GUI
# Hold
'''
Defines a model for ElementTemplates which are used to create real elements ServiceNodes
'''
class ElementTemplate(models.Model):
name = models.CharField(max_length=50)
logo = models.FileField(upload_to='setty/', storage=OverwriteStorage())
description = models.TextField()
logo = models.FileField(
upload_to='setty/', storage=OverwriteStorage()) # Logo for the
description = models.TextField() # Description of a ServiceNode
# List of wich ServiceNodes are compatible
compatibles = models.ManyToManyField('self', blank=True)
category = models.ForeignKey(
ElementCategory, on_delete=models.CASCADE, null=True)
ElementCategory, on_delete=models.CASCADE, null=True) # Which category
# The name of the Model which the template represents
prototype = models.TextField(default="<SYNTAX ERROR>")
def __unicode__(self):
return self.name
# http://stackoverflow.com/questions/929029/how-do-i-access-the-child-classes-of-an-object-in-django-without-knowing-the-name/929982#929982
# Super base class to prevent messing the code in the controller
# it saves the type info to DB, and when the objects are queried, the cast method returns the real class
# not the base
'''
http://stackoverflow.com/questions/929029/how-do-i-access-the-child-classes-of-an-object-in-django-without-knowing-the-name/929982#929982
Base class to prevent messing with the code in the controller
it saves the type info to DB, and when the objects are queried, the cast method returns the real class
not the base
'''
class InheritanceCastModel(models.Model):
......@@ -97,6 +104,11 @@ class InheritanceCastModel(models.Model):
class Meta:
abstract = True
'''
Element stores the base information for all types of nodes shown in the designer.
It stores the id, position and number of anchors
'''
class Element(InheritanceCastModel):
display_id = models.TextField()
......@@ -104,20 +116,24 @@ class Element(InheritanceCastModel):
position_top = models.FloatField()
anchor_number = models.PositiveSmallIntegerField()
# Get the display related data in a dictionary
def getDisplayData(self):
return {'displayId': self.display_id,
'positionLeft': self.position_left,
'positionTop': self.position_top,
'anchorNumber': self.anchor_number}
# Set the display related data from a dictionary
def setDisplayData(self, data):
self.display_id = data["displayId"]
self.position_left = data["positionLeft"]
self.position_top = data["positionTop"]
self.anchor_number = data["anchorNumber"]
# ElementConnection represents connection between Elements. Has a source
# and a target endpoint.
'''
ElementConnection represents connection between Elements. Has a source
and a target endpoint.
'''
class ElementConnection(models.Model):
......@@ -139,30 +155,38 @@ class ElementConnection(models.Model):
return {'targetEndpoint': self.target_endpoint,
'sourceEndpoint': self.source_endpoint}
# Represents an CIRCLE VM Instance which is known by Salt-Master and used
# in Setty configuration
'''
Represents an CIRCLE VM Instance which is known by Salt-Master and used
in Setty configuration
'''
class Machine(Element):
MACHINE_STATUS_CHOICES = (
(1, 'Running'),
(2, 'Unreachable'))
# Which service contains the given Machine instnace
service = models.ForeignKey(
Service, on_delete=models.CASCADE, related_name="service_id")
hostname = models.TextField(null=False) # also serves as salt-minion id
# User given alias for the machine
alias = models.CharField(max_length=50)
# User's description for the machine
description = models.TextField(default="")
status = models.CharField(choices=MACHINE_STATUS_CHOICES, max_length=1)
def __unicode__(self):
return "%s" % self.hostname
# Get the name and the type of the editable fields
@staticmethod
def getInformation():
return {'hostname': Machine._meta.get_field('hostname').get_internal_type(),
'alias': Machine._meta.get_field('alias').get_internal_type(),
'description': Machine._meta.get_field('description').get_internal_type()}
# Get the all the Machine related data in a dictionary
def getDataDictionary(self):
element_data = self.getDisplayData()
......@@ -172,6 +196,7 @@ class Machine(Element):
element_data.update(self_data)
return element_data
# Fill the fields from the given dictioary
def fromDataDictionary(self, data):
self.setDisplayData(data)
......@@ -179,27 +204,34 @@ class Machine(Element):
self.alias = data["alias"]
self.description = data["description"]
# Create a new instance of Machine
@staticmethod
def clone():
return Machine()
class ServiceNode(Element):
# The Service which the ServiceNode belongs to
service = models.ForeignKey(
Service, on_delete=models.CASCADE, default=None)
name = models.CharField(max_length=50)
# TODO: Remove this
config_file = models.FileField(
default=None, upload_to='setty/node_configs/', storage=OverwriteStorage())
# User's description for the ServiceNode
description = models.TextField(default="")
# Override the default init function to create generatedCommands
# instance member to store the SaltCommands for deployment
def __init__(self, *args, **kwargs):
super(ServiceNode, self).__init__(*args, **kwargs)
self.hostingMachine = None
self.generatedCommands = []
def __unicode__(self):
return "%s" % self.name
# Get the saved data from the node
def getDataDictionary(self):
element_data = self.getDisplayData()
......@@ -209,11 +241,13 @@ class ServiceNode(Element):
element_data.update(self_data)
return element_data
# Fill the members of the class from dictionary
def fromDataDictionary(self, data):
self.setDisplayData(data)
self.name = data['name']
self.description = data['description']
# Get fieldnames and types in a dictionary
@staticmethod
def getInformation():
return {'name': ServiceNode._meta.get_field('name').get_internal_type(),
......@@ -223,10 +257,17 @@ class ServiceNode(Element):
def clone():
raise PermissionDenied
# functions for deployement
# Check that all the required fields are set, and
# has the connections for the other services or machines
# which are needed
def checkDependenciesAndAttributes(self):
return []
# functions for deployement
# Checks whether the current Node has a connection to
# an other node (Machine or ServiceNode derivative).
# The other object's type is the ObjOther parameter
# Returns the queryed node if it exists, or None
def checkDependecy(self, ObjOther):
elementConnections = ElementConnection.objects.filter(
Q(target=self) | Q(source=self))
......@@ -240,46 +281,50 @@ class ServiceNode(Element):
return connection.target.cast()
return None
# Compare functions for sorting the ServiceNodes by DeploymentPriority
def __cmp__(self, other):
if not isinstance( other, ServiceNode ):
if not isinstance(other, ServiceNode):
raise PermissionDenied
return self.getDeploymentPriority(self).__cmp__(other.getDeploymentPriority(other))
# Returns the hosting Machine for the ServiceNode. It must be overriden
# for nodes where they depend on an other service, and the hosting machine
# is the other service's hosting machine
def getHostingMachine(self):
if self.hostingMachine:
return self.hostingMachine
elementConnections = ElementConnection.objects.filter(
Q(target=self) | Q(source=self))
for connection in elementConnections:
if isinstance(connection.target.cast(), Machine):
self.hostingMachine = connection.target.cast()
return self.hostingMachine
return connection.target.cast()
if isinstance(connection.source.cast(), Machine):
self.hostingMachine = connection.source.cast()
return self.hostingMachine
return connection.source.cast()
raise PermissionDenied
# Return the deployment priority. The nodes are sorted by this attribute
# and deployed in that order
def getDeploymentPriority(self):
return 0
# Generate the salt commands which are needed to deploy the service
def generateSaltCommands(self):
raise PermissionDenied
# Replace the parameters in the given pillar with the stored values
def replacePillarParameters(self, pillar):
raise PermissionDenied
class WordpressNode(ServiceNode):
# DB related fields
databaseName = models.TextField(default="")
databaseHost = models.TextField(default="")
databaseHost = models.TextField(default="") # todo: remove this
databaseUser = models.TextField(default="")
databasePass = models.TextField(default="")
# admin user
# Admin user for the
adminUsername = models.TextField(default="")
adminPassword = models.TextField(default="")
adminEmail = models.TextField(default="")
......@@ -322,7 +367,9 @@ class WordpressNode(ServiceNode):
@staticmethod
def getInformation():
superInformation = ServiceNode.getInformation()
ownInformation = {'database-name': WordpressNode._meta.get_field('databaseName').get_internal_type(),
ownInformation = {'database-name':
WordpressNode._meta.get_field(
'databaseName').get_internal_type(),
'database-host': WordpressNode._meta.get_field('databaseHost').get_internal_type(),
'database-user': WordpressNode._meta.get_field('databaseUser').get_internal_type(),
'database-pass': WordpressNode._meta.get_field('databasePass').get_internal_type(),
......@@ -339,45 +386,41 @@ class WordpressNode(ServiceNode):
errorMessages = ServiceNode.checkDependenciesAndAttributes(self)
if not self.databaseHost:
errorMessage.append("DATABASENAME_NOT_SET")
errorMessages.append("DATABASENAME_NOT_SET")
if not self.databaseHost:
errorMessage.append("DATABASEHOST_NOT_SET")
errorMessages.append("DATABASEHOST_NOT_SET")
if not self.databaseUser:
errorMessage.append("DATABASEUSER_NOT_SET")
errorMessages.append("DATABASEUSER_NOT_SET")
if not self.databasePass:
errorMessage.append("DATABASEPASS_NOT_SET")
errorMessages.append("DATABASEPASS_NOT_SET")
if not self.adminUsername:
errorMessage.append("ADMINUSERNAME_NOT_SET")
errorMessages.append("ADMINUSERNAME_NOT_SET")
if not self.adminPassword:
errorMessage.append("ADMINPASSWORD_NOT_SET")
errorMessages.append("ADMINPASSWORD_NOT_SET")
if not self.adminEmail:
errorMessage.append("ADMINEMAIL_NOT_SET")
errorMessages.append("ADMINEMAIL_NOT_SET")
if not self.siteTitle:
errorMessage.append("SITETITLE_NOT_SET")
errorMessages.append("SITETITLE_NOT_SET")
if not self.siteUrl:
errorMessage.append("SITEURL_NOT_SET")
errorMessages.append("SITEURL_NOT_SET")
if not self.checkDependecy(MySQLNode):
errorMessage.append("NODE_NOT_CONNECTED")
errorMessages.append("NODE_NOT_CONNECTED")
if not self.checkDependecy(WebServerNode):
errorMessage.append("NODE_NOT_CONNECTED")
errorMessages.append("NODE_NOT_CONNECTED")
return errorMessages
def getHostingMachine(self):
if self.hostingMachine:
return hostingMachine
apacheNode = self.checkDependecy(ApacheNode)
if not apacheNode:
raise PermissionDenied
self.hostingMachine = apacheNode.getHostingMachine()
if not self.hostingMachine:
hostingMachine = apacheNode.getHostingMachine()
if not hostingMachine:
raise PermissionDenied
return self.hostingMachine
return hostingMachine
@staticmethod
def getDeploymentPriority(self):
......@@ -385,60 +428,77 @@ class WordpressNode(ServiceNode):
def replacePillarParameters(self, pillar):
pillar = replaceParameter(
pillar, r'%%DATABASE_NAME%%', self.databaseName)
pillar, r'DATABASE_NAME', self.databaseName)
pillar = replaceParameter(
pillar, r'%%DATABASE_HOST%%', self.databaseHost)
pillar, r'DATABASE_HOST', self.databaseHost)
pillar = replaceParameter(
pillar, r'%%DATABASE_USER%%', self.databaseUser)
pillar, r'DATABASE_USER', self.databaseUser)
pillar = replaceParameter(
pillar, r'%%DATABASE_PASS%%', self.databasePass)
pillar, r'DATABASE_PASS', self.databasePass)
pillar = replaceParameter(
pillar, r'%%ADMIN_USERNAME%%', self.adminUsername)
pillar, r'ADMIN_USERNAME', self.adminUsername)
pillar = replaceParameter(
pillar, r'%%ADMIN_PASSWORD%%', self.adminPassword)
pillar, r'ADMIN_PASSWORD', self.adminPassword)
pillar = replaceParameter(pillar, r'%%ADMIN_EMAIL%%', self.adminEmail)
pillar = replaceParameter(pillar, r'%%SITE_TITLE%%', self.siteTitle)
pillar = replaceParameter(pillar, r'%%SITE_URL%%', self.siteUrl)
pillar = replaceParameter(pillar, r'ADMIN_EMAIL', self.adminEmail)
pillar = replaceParameter(pillar, r'SITE_TITLE', self.siteTitle)
pillar = replaceParameter(pillar, r'SITE_URL', self.siteUrl)
return pillar
def generateSaltCommands(self):
mysqlNode = self.checkDependecy(MySQLNode)
apacheNode = self.checkDependecy(ApacheNode)
if not mysqlNode:
raise PermissionDenied
if not apacheNode:
raise PermissionDenied
self.hostingMachine = apacheNode.getHostingMachine()
createMySQLUserCommand = mysqlNode.makeCreateDatabaseCommand(
installPhpCommand = apacheNode.makeInstallPhpCommand()
restartApacheCommand = apacheNode.makeRestartCommand()
createMySQLDatabaseCommand = mysqlNode.makeCreateDatabaseCommand(
self.databaseName)
createMySQLDatabaseCommand = mysqlNode.makeCreateUserCommand(
createMySQLUserCommand = mysqlNode.makeCreateUserCommand(
self.databaseUser, self.databasePass, self.databaseName)
pillarFilePath = os.path.join(
SALTSTACK_PILLAR_FOLDER, "wordpress.sls")
with open(pillarFilePath, 'r') as pillarFile:
pillar = str(yaml.load(pillarFile))
pillar = self.replacePillarParameters(pillar)
saltCommand = SaltCommand()
saltCommand.hostname = self.hostingMachine.hostname
saltCommand.command = "wordpress"
saltCommand.parameters = [eval(pillar)]
self.generatedCommands = []
self.generatedCommands.append(createMySQLDatabaseCommand)
self.generatedCommands.append(createMySQLUserCommand)
self.generatedCommands.append(saltCommand)
installWordpressCommand = SaltCommand()
installWordpressCommand.hostname = self.getHostingMachine().hostname
installWordpressCommand.command = "wordpress"
installWordpressCommand.parameters = {'wordpress':
{'cli':
{'source':
'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar',
'hash':
'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.sha512'},
'lookup': {'docroot': '/var/www/html'},
'sites':
{'mysitename.com':
{'username': self.adminUsername,
'password': self.adminPassword,
'database': self.databaseName,
'dbhost': salthelper.getIpAddressOfMinion( mysqlNode.getHostingMachine().hostname ),
'dbuser': self.databaseUser,
'dbpass': self.databasePass,
'url': self.siteUrl,
'title': self.siteTitle,
'email': self.adminEmail}}}}
installMySqlClientCommand = SaltCommand()
installMySqlClientCommand.hostname = self.getHostingMachine().hostname
installMySqlClientCommand.command = "mysql.client"
self.generatedCommands.append(installPhpCommand)
self.generatedCommands.append(restartApacheCommand)
self.generatedCommands.append(installMySqlClientCommand)
self.generatedCommands.append(createMySQLDatabaseCommand)
self.generatedCommands.append(createMySQLUserCommand)
self.generatedCommands.append(installWordpressCommand)
class WebServerNode(ServiceNode):
useSSL = models.BooleanField(default=False)
listeningPort = models.PositiveIntegerField()
useSSL = models.BooleanField(default=False) # todo: remove this
listeningPort = models.PositiveIntegerField() # todo: remove this
def getDataDictionary(self):
element_data = ServiceNode.getDataDictionary(self)
......@@ -478,9 +538,9 @@ class WebServerNode(ServiceNode):
return 10
def replacePillarParameters(self, pillar):
pillar = replaceParameter(pillar, r"%%USE_SSL%%", self.useSSL)
pillar = replaceParameter(pillar, r"USE_SSL", self.useSSL)
pillar = replaceParameter(pillar,
r"%%LISTENING_PORT%%", self.listeningPort)
r"LISTENING_PORT", self.listeningPort)
return pillar
......@@ -491,19 +551,26 @@ class ApacheNode(WebServerNode):
return ApacheNode()
def generateSaltCommands(self):
pillarFilePath = os.path.join(
SALTSTACK_PILLAR_FOLDER, "apache.sls")
with open(pillarFilePath, 'r') as pillarFile:
pillar = str(yaml.load(pillarFile))
pillar = WebServerNode.replacePillarParameters(self, pillar)
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "apache"
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "apache"
saltCommand.parameters = [eval(pillar)]
self.generatedCommands = []
self.generatedCommands.append(saltCommand)
self.generatedCommands = []
self.generatedCommands.append(saltCommand)
def makeInstallPhpCommand(self):
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "php-simple"
return saltCommand
def makeRestartCommand(self):
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "apache.restart"
return saltCommand
class NginxNode(WebServerNode):
......@@ -545,7 +612,7 @@ class NginxNode(WebServerNode):
pillar = str(yaml.load(pillarFile))
pillar = WebServerNode.replacePillarParameters(self, pillar)
pillar = replaceParameter(pillar,
r"%%WORKER_CONNECTIONS%%", self.worker_connections)
r"WORKER_CONNECTIONS", self.worker_connections)
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
......@@ -557,9 +624,9 @@ class NginxNode(WebServerNode):
class DatabaseNode(ServiceNode):
adminUserName = models.CharField(max_length=50)
adminUserName = models.CharField(max_length=50) # todo: remove this
adminPassword = models.CharField(max_length=50)
listeningPort = models.PositiveIntegerField()
listeningPort = models.PositiveIntegerField() # todo: remove this
def getDataDictionary(self):
element_data = ServiceNode.getDataDictionary(self)
......@@ -605,11 +672,11 @@ class DatabaseNode(ServiceNode):
def replacePillarParameters(self, pillar):
pillar = replaceParameter(pillar,
r"%%ADMIN_USERNAME%%", self.adminUserName)
r"ADMIN_USERNAME", self.adminUserName)
pillar = replaceParameter(pillar,
r"%%ADMIN_PASSWORD%%", self.adminUserName)
r"ADMIN_PASSWORD", self.adminUserName)
pillar = replaceParameter(pillar,
r'%%LISTENING_PORT%%', self.listeningPort)
r'LISTENING_PORT', self.listeningPort)
return pillar
......@@ -641,32 +708,47 @@ class MySQLNode(DatabaseNode):
def clone():
return MySQLNode()
# Generate SaltCommand for database creation on the current MySQL instance
def makeCreateDatabaseCommand(self, databaseName):
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "mysql.database"
saltCommand.parameters = [databaseName]
saltCommand.parameters = {'mysql': {
'server': {'root_password': self.adminPassword}, 'database': [databaseName]}}
return saltCommand
# Generate SaltCommand for user creation on the current MySQL instance
def makeCreateUserCommand(self, databaseUser, databasePass, availableDatabases):
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "mysql.user"
saltCommand.parameters = {databaseUser: {'password': databasePass, 'host': 'localhost', 'databases': [
{'database': availableDatabases, 'grants': ['all privileges']}]}}
databaseGrants = [{'database': '*', 'grants': ['all privileges']}]
if isinstance(availableDatabases, list):
for dbAccess in availableDatabases:
databaseGrants.append(
{'database': dbAccess, 'grants': ['all privileges']})
else:
databaseGrants.append(
{'database': availableDatabases, 'grants': ['all privileges']})
saltCommand.parameters = {'mysql': {
'server': {
'root_password': self.adminPassword
},
'user': {
databaseUser:
{'password': databasePass,
'host': '%',
'databases': databaseGrants
}}}}
return saltCommand
def generateSaltCommands(self):
pillarFilePath = os.path.join(
SALTSTACK_PILLAR_FOLDER, "mysql.sls")
with open(pillarFilePath, 'r') as pillarFile:
pillar = str(yaml.load(pillarFile))
pillar = DatabaseNode.replacePillarParameters(self, pillar)
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "mysql.server"
saltCommand.parameters = [eval(pillar)]
saltCommand = SaltCommand()
saltCommand.hostname = self.getHostingMachine().hostname
saltCommand.command = "mysql.server"
saltCommand.parameters = {
'mysql': {'server': {'root_password': self.adminPassword, 'mysqld':{'bind-address' : '0.0.0.0'}}}}
self.generatedCommands = []
self.generatedCommands.append(saltCommand)
self.generatedCommands = []
self.generatedCommands.append(saltCommand)
......@@ -3,55 +3,59 @@ import salt.config
import salt.runner
import salt.client
import logging
logger = logging.getLogger(__name__)
class SaltCommand:
def __init__(self):
self.hostname = ""
self.command = ""
self.parameters = ""
self.parameters = None
# For debugging purposes only
def __str__(self):
return "Command: " + self.hostname + " - " + self.command + " - " + str(self.parameters)
def toDict(self):
return { 'hostname': self.hostname, 'command': self.command, 'parameters': self.parameters }
return {'hostname': self.hostname,
'command': self.command,
'parameters': self.parameters}
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()
def getAllMinionsGrouped(self):
query_result = self.salt_runner.cmd('manage.status', []);
query_result = self.salt_runner.cmd('manage.status', [])
return query_result
def getAllMinionsUngrouped(self):
query_result = self.salt_runner.cmd('manage.status', []);
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', []);
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
return self.salt_runner.cmd('manage.down', [])
def checkMinionExists(self, hostname):
query_res = self.salt_localclient.cmd( hostname,'network.get_hostname' );
query_res = self.salt_localclient.cmd(hostname, 'network.get_hostname')
return query_res != {}
def getIpAddressOfMinion(self, hostname):
query_res = self.salt_localclient.cmd(hostname, 'network.ip_addrs')
return query_res[hostname][0]
def executeCommand(self, saltCommands):
for saltCommand in saltCommands:
self.salt_localclient.cmd(saltCommand.hostname, "state.sls",[saltCommand.command],kwarg={"pillar":saltCommand.parameters} )
for command in saltCommands:
if command.parameters:
self.salt_localclient.cmd(command.hostname, "state.sls",
[command.command],
kwarg={"pillar": command.parameters})
else:
self.salt_localclient.cmd(command.hostname, "state.sls",
[command.command])
......@@ -29,6 +29,15 @@ $.ajaxSetup({
}
});
showErrorDialog = function( errors ){
$("#errorListContainer").empty();
$.each( errors, function(index, message){
icon = $("<i/>").addClass("fa").addClass("fa-remove");
item = $("<li/>").addClass("list-group-item").addClass("list-group-item-danger").append(icon).append( message );
$("#errorListContainer").append( item );
});
$("#errorDialog").modal('show');
}
/* Setty implementation starts here. */
......@@ -101,11 +110,12 @@ jsPlumb.ready(function() {
event: "addMachine",
data: JSON.stringify({ "hostname": machineHostname } )
}, function(result) {
if(result.error)
if(result.errors)
{
alert(result.error)
showErrorDialog( result.errors );
return;
}
addMachine( result );
undoStack.splice(stackIndexer, 0, removeElement);
redoStack.splice(stackIndexer, 0, addElement);
......@@ -793,14 +803,14 @@ jsPlumb.ready(function() {
$('body').on('click', '#deployService',function() {
if($("#serviceStatus").text() == "Unsaved") {
alert("Only saved services can be deployed");
showErrorDialog( ["Only saved services can be deployed"]);
return;
}
$.post("", {
event: "deploy",
}, function(result) {
if ( result.status )
alert( result.errors );
if ( result.errors )
showErrorDialog( result.errors );
else
alert("Deploying....");
});
......
......@@ -97,6 +97,29 @@
</div>
</div>
<div class="modal fade" id="errorDialog" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button class="close" type="button" data-dismiss="modal">&times;</button>
<h4 class="modal-title"><i class="fa fa-trash-o"></i> {% trans 'Error occured during operation' %}</h4>
</div>
<div class="modal-body">
<p>{% trans 'The following errors occured' %}:</p>
<ul id="errorListContainer" class="list-group">
</ul>
</div>
<div class="modal-footer">
<div class="row">
<div class="col-xs-2">
<button class="btn btn-primary btn-md" type="button" data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Adding element dialog -->
<div class="modal fade" id="addElementDialog" role="dialog">
......
......@@ -18,14 +18,13 @@
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Q
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from braces.views import LoginRequiredMixin
from django.views.generic import TemplateView, DeleteView
from django_tables2 import SingleTableView
from saltstackhelper import *
from controller import *
from controller import SettyController
from models import Service, ElementTemplate
from dashboard.views.util import FilterMixin
from django.utils.translation import ugettext as _
import json
......@@ -86,7 +85,7 @@ class DetailView(LoginRequiredMixin, TemplateView):
'serviceNodes'], data['machines'], data['elementConnections'])
elif eventName == "getMachineAvailableList":
result = SettyController.getMachineAvailableList(
serviceId, data["usedHostnames"], self.request.user )
serviceId, data["usedHostnames"], self.request.user)
elif eventName == "addServiceNode":
result = SettyController.addServiceNode(
data["elementTemplateId"])
......@@ -103,7 +102,7 @@ class DetailView(LoginRequiredMixin, TemplateView):
result = SettyController.getInformation(
templateId, hostname)
return JsonResponse(result)
......@@ -151,7 +150,7 @@ class CreateView(LoginRequiredMixin, TemplateView):
service_name = "Noname"
try:
serviceNameAvailable = Service.objects.get(name=service_name)
Service.objects.get(name=service_name)
raise PermissionDenied
except Service.DoesNotExist:
pass
......
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