Commit 31912de1 authored by Giorgos Verigakis's avatar Giorgos Verigakis
Browse files

Connect networks API with the Ganeti

* Create Ganeti network link when creating a network.
* Delete the link when deleting the network.
* Use a pool of GANETI_MAX_LINK_NUMBER network links.
* Adding or removing a server from a network requires reboot.

A database migration is needed.

Refs #513
Refs #411
parent dbaf24e9
......@@ -10,10 +10,9 @@ from django.template.loader import render_to_string
from django.utils import simplejson as json
from synnefo.api.faults import BadRequest, ServiceUnavailable
from synnefo.api.util import random_password, get_vm, get_nic
from synnefo.api.util import random_password, get_vm
from synnefo.util.vapclient import request_forwarding as request_vnc_forwarding
from synnefo.logic.backend import (reboot_instance, startup_instance, shutdown_instance,
get_instance_console)
from synnefo.logic import backend
from synnefo.logic.utils import get_rsapi_state
......@@ -76,7 +75,7 @@ def reboot(request, vm, args):
reboot_type = args.get('type', '')
if reboot_type not in ('SOFT', 'HARD'):
raise BadRequest('Malformed Request.')
reboot_instance(vm, reboot_type.lower())
backend.reboot_instance(vm, reboot_type.lower())
return HttpResponse(status=202)
@server_action('start')
......@@ -87,7 +86,7 @@ def start(request, vm, args):
if args:
raise BadRequest('Malformed Request.')
startup_instance(vm)
backend.startup_instance(vm)
return HttpResponse(status=202)
@server_action('shutdown')
......@@ -98,7 +97,7 @@ def shutdown(request, vm, args):
if args:
raise BadRequest('Malformed Request.')
shutdown_instance(vm)
backend.shutdown_instance(vm)
return HttpResponse(status=202)
@server_action('rebuild')
......@@ -196,7 +195,7 @@ def get_console(request, vm, args):
if settings.TEST:
console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
else:
console_data = get_instance_console(vm)
console_data = backend.get_instance_console(vm)
if console_data['kind'] != 'vnc':
raise ServiceUnavailable('Could not create a console of requested type.')
......@@ -251,7 +250,7 @@ def add(request, net, args):
if not server_id:
raise BadRequest('Malformed Request.')
vm = get_vm(server_id, request.user)
vm.nics.create(network=net)
backend.connect_to_network(vm, net)
vm.save()
net.save()
return HttpResponse(status=202)
......@@ -271,8 +270,7 @@ def remove(request, net, args):
if not server_id:
raise BadRequest('Malformed Request.')
vm = get_vm(server_id, request.user)
nic = get_nic(vm, net)
nic.delete()
backend.disconnect_from_network(vm, net)
vm.save()
net.save()
return HttpResponse(status=202)
......@@ -28,5 +28,8 @@ class ItemNotFound(Fault):
class BuildInProgress(Fault):
code = 409
class OverLimit(Fault):
code = 413
class ServiceUnavailable(Fault):
code = 503
from synnefo.api.actions import network_actions
from synnefo.api.common import method_not_allowed
from synnefo.api.faults import BadRequest, Unauthorized
from synnefo.api.faults import BadRequest, OverLimit, Unauthorized
from synnefo.api.util import (isoformat, isoparse, get_network,
get_request_dict, api_method)
from synnefo.db.models import Network
from synnefo.logic import backend
from django.conf.urls.defaults import patterns
from django.db.models import Q
......@@ -104,6 +105,10 @@ def create_network(request):
raise BadRequest('Malformed request.')
network = Network.objects.create(name=name, owner=request.user, state='ACTIVE')
if not backend.create_network(network):
network.delete()
raise OverLimit('Maximum number of networks reached.')
networkdict = network_to_dict(network)
return render_network(request, networkdict, status=202)
......@@ -157,11 +162,7 @@ def delete_network(request, network_id):
if network_id in ('1', 'public'):
raise Unauthorized('Can not delete the public network.')
net = get_network(network_id, request.user)
net.nics.all().delete()
for vm in net.machines.all():
vm.save()
net.state = 'DELETED'
net.save()
backend.delete_network(net)
return HttpResponse(status=204)
@api_method('POST')
......
......@@ -77,7 +77,9 @@ def nic_to_dict(nic):
'mac': nic.mac,
'firewallProfile': nic.firewall_profile}
if nic.ipv4 or nic.ipv6:
d['values'] = [{'version': 4, 'addr': nic.ipv4}, {'version': 6, 'addr': nic.ipv6}]
d['values'] = [
{'version': 4, 'addr': nic.ipv4 or ''},
{'version': 6, 'addr': nic.ipv6 or ''}]
return d
def metadata_to_dict(vm):
......
......@@ -895,21 +895,22 @@ class CreateNetwork(BaseTestCase):
class GetNetworkDetails(BaseTestCase):
SERVERS = 5
NETWORKS = 1
def test_get_network_details(self):
name = 'net'
self.create_network(name)
servers = VirtualMachine.objects.all()
network = Network.objects.all()[0]
network = Network.objects.all()[1]
net = self.get_network_details(network.id)
self.assertEqual(net['name'], network.name)
self.assertEqual(net['name'], name)
self.assertEqual(net['servers']['values'], [])
server_id = choice(servers).id
self.add_to_network(network.id, server_id)
net = self.get_network_details(network.id)
self.assertEqual(net['name'], network.name)
self.assertEqual(net['servers']['values'], [server_id])
class UpdateNetworkName(BaseTestCase):
......@@ -930,9 +931,10 @@ class UpdateNetworkName(BaseTestCase):
class DeleteNetwork(BaseTestCase):
NETWORKS = 5
def test_delete_network(self):
for i in range(5):
self.create_network('net%d' % i)
networks = self.list_networks()
network = choice(networks)
while network['id'] == 1:
......@@ -949,28 +951,18 @@ class DeleteNetwork(BaseTestCase):
class NetworkActions(BaseTestCase):
SERVERS = 20
NETWORKS = 1
def test_add_remove_server(self):
self.create_network('net')
server_ids = [vm.id for vm in VirtualMachine.objects.all()]
network = self.list_networks(detail=True)[0]
network = self.list_networks(detail=True)[1]
network_id = network['id']
to_add = set(sample(server_ids, 10))
for server_id in to_add:
self.add_to_network(network_id, server_id)
net = self.get_network_details(network_id)
self.assertTrue(server_id in net['servers']['values'])
net = self.get_network_details(network_id)
self.assertEqual(set(net['servers']['values']), to_add)
to_remove = set(sample(to_add, 5))
for server_id in to_remove:
self.remove_from_network(network_id, server_id)
net = self.get_network_details(network_id)
self.assertTrue(server_id not in net['servers']['values'])
net = self.get_network_details(network_id)
self.assertEqual(set(net['servers']['values']), to_add - to_remove)
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'NetworkLink'
db.create_table('db_networklink', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('network', self.gf('django.db.models.fields.related.OneToOneField')(related_name='link', unique=True, null=True, to=orm['db.Network'])),
('index', self.gf('django.db.models.fields.IntegerField')()),
('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('available', self.gf('django.db.models.fields.BooleanField')(default=True, blank=True)),
))
db.send_create_signal('db', ['NetworkLink'])
def backwards(self, orm):
# Deleting model 'NetworkLink'
db.delete_table('db_networklink')
models = {
'db.debit': {
'Meta': {'object_name': 'Debit'},
'description': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']"}),
'vm': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.VirtualMachine']"}),
'when': ('django.db.models.fields.DateTimeField', [], {})
},
'db.disk': {
'Meta': {'object_name': 'Disk'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']", 'null': 'True', 'blank': 'True'}),
'size': ('django.db.models.fields.PositiveIntegerField', [], {}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'vm': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.VirtualMachine']", 'null': 'True', 'blank': 'True'})
},
'db.flavor': {
'Meta': {'unique_together': "(('cpu', 'ram', 'disk'),)", 'object_name': 'Flavor'},
'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'db.flavorcost': {
'Meta': {'object_name': 'FlavorCost'},
'cost_active': ('django.db.models.fields.PositiveIntegerField', [], {}),
'cost_inactive': ('django.db.models.fields.PositiveIntegerField', [], {}),
'effective_from': ('django.db.models.fields.DateTimeField', [], {}),
'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'db.image': {
'Meta': {'object_name': 'Image'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']", 'null': 'True', 'blank': 'True'}),
'sourcevm': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.VirtualMachine']", 'null': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.imagemetadata': {
'Meta': {'object_name': 'ImageMetadata'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Image']"}),
'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'})
},
'db.limit': {
'Meta': {'object_name': 'Limit'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']"}),
'value': ('django.db.models.fields.IntegerField', [], {})
},
'db.network': {
'Meta': {'object_name': 'Network'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']", 'null': 'True'}),
'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.networkinterface': {
'Meta': {'object_name': 'NetworkInterface'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'index': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'ipv4': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True'}),
'ipv6': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
'mac': ('django.db.models.fields.CharField', [], {'max_length': '17', 'null': 'True'}),
'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'to': "orm['db.VirtualMachine']"}),
'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'to': "orm['db.Network']"}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.networklink': {
'Meta': {'object_name': 'NetworkLink'},
'available': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'index': ('django.db.models.fields.IntegerField', [], {}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'network': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'link'", 'unique': 'True', 'null': 'True', 'to': "orm['db.Network']"})
},
'db.synnefouser': {
'Meta': {'object_name': 'SynnefoUser'},
'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'credit': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
'realname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'uniq': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.virtualmachine': {
'Meta': {'object_name': 'VirtualMachine'},
'action': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'charged': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 29, 12, 32, 13, 412198)'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']"}),
'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'operstate': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']"}),
'sourceimage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Image']"}),
'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.virtualmachinegroup': {
'Meta': {'object_name': 'VirtualMachineGroup'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'symmetrical': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']"}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.virtualmachinemetadata': {
'Meta': {'object_name': 'VirtualMachineMetadata'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
'vm': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.VirtualMachine']"})
}
}
complete_apps = ['db']
......@@ -401,3 +401,10 @@ class NetworkInterface(models.Model):
ipv4 = models.IPAddressField(null=True)
ipv6 = models.CharField(max_length=100, null=True)
firewall_profile = models.CharField(choices=FIREWALL_PROFILES, max_length=30, null=True)
class NetworkLink(models.Model):
network = models.OneToOneField(Network, null=True, related_name='link')
index = models.IntegerField()
name = models.CharField(max_length=255)
available = models.BooleanField(default=True)
......@@ -5,7 +5,7 @@
#
from django.conf import settings
from synnefo.db.models import VirtualMachine
from synnefo.db.models import VirtualMachine, Network, NetworkLink
from synnefo.logic import utils
from synnefo.util.rapi import GanetiRapiClient
......@@ -52,16 +52,21 @@ def process_net_status(vm, nics):
detailing the NIC configuration of a VM instance.
Update the state of the VM in the DB accordingly.
"""
# For the time being, we can only update the ipfour field,
# based on the IPv4 address of the first NIC
if len(nics) > 0:
ipv4 = nics[0]['ip']
if ipv4 == '':
ipv4 = '0.0.0.0'
vm.ipfour = ipv4
vm.nics.all().delete()
for i, nic in enumerate(nics):
if i == 0:
net = Network.objects.filter(public=True)[0]
else:
link = NetworkLink.objects.get(name=nic['link'])
net = link.network
vm.nics.create(
network=net,
index=i,
mac=nic.get('mac', ''),
ipv4=nic.get('ip', ''))
vm.save()
......@@ -96,12 +101,15 @@ def start_action(vm, action):
def create_instance(vm, flavor, password):
# FIXME: `password` must be passed to the Ganeti OS provider via CreateInstance()
nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
return rapi.CreateInstance(
mode='create',
name=vm.backend_id,
disk_template='plain',
disks=[{"size": 2000}], #FIXME: Always ask for a 2GB disk for now
nics=[{}],
nics=[nic],
os='debootstrap+default', #TODO: select OS from imageRef
ip_check=False,
name_check=False,
......@@ -111,24 +119,78 @@ def create_instance(vm, flavor, password):
def delete_instance(vm):
start_action(vm, 'DESTROY')
rapi.DeleteInstance(vm.backend_id)
rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
vm.nics.all().delete()
def reboot_instance(vm, reboot_type):
assert reboot_type in ('soft', 'hard')
rapi.RebootInstance(vm.backend_id, reboot_type)
rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
def startup_instance(vm):
start_action(vm, 'START')
rapi.StartupInstance(vm.backend_id)
rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
def shutdown_instance(vm):
start_action(vm, 'STOP')
rapi.ShutdownInstance(vm.backend_id)
rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
def get_instance_console(vm):
return rapi.GetInstanceConsole(vm.backend_id)
def create_network_link():
try:
last = NetworkLink.objects.order_by('-index')[0]
index = last.index + 1
except IndexError:
index = 1
if index <= settings.GANETI_MAX_LINK_NUMBER:
name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
return NetworkLink.objects.create(index=index, name=name, available=True)
return None # All link slots are filled
def create_network(net):
try:
link = NetworkLink.objects.filter(available=True)[0]
except IndexError:
link = create_network_link()
if not link:
return False
link.network = net
link.available = False
link.save()
return True
def delete_network(net):
link = net.link
link.available = True
link.netowrk = False
link.save()
for vm in net.machines.all():
disconnect_from_network(vm, net)
vm.save()
net.state = 'DELETED'
net.save()
def connect_to_network(vm, net):
nic = {'mode': 'bridged', 'link': net.link.name}
rapi.ModifyInstance(vm.backend_id, nics=[('add', nic)], dry_run=settings.TEST)
def disconnect_from_network(vm, net):
nics = vm.nics.order_by('index')[1:] # Skip the public network
ops = [('remove', {})] * len(nics)
for nic in nics:
if nic.network == net:
continue
ops.append(('add', {
'mode': 'bridged',
'link': nic.network.link.name,
'mac': nic.mac}))
for op in ops:
rapi.ModifyInstance(vm.backend_id, nics=[op], dry_run=settings.TEST)
......@@ -147,7 +147,7 @@ class ProcessNetStatusTestCase(TestCase):
}
vm = VirtualMachine.objects.get(pk=30000)
backend.process_net_status(vm, msg['nics'])
self.assertEquals(vm.ipfour, '192.168.33.1')
self.assertEquals(vm.nics.all()[0].ipv4, '192.168.33.1')
def test_set_empty_ipv4(self):
"""Test reception of a net status notification with no IPv4 assigned"""
......@@ -159,4 +159,4 @@ class ProcessNetStatusTestCase(TestCase):
}
vm = VirtualMachine.objects.get(pk=30000)
backend.process_net_status(vm, msg['nics'])
self.assertEquals(vm.ipfour, '0.0.0.0')
self.assertEquals(vm.nics.all()[0].ipv4, '')
......@@ -239,3 +239,6 @@ LOGIN_PATH = "/login"
# work after this many hours after 2011/05/10
AUTH_TOKEN_DURATION = 30 * 24
GANETI_PUBLIC_LINK = 'snf_public'
GANETI_LINK_PREFIX = 'prv'
GANETI_MAX_LINK_NUMBER = 100
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