Commit 4ff07fee authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

cyclades: refactor server creation command

Split the server creation command in more steps in order to be
decorated with the 'server_command' function and follow the logic of all
other server commands. The main change is that the commission job is not
accepted when the VM is stored in DB, but when the OP_INSTANCE_CREATE
job finishes in Ganeti, no matter whether it succeeded or not. Finally,
this commit includes a transaction testcase that checks instance
creation.
parent a272fe52
......@@ -79,9 +79,11 @@ def handle_vm_quotas(vm, job_id, job_opcode, job_status, job_fields):
if vm.task_job_id == job_id and vm.serial is not None:
# Commission for this change has already been issued. So just
# accept/reject it
# accept/reject it. Special case is OP_INSTANCE_CREATE, which even
# if fails, must be accepted, as the user must manually remove the
# failed server
serial = vm.serial
if job_status == "success":
if job_status == "success" or job_opcode == "OP_INSTANCE_CREATE":
quotas.accept_serial(serial)
elif job_status in ["error", "canceled"]:
log.debug("Job %s failed. Rejecting related serial %s", job_id,
......
......@@ -42,7 +42,7 @@ def validate_server_action(vm, action):
# Check if action can be performed to VM's operstate
operstate = vm.operstate
if operstate == "BUILD":
if operstate == "BUILD" and action != "BUILD":
raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
elif (action == "START" and operstate != "STOPPED") or\
(action == "STOP" and operstate != "STARTED") or\
......@@ -101,6 +101,17 @@ def server_command(action):
auto_accept=False)
vm.serial = serial
# XXX: Special case for server creation!
if action == "BUILD":
# Perform a commit, because the VirtualMachine must be saved to
# DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti.
# Otherwise, messages will arrive from snf-dispatcher about
# this instance, before the VM is stored in DB.
transaction.commit()
# After committing the locks are released. Refetch the instance
# to guarantee x-lock.
vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
# Send the job to Ganeti and get the associated jobID
try:
job_id = func(vm, *args, **kwargs)
......@@ -128,24 +139,13 @@ def server_command(action):
return decorator
@transaction.commit_manually
@transaction.commit_on_success
def create(userid, name, password, flavor, image, metadata={},
personality=[], private_networks=None, floating_ips=None,
use_backend=None):
if use_backend is None:
# Allocate backend to host the server. Commit after allocation to
# release the locks hold by the backend allocator.
try:
backend_allocator = BackendAllocator()
use_backend = backend_allocator.allocate(userid, flavor)
if use_backend is None:
log.error("No available backend for VM with flavor %s", flavor)
raise faults.ServiceUnavailable("No available backends")
except:
transaction.rollback()
raise
else:
transaction.commit()
# Allocate server to a Ganeti backend
use_backend = allocate_new_server(userid, flavor)
if private_networks is None:
private_networks = []
......@@ -164,60 +164,27 @@ def create(userid, name, password, flavor, image, metadata={},
else:
flavor.disk_provider = None
try:
# We must save the VM instance now, so that it gets a valid
# vm.backend_vm_id.
vm = VirtualMachine.objects.create(
name=name,
backend=use_backend,
userid=userid,
imageid=image["id"],
flavor=flavor,
action="CREATE")
log.info("Created entry in DB for VM '%s'", vm)
nics = create_instance_nics(vm, userid, private_networks, floating_ips)
# Also we must create the VM metadata in the same transaction.
for key, val in metadata.items():
VirtualMachineMetadata.objects.create(
meta_key=key,
meta_value=val,
vm=vm)
# Issue commission to Quotaholder and accept it since at the end of
# this transaction the VirtualMachine object will be created in the DB.
# Note: the following call does a commit!
quotas.issue_and_accept_commission(vm)
except:
transaction.rollback()
raise
else:
transaction.commit()
# We must save the VM instance now, so that it gets a valid
# vm.backend_vm_id.
vm = VirtualMachine.objects.create(name=name,
backend=use_backend,
userid=userid,
imageid=image["id"],
flavor=flavor,
operstate="BUILD")
log.info("Created entry in DB for VM '%s'", vm)
nics = create_instance_nics(vm, userid, private_networks, floating_ips)
for key, val in metadata.items():
VirtualMachineMetadata.objects.create(
meta_key=key,
meta_value=val,
vm=vm)
jobID = None
try:
vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
# dispatch server created signal needed to trigger the 'vmapi', which
# enriches the vm object with the 'config_url' attribute which must be
# passed to the Ganeti job.
server_created.send(sender=vm, created_vm_params={
'img_id': image['backend_id'],
'img_passwd': password,
'img_format': str(image['format']),
'img_personality': json.dumps(personality),
'img_properties': json.dumps(image['metadata']),
})
jobID = backend.create_instance(vm, nics, flavor, image)
# At this point the job is enqueued in the Ganeti backend
vm.backendjobid = jobID
vm.task = "BUILD"
vm.task_job_id = jobID
vm.save()
transaction.commit()
log.info("User %s created VM %s, NICs %s, Backend %s, JobID %s",
userid, vm, nics, backend, str(jobID))
# Create the server in Ganeti.
create_server(vm, nics, flavor, image, personality, password)
except:
# If an exception is raised, then the user will never get the VM id.
# In order to delete it from DB and release it's resources, we
......@@ -233,6 +200,49 @@ def create(userid, name, password, flavor, image, metadata={},
return vm
@transaction.commit_on_success
def allocate_new_server(userid, flavor):
"""Allocate a new server to a Ganeti backend.
Allocation is performed based on the owner of the server and the specified
flavor. Also, backends that do not have a public IPv4 address are excluded
from server allocation.
This function runs inside a transaction, because after allocating the
instance a commit must be performed in order to release all locks.
"""
backend_allocator = BackendAllocator()
use_backend = backend_allocator.allocate(userid, flavor)
if use_backend is None:
log.error("No available backend for VM with flavor %s", flavor)
raise faults.ServiceUnavailable("No available backends")
return use_backend
@server_command("BUILD")
def create_server(vm, nics, flavor, image, personality, password):
# dispatch server created signal needed to trigger the 'vmapi', which
# enriches the vm object with the 'config_url' attribute which must be
# passed to the Ganeti job.
server_created.send(sender=vm, created_vm_params={
'img_id': image['backend_id'],
'img_passwd': password,
'img_format': str(image['format']),
'img_personality': json.dumps(personality),
'img_properties': json.dumps(image['metadata']),
})
# send job to Ganeti
jobID = backend.create_instance(vm, nics, flavor, image)
# At this point the job is enqueued in the Ganeti backend
vm.backendjobid = jobID
vm.save()
log.info("User %s created VM %s, NICs %s, Backend %s, JobID %s",
vm.userid, vm, nics, backend, str(jobID))
return jobID
def create_instance_nics(vm, userid, private_networks=[], floating_ips=[]):
"""Create NICs for VirtualMachine.
......
......@@ -29,22 +29,22 @@
# policies, either expressed or implied, of GRNET S.A.
# Provides automated tests for logic module
from django.test import TestCase
from django.test import TransactionTestCase
#from snf_django.utils.testing import mocked_quotaholder
from synnefo.logic import servers
from synnefo.db import models_factory as mfactory
from synnefo.db import models_factory as mfactory, models
from mock import patch
from snf_django.lib.api import faults
from snf_django.utils.testing import mocked_quotaholder, override_settings
from django.conf import settings
from copy import deepcopy
@patch("synnefo.logic.rapi_pool.GanetiRapiClient")
class ServerTest(TestCase):
class ServerCreationTest(TransactionTestCase):
def test_create(self, mrapi):
flavor = mfactory.FlavorFactory()
backend = mfactory.BackendFactory()
kwargs = {
"userid": "test",
"name": "test_vm",
......@@ -54,15 +54,43 @@ class ServerTest(TestCase):
"metadata": "{}"},
"metadata": {"foo": "bar"},
"personality": [],
"use_backend": backend,
}
# no backend!
mfactory.BackendFactory(offline=True)
self.assertRaises(faults.ServiceUnavailable, servers.create, **kwargs)
self.assertEqual(models.VirtualMachine.objects.count(), 0)
mfactory.BackendFactory(drained=False)
mfactory.BackendNetworkFactory(network__public=True)
# error in nics
req = deepcopy(kwargs)
req["private_networks"] = [42]
self.assertRaises(faults.ItemNotFound, servers.create, **req)
self.assertEqual(models.VirtualMachine.objects.count(), 0)
# error in enqueue. check the vm is deleted and resources released
mrapi().CreateInstance.side_effect = Exception("ganeti is down")
with mocked_quotaholder():
self.assertRaises(Exception, servers.create, **kwargs)
vm = models.VirtualMachine.objects.get()
self.assertTrue(vm.deleted)
self.assertEqual(len(vm.nics.all()), 0)
vm.delete()
# success with no nics
mrapi().CreateInstance.side_effect = None
mrapi().CreateInstance.return_value = 42
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=[]):
with mocked_quotaholder():
vm = servers.create(**kwargs)
self.assertEqual(models.VirtualMachine.objects.count(), 1)
vm = models.VirtualMachine.objects.get(id=vm.id)
self.assertEqual(vm.nics.count(), 0)
self.assertEqual(vm.backendjobid, 42)
self.assertEqual(vm.task_job_id, 42)
self.assertEqual(vm.task, "BUILD")
# test connect in IPv6 only network
net = mfactory.IPv6NetworkFactory(state="ACTIVE")
......@@ -79,6 +107,9 @@ class ServerTest(TestCase):
self.assertEqual(ganeti_nic["ip"], None)
self.assertEqual(ganeti_nic["network"], net.backend_id)
@patch("synnefo.logic.rapi_pool.GanetiRapiClient")
class ServerTest(TransactionTestCase):
def test_connect_network(self, mrapi):
# Common connect
net = mfactory.NetworkFactory(subnet="192.168.2.0/24",
......@@ -132,7 +163,7 @@ class ServerTest(TestCase):
@patch("synnefo.logic.rapi_pool.GanetiRapiClient")
class ServerCommandTest(TestCase):
class ServerCommandTest(TransactionTestCase):
def test_pending_task(self, mrapi):
vm = mfactory.VirtualMachineFactory(task="REBOOT", task_job_id=1)
self.assertRaises(faults.BadRequest, servers.start, vm)
......
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 to comment