Commit 084129f6 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

cyclades: Add network policy at server create

Add 'DEFAULT_INSTANCE_NETWORKS' setting that can be used by the
administrator to define the list of networks that each server that is
created must be connected to. Each created VM will have a NIC connected
to each of these networks. This settings can include a list of network
UUIDs or the special "public" ID, which corresponds to any public
network that has a free IP address.

Also, extend POST /servers API call, with an optional 'networks'
attribute, that the user can use to define a list of private networks
that wants it's VM to be connected.
parent 07b2216a
......@@ -285,6 +285,8 @@ def create_server(request):
flavor_id = server['flavorRef']
personality = server.get('personality', [])
assert isinstance(personality, list)
private_networks = server.get("networks", [])
assert isinstance(private_networks, list)
except (KeyError, AssertionError):
raise faults.BadRequest("Malformed request")
......@@ -298,7 +300,8 @@ def create_server(request):
password = util.random_password()
vm = servers.create(user_id, name, password, flavor, image,
metadata=metadata, personality=personality)
metadata=metadata, personality=personality,
private_networks=private_networks)
server = vm_to_dict(vm, detail=True)
server['status'] = 'BUILD'
......
......@@ -32,6 +32,7 @@
# or implied, of GRNET S.A.
import json
from copy import deepcopy
from snf_django.utils.testing import (BaseAPITest, mocked_quotaholder,
override_settings)
......@@ -43,7 +44,7 @@ from synnefo.lib.services import get_service_path
from synnefo.lib import join_urls
from synnefo import settings
from mock import patch
from mock import patch, Mock, call
class ComputeAPITest(BaseAPITest):
......@@ -234,39 +235,47 @@ class ServerAPITest(ComputeAPITest):
self.assertMethodNotAllowed(response)
@patch('synnefo.api.util.get_image')
fixed_image = Mock()
fixed_image.return_value = {'location': 'pithos://foo',
'checksum': '1234',
"id": 1,
"name": "test_image",
'disk_format': 'diskdump'}
@patch('synnefo.api.util.get_image', fixed_image)
@patch('synnefo.logic.rapi_pool.GanetiRapiClient')
class ServerCreateAPITest(ComputeAPITest):
def test_create_server(self, mrapi, mimage):
"""Test if the create server call returns the expected response
if a valid request has been speficied."""
mimage.return_value = {'location': 'pithos://foo',
'checksum': '1234',
"id": 1,
"name": "test_image",
'disk_format': 'diskdump'}
mrapi().CreateInstance.return_value = 12
flavor = mfactory.FlavorFactory()
def setUp(self):
self.flavor = mfactory.FlavorFactory()
# Create public network and backend
network = mfactory.NetworkFactory(public=True)
backend = mfactory.BackendFactory()
mfactory.BackendNetworkFactory(network=network, backend=backend)
request = {
self.network = mfactory.NetworkFactory(public=True)
self.backend = mfactory.BackendFactory()
mfactory.BackendNetworkFactory(network=self.network,
backend=self.backend,
operstate="ACTIVE")
self.request = {
"server": {
"name": "new-server-test",
"userid": "test_user",
"imageRef": 1,
"flavorRef": flavor.id,
"flavorRef": self.flavor.id,
"metadata": {
"My Server Name": "Apache1"
},
"personality": []
}
}
with mocked_quotaholder():
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
def test_create_server(self, mrapi):
"""Test if the create server call returns the expected response
if a valid request has been speficied."""
mrapi().CreateInstance.return_value = 12
with override_settings(settings, DEFAULT_INSTANCE_NETWORKS=[]):
with mocked_quotaholder():
response = self.mypost('/api/v1.1/servers', 'test_user',
json.dumps(self.request), 'json')
self.assertEqual(response.status_code, 202)
mrapi().CreateInstance.assert_called_once()
......@@ -282,28 +291,91 @@ class ServerCreateAPITest(ComputeAPITest):
self.assertEqual(api_server['status'], db_vm.operstate)
# Test drained flag in Network:
network.drained = True
network.save()
self.network.drained = True
self.network.save()
with mocked_quotaholder():
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
json.dumps(self.request), 'json')
self.assertEqual(response.status_code, 503, "serviceUnavailable")
def test_create_server_no_flavor(self, mrapi, mimage):
request = {
"server": {
"name": "new-server-test",
"userid": "test_user",
"imageRef": 1,
"flavorRef": 42,
"metadata": {
"My Server Name": "Apache1"
},
"personality": []
}
}
response = self.mypost('servers', 'test_user',
json.dumps(request), 'json')
def test_create_network_settings(self, mrapi):
mrapi().CreateInstance.return_value = 12
bnet1 = mfactory.BackendNetworkFactory(operstate="ACTIVE",
backend=self.backend)
bnet2 = mfactory.BackendNetworkFactory(operstate="ACTIVE",
backend=self.backend)
bnet3 = mfactory.BackendNetworkFactory(network__userid="test_user",
operstate="ACTIVE",
backend=self.backend)
bnet4 = mfactory.BackendNetworkFactory(network__userid="test_user",
operstate="ACTIVE",
backend=self.backend)
# User requested private networks
request = deepcopy(self.request)
request["server"]["networks"] = [bnet3.network.id, bnet4.network.id]
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=["public", bnet1.network.id,
bnet2.network.id]):
with mocked_quotaholder():
response = self.mypost('/api/v1.1/servers', 'test_user',
json.dumps(request), 'json')
self.assertEqual(response.status_code, 202)
name, args, kwargs = mrapi().CreateInstance.mock_calls[0]
self.assertEqual(len(kwargs["nics"]), 5)
self.assertEqual(kwargs["nics"][0]["network"],
self.network.backend_id)
self.assertEqual(kwargs["nics"][1]["network"],
bnet1.network.backend_id)
self.assertEqual(kwargs["nics"][2]["network"],
bnet2.network.backend_id)
self.assertEqual(kwargs["nics"][3]["network"],
bnet3.network.backend_id)
self.assertEqual(kwargs["nics"][4]["network"],
bnet4.network.backend_id)
with override_settings(settings,
DEFAULT_INSTANCE_NETWORKS=[bnet2.network.id]):
with mocked_quotaholder():
response = self.mypost('/api/v1.1/servers', 'test_user',
json.dumps(request), 'json')
self.assertEqual(response.status_code, 202)
name, args, kwargs = mrapi().CreateInstance.mock_calls[1]
self.assertEqual(len(kwargs["nics"]), 3)
self.assertEqual(kwargs["nics"][0]["network"],
bnet2.network.backend_id)
self.assertEqual(kwargs["nics"][1]["network"],
bnet3.network.backend_id)
self.assertEqual(kwargs["nics"][2]["network"],
bnet4.network.backend_id)
# test invalid network in DEFAULT_INSTANCE_NETWORKS
with override_settings(settings, DEFAULT_INSTANCE_NETWORKS=[42]):
response = self.mypost('/api/v1.1/servers', 'test_user',
json.dumps(request), 'json')
self.assertFault(response, 500, "internalServerError")
# test connect to public netwok
request = deepcopy(self.request)
request["server"]["networks"] = [self.network.id]
with override_settings(settings, DEFAULT_INSTANCE_NETWORKS=["public"]):
response = self.mypost('/api/v1.1/servers', 'test_user',
json.dumps(request), 'json')
self.assertFault(response, 403, "forbidden")
# test wrong user
request = deepcopy(self.request)
request["server"]["networks"] = [bnet3.network.id]
with override_settings(settings, DEFAULT_INSTANCE_NETWORKS=["public"]):
with mocked_quotaholder():
response = self.mypost('/api/v1.1/servers', 'dummy_user',
json.dumps(request), 'json')
self.assertItemNotFound(response)
def test_create_server_no_flavor(self, mrapi):
request = deepcopy(self.request)
request["server"]["flavorRef"] = 42
with mocked_quotaholder():
response = self.mypost('/api/v1.1/servers', 'test_user',
json.dumps(request), 'json')
self.assertItemNotFound(response)
......
......@@ -507,7 +507,24 @@ def create_instance(vm, nics, flavor, image):
kw['disks'][0]['provider'] = provider
kw['disks'][0]['origin'] = flavor.disk_origin
kw['nics'] = nics
kw['nics'] = [{"network": nic.network.backend_id, "ip": nic.ipv4}
for nic in nics]
backend = vm.backend
depend_jobs = []
for nic in nics:
network = Network.objects.select_for_update().get(id=nic.network.id)
bnet, created = BackendNetwork.objects.get_or_create(backend=backend,
network=network)
if bnet.operstate != "ACTIVE":
if network.public:
# TODO: What to raise here ?
raise Exception("LALA")
else:
depend_jobs.append(create_network(network, backend,
connect=True))
kw["depends"] = [[job, ["success", "error", "canceled"]]
for job in depend_jobs]
if vm.backend.use_hotplug():
kw['hotplug'] = True
# Defined in settings.GANETI_CREATEINSTANCE_KWARGS
......
......@@ -13,7 +13,7 @@ from synnefo.api import util
from synnefo.logic import backend
from synnefo.logic.backend_allocator import BackendAllocator
from synnefo.logic.rapi import GanetiApiError
from synnefo.db.models import (NetworkInterface, VirtualMachine,
from synnefo.db.models import (NetworkInterface, VirtualMachine, Network,
VirtualMachineMetadata, FloatingIP)
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
......@@ -125,7 +125,8 @@ def server_command(action):
@transaction.commit_manually
def create(userid, name, password, flavor, image, metadata={},
personality=[], network=None, use_backend=None):
personality=[], network=None, private_networks=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.
......@@ -154,13 +155,6 @@ def create(userid, name, password, flavor, image, metadata={},
flavor.disk_provider = None
try:
if network is None:
# Allocate IP from public network
(network, address) = util.get_public_ip(use_backend)
nic = {'ip': address, 'network': network.backend_id}
else:
address = util.get_network_free_address(network)
# We must save the VM instance now, so that it gets a valid
# vm.backend_vm_id.
vm = VirtualMachine.objects.create(
......@@ -171,12 +165,6 @@ def create(userid, name, password, flavor, image, metadata={},
flavor=flavor,
action="CREATE")
# Create VM's public NIC. Do not wait notification form ganeti hooks to
# create this NIC, because if the hooks never run (e.g. building error)
# the VM's public IP address will never be released!
NetworkInterface.objects.create(machine=vm, network=network, index=0,
ipv4=address, state="BUILDING")
log.info("Created entry in DB for VM '%s'", vm)
# dispatch server created signal
......@@ -188,6 +176,8 @@ def create(userid, name, password, flavor, image, metadata={},
'img_properties': json.dumps(image['metadata']),
})
nics = create_instance_nics(vm, userid, private_networks)
# Also we must create the VM metadata in the same transaction.
for key, val in metadata.items():
VirtualMachineMetadata.objects.create(
......@@ -205,7 +195,7 @@ def create(userid, name, password, flavor, image, metadata={},
transaction.commit()
try:
jobID = backend.create_instance(vm, [nic], flavor, image)
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"
......@@ -213,7 +203,7 @@ def create(userid, name, password, flavor, image, metadata={},
vm.save()
transaction.commit()
log.info("User %s created VM %s, NIC %s, Backend %s, JobID %s",
userid, vm, nic, backend, str(jobID))
userid, vm, nics, backend, str(jobID))
except GanetiApiError as e:
log.exception("Can not communicate to backend %s: %s.",
backend, e)
......@@ -232,6 +222,54 @@ def create(userid, name, password, flavor, image, metadata={},
return vm
def create_instance_nics(vm, userid, private_networks):
"""Create NICs for VirtualMachine.
Helper function for allocating IP addresses and creating NICs in the DB
for a VirtualMachine. Created NICs are the combination of the default
network policy (defined by administration settings) and the private
networks defined by the user.
"""
attachments = []
for network_id in settings.DEFAULT_INSTANCE_NETWORKS:
network, address = None, None
if network_id == "public":
network, address = util.get_public_ip(backend=vm.backend)
else:
try:
network = Network.objects.get(id=network_id, deleted=False)
except Network.DoesNotExist:
msg = "Invalid configuration. Setting"\
" 'DEFAULT_INSTANCE_NETWORKS' contains invalid"\
" network '%s'" % network_id
log.error(msg)
raise Exception(msg)
if network.dhcp:
address = util.get_network_free_address(network)
attachments.append((network, address))
for network_id in private_networks:
network, address = None, None
network = util.get_network(network_id, userid, non_deleted=True)
if network.public:
raise faults.Forbidden("Can not connect to public network")
if network.dhcp:
address = util.get_network_free_address(network)
attachments.append((network, address))
nics = []
for index, (network, address) in enumerate(attachments):
# Create VM's public NIC. Do not wait notification form ganeti
# hooks to create this NIC, because if the hooks never run (e.g.
# building error) the VM's public IP address will never be
# released!
nic = NetworkInterface.objects.create(machine=vm, network=network,
index=index, ipv4=address,
state="BUILDING")
nics.append(nic)
return nics
@server_command("DESTROY")
def destroy(vm):
log.info("Deleting VM %s", 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