Revision 77f0fa63

b/snf-cyclades-app/synnefo/api/actions.py
34 34
from socket import getfqdn
35 35
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
36 36

  
37
from django.db import transaction
37 38
from django.conf import settings
38 39
from django.http import HttpResponse
39 40
from django.template.loader import render_to_string
......
42 43
from synnefo.api.faults import (BadRequest, ServiceUnavailable,
43 44
                                ItemNotFound, BuildInProgress)
44 45
from synnefo.api.util import random_password, get_vm, get_nic_from_index
45
from synnefo.db.models import NetworkInterface
46
from synnefo.logic import backend
46
from synnefo.db.models import NetworkInterface, Network
47
from synnefo.logic import backend, ippool
47 48
from synnefo.logic.utils import get_rsapi_state
48 49

  
49 50

  
......
289 290

  
290 291

  
291 292
@network_action('add')
293
@transaction.commit_on_success
292 294
def add(request, net, args):
293 295
    # Normal Response Code: 202
294 296
    # Error Response Codes: computeFault (400, 500),
......
303 305
    if not server_id:
304 306
        raise BadRequest('Malformed Request.')
305 307
    vm = get_vm(server_id, request.user_uniq)
306
    backend.connect_to_network(vm, net)
308

  
309
    #Dummy write to enforce isolation
310
    Network.objects.filter(id=net.id).update(id=net.id)
311
    net = Network.objects.get(id=net.id)
312
    # Get a free IP from the address pool.
313
    pool = ippool.IPPool(net)
314
    try:
315
        address = pool.get_free_address()
316
    except ippool.IPPool.IPPoolExhausted:
317
        raise ServiceUnavailable('Network is full')
318
    pool.save()
319

  
320
    backend.connect_to_network(vm, net, address)
307 321
    return HttpResponse(status=202)
308 322

  
323

  
309 324
@network_action('remove')
310 325
def remove(request, net, args):
311 326
    # Normal Response Code: 202
b/snf-cyclades-app/synnefo/db/migrations/0045_auto__chg_field_networkinterface_mac__add_field_network_reservations.py
1
# encoding: utf-8
2
import datetime
3
from south.db import db
4
from south.v2 import SchemaMigration
5
from django.db import models
6

  
7
class Migration(SchemaMigration):
8
    
9
    def forwards(self, orm):
10
        
11
        # Changing field 'NetworkInterface.mac'
12
        db.alter_column('db_networkinterface', 'mac', self.gf('django.db.models.fields.CharField')(max_length=32, null=True))
13

  
14
        # Adding field 'Network.reservations'
15
        db.add_column('db_network', 'reservations', self.gf('django.db.models.fields.TextField')(default=''), keep_default=False)
16
    
17
    
18
    def backwards(self, orm):
19
        
20
        # Changing field 'NetworkInterface.mac'
21
        db.alter_column('db_networkinterface', 'mac', self.gf('django.db.models.fields.CharField')(max_length=17, null=True))
22

  
23
        # Deleting field 'Network.reservations'
24
        db.delete_column('db_network', 'reservations')
25
    
26
    
27
    models = {
28
        'db.backend': {
29
            'Meta': {'object_name': 'Backend'},
30
            'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
31
            'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
32
            'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
33
            'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
34
            'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
35
            'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
36
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
37
            'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
38
            'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
39
            'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
40
            'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
41
            'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
42
            'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}),
43
            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
44
            'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
45
        },
46
        'db.backendnetwork': {
47
            'Meta': {'object_name': 'BackendNetwork'},
48
            'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'to': "orm['db.Backend']"}),
49
            'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
50
            'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
51
            'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}),
52
            'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
53
            'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}),
54
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
55
            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
56
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
57
            'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'to': "orm['db.Network']"}),
58
            'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}),
59
            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
60
        },
61
        'db.bridgepool': {
62
            'Meta': {'object_name': 'BridgePool'},
63
            'available': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
64
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
65
            'index': ('django.db.models.fields.IntegerField', [], {'unique': 'True'}),
66
            'value': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'})
67
        },
68
        'db.flavor': {
69
            'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'},
70
            'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
71
            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
72
            'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
73
            'disk_template': ('django.db.models.fields.CharField', [], {'default': "'drbd'", 'max_length': '32'}),
74
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
75
            'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'})
76
        },
77
        'db.macprefixpool': {
78
            'Meta': {'object_name': 'MacPrefixPool'},
79
            'available': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
80
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
81
            'index': ('django.db.models.fields.IntegerField', [], {'unique': 'True'}),
82
            'value': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'})
83
        },
84
        'db.network': {
85
            'Meta': {'object_name': 'Network'},
86
            'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}),
87
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
88
            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
89
            'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
90
            'gateway': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
91
            'gateway6': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
92
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
93
            'link': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
94
            'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
95
            'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}),
96
            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
97
            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
98
            'reservations': ('django.db.models.fields.TextField', [], {'default': "''"}),
99
            'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}),
100
            'subnet': ('django.db.models.fields.CharField', [], {'default': "'10.0.0.0/24'", 'max_length': '32'}),
101
            'subnet6': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
102
            'type': ('django.db.models.fields.CharField', [], {'default': "'PRIVATE_PHYSICAL_VLAN'", 'max_length': '50'}),
103
            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
104
            'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'})
105
        },
106
        'db.networkinterface': {
107
            'Meta': {'object_name': 'NetworkInterface'},
108
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
109
            'dirty': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
110
            'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
111
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
112
            'index': ('django.db.models.fields.IntegerField', [], {}),
113
            'ipv4': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'}),
114
            'ipv6': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
115
            'mac': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
116
            'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'to': "orm['db.VirtualMachine']"}),
117
            'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'to': "orm['db.Network']"}),
118
            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
119
        },
120
        'db.virtualmachine': {
121
            'Meta': {'object_name': 'VirtualMachine'},
122
            'action': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
123
            'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'to': "orm['db.Backend']"}),
124
            'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
125
            'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
126
            'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
127
            'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}),
128
            'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
129
            'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}),
130
            'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
131
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
132
            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
133
            'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']"}),
134
            'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
135
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
136
            'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
137
            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
138
            'operstate': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
139
            'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
140
            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
141
            'userid': ('django.db.models.fields.CharField', [], {'max_length': '100'})
142
        },
143
        'db.virtualmachinemetadata': {
144
            'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'},
145
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
146
            'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
147
            'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
148
            'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"})
149
        }
150
    }
151
    
152
    complete_apps = ['db']
b/snf-cyclades-app/synnefo/db/models.py
36 36
from hashlib import sha1
37 37
from synnefo.api.faults import ServiceUnavailable
38 38
from synnefo.util.rapi import GanetiRapiClient
39
from synnefo.logic.ippool import IPPool
39 40
from synnefo import settings as snf_settings
40 41
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
41 42

  
......
407 408
        return u'%s: %s' % (self.meta_key, self.meta_value)
408 409

  
409 410

  
410

  
411 411
class Network(models.Model):
412 412
    OPER_STATES = (
413 413
        ('PENDING', 'Pending'),
......
436 436
        ('CUSTOM_BRIDGED', 'Custom bridged network')
437 437
    )
438 438

  
439

  
440 439
    name = models.CharField('Network Name', max_length=128)
441 440
    userid = models.CharField('User ID of the owner', max_length=128, null=True)
442 441
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
......
459 458
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
460 459
                              default=None)
461 460

  
461
    reservations = models.TextField(default='')
462

  
463
    ip_pool = None
464

  
465

  
462 466
    class InvalidBackendIdError(Exception):
463 467
        def __init__(self, value):
464 468
            self.value = value
......
538 542
            for back in Backend.objects.all():
539 543
                BackendNetwork.objects.create(backend=back, network=self)
540 544

  
545
    @property
546
    def pool(self):
547
        if self.ip_pool:
548
            return self.ip_pool
549
        else:
550
            self.ip_pool = IPPool(self)
551
            return self.ip_pool
552

  
553
    def reserve_address(self, address, pool=None):
554
        pool = pool or self.pool
555
        pool.reserve(address)
556
        pool._update_network()
557
        self.save()
558

  
559
    def release_address(self, address, pool=None):
560
        pool = pool or self.pool
561
        pool.release(address)
562
        pool._update_network()
563
        self.save()
564

  
565
    # def get_free_address(self):
566
    #     # Get yourself inside a transaction
567
    #     network = Network.objects.get(id=self.id)
568
    #     # Get the pool object
569
    #     pool = network.pool
570
    #     print network is self
571
    #     try:
572
    #         address = pool.get_free_address()
573
    #     except IPPoolExhausted:
574
    #         raise Network.NetworkIsFull
575

  
576
    #     pool._update_network()
577
    #     network.save()
578
    #     return address
579

  
580
    # class NetworkIsFull(Exception):
581
    #     pass
582

  
541 583

  
542 584
class BackendNetwork(models.Model):
543 585
    OPER_STATES = (
......
609 651
    created = models.DateTimeField(auto_now_add=True)
610 652
    updated = models.DateTimeField(auto_now=True)
611 653
    index = models.IntegerField(null=False)
612
    mac = models.CharField(max_length=17, null=True)
654
    mac = models.CharField(max_length=32, null=True)
613 655
    ipv4 = models.CharField(max_length=15, null=True)
614 656
    ipv6 = models.CharField(max_length=100, null=True)
615 657
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
b/snf-cyclades-app/synnefo/logic/backend.py
114 114
    Update the state of the VM in the DB accordingly.
115 115
    """
116 116

  
117
    vm.nics.all().delete()
118
    for i, nic in enumerate(nics):
119
        network = nic.get('network', '')
117
    old_nics = vm.nics.order_by('index')
118
    new_nics = enumerate(nics)
119

  
120
    networks = {}
121

  
122
    for old_nic in old_nics:
123
        pk = old_nic.network.pk
124
        # Get the cached Network or get it from DB
125
        if pk in networks:
126
            net = networks[pk]
127
        else:
128
            #Dummy write to enforce isolation
129
            Network.objects.filter(id=pk).update(id=pk)
130
            net = Network.objects.get(pk=pk)
131
        net.release_address(old_nic.ipv4)
132
        old_nic.delete()
133

  
134
    for i, new_nic in new_nics:
135
        network = new_nic.get('network', '')
120 136
        n = str(network)
121
        if n == settings.GANETI_PUBLIC_NETWORK:
122
            net = Network.objects.get(public=True)
137
        pk = utils.id_from_network_name(n)
138

  
139
        # Get the cached Network or get it from DB
140
        if pk in networks:
141
            net = networks[pk]
123 142
        else:
124
            pk = utils.id_from_network_name(n)
125
            net = Network.objects.get(id=pk)
143
            net = Network.objects.get(pk=pk)
144

  
145
        # Get the new nic info
146
        mac = new_nic.get('mac', '')
147
        ipv4 = new_nic.get('ip', '')
148
        ipv6 = new_nic.get('ipv6', '')
126 149

  
127
        firewall = nic.get('firewall', '')
150
        firewall = new_nic.get('firewall', '')
128 151
        firewall_profile = _reverse_tags.get(firewall, '')
129 152
        if not firewall_profile and net.public:
130 153
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
131 154

  
155
        if ipv4:
156
            net.reserve_address(ipv4)
157

  
132 158
        vm.nics.create(
133 159
            network=net,
134 160
            index=i,
135
            mac=nic.get('mac', ''),
136
            ipv4=nic.get('ip', ''),
137
            ipv6=nic.get('ipv6', ''),
161
            mac=mac,
162
            ipv4=ipv4,
163
            ipv6=ipv6,
138 164
            firewall_profile=firewall_profile,
139 165
            dirty=False)
140 166

  
141 167
    vm.backendtime = etime
142 168
    vm.save()
143
    net.save()
144 169

  
145 170

  
146 171
@transaction.commit_on_success
......
490 515
        backend.client.DeleteNetwork(network.backend_id, jobs)
491 516

  
492 517

  
493
def connect_to_network(vm, network):
518
def connect_to_network(vm, network, address):
494 519
    """Connect a virtual machine to a network.
495 520

  
496 521
    @param vm: VirtualMachine object
......
498 523

  
499 524
    """
500 525

  
501
    ip = network.dhcp and 'pool' or None
526
    # ip = network.dhcp and 'pool' or None
502 527

  
503
    nic = {'ip': ip, 'network': network.backend_id}
528
    nic = {'ip': address, 'network': network.backend_id}
504 529
    vm.client.ModifyInstance(vm.backend_vm_id, nics=[('add',  nic)],
505 530
                             hotplug=True, dry_run=settings.TEST)
506 531

  
b/snf-cyclades-app/synnefo/logic/ippool.py
1
import ipaddr
2

  
3
from bitarray import bitarray
4
from base64 import b64encode, b64decode
5

  
6

  
7
class IPPool(object):
8
    def __init__(self, network):
9
        self.net = network
10
        self.network = ipaddr.IPNetwork(self.net.subnet)
11

  
12
        gateway = self.net.gateway
13
        self.gateway = gateway and ipaddr.IPAddress(gateway) or None
14

  
15
        if self.net.reservations:
16
            self.reservations = bitarray()
17
            self.reservations.fromstring(b64decode(self.net.reservations))
18
        else:
19
            numhosts = self.network.numhosts
20
            self.reservations = bitarray(numhosts)
21
            self.reservations.setall(False)
22
            self.reservations[0] = True
23
            self.reservations[numhosts - 1] = True
24
            # if self.net.type == 'PUBLIC_ROUTED':
25
            #     self.reservations[numhosts - 2] = True
26

  
27
    def _contains(self, address):
28
        if address is None:
29
            return False
30
        addr = ipaddr.IPAddress(address)
31

  
32
        return (addr in self.network)
33

  
34
    def _address_index(self, address):
35
        """Convert IP address to bitarray index
36

  
37
        """
38
        if not self._contains(address):
39
            raise Exception("%s does not contain %s" % (self.network, address))
40
        addr = ipaddr.IPAddress(address)
41

  
42
        return int(addr) - int(self.network.network)
43

  
44
    def _mark(self, address, value):
45
        index = self._address_index(address)
46
        self.reservations[index] = value
47

  
48
    def reserve(self, address):
49
        self._mark(address, True)
50

  
51
    def release(self, address):
52
        self._mark(address, False)
53

  
54
    def is_reserved(self, address):
55
        index = self._address_index(address)
56
        return self.reservations[index]
57

  
58
    def is_full(self):
59
        return self.reservations.all()
60

  
61
    def count_reserved(self):
62
        return self.reservations.count(True)
63

  
64
    def count_free(self):
65
        return self.reservations.count(False)
66

  
67
    def get_map(self):
68
        return self.reservations.to01().replace("1", "X").replace("0", ".")
69

  
70
    def get_free_address(self):
71
        if self.is_full():
72
            raise Exception("%s if full", self.network)
73

  
74
        index = self.reservations.index(False)
75
        address = str(self.network[index])
76
        self.reserve(address)
77
        return address
78

  
79
    def save(self):
80
        self._update_network()
81
        self.net.save()
82

  
83
    def _update_network(self):
84
        self.net.reservations = b64encode(self.reservations.tostring())
85

  
86
    class IPPoolExhausted(Exception):
87
        pass

Also available in: Unified diff