Revision 452d2391

b/.gitignore
12 12
*.installed.cfg
13 13
*.sqlite
14 14
.Python
15
.idea
16
.DS_Store
b/db/charger.py
8 8

  
9 9
from db.models import *
10 10

  
11
from datetime import datetime
12 11

  
13
def stop_virtual_machine(vm):
14
    """Send message to stop a virtual machine instance"""
15
    
16
    # send the message to ganeti
17
    
18
    return
19

  
20
def charge():
12
def periodically_charge():
21 13
    """Scan all virtual machines and charge each user"""
22 14
    all_vms = VirtualMachine.objects.all()
23 15
    
24 16
    if len(all_vms) == 0:
25 17
        print "No virtual machines found"
18
        return
26 19
    
27 20
    for vm in all_vms:
28
        cost = 0
29
        
30 21
        # Running and Stopped is charged, else the cost is zero
31
        
32
        
33
        start = vm.charged
34
        end = datetime.now()
35
        user_credits = vm.owner.charge_credits(cost, start, end)
36
        vm.charged = end
37
        
38
        # update the values in the database
39
        vm.save()
40
        vm.owner.save()
41
        
42
        if user_credits <= 0:
43
            stop_virtual_machine(vm)
22
        # FIXME: not implemented!
23
        vm.charge()
44 24

  
45 25
# vim: set ts=4 sts=4 sw=4 et ai :
b/db/fixtures/db_test_data.json
4 4
        "pk": 30000,
5 5
        "fields": {
6 6
            "name": "test db user",
7
            "credit": 0,
7
            "credit": 10,
8 8
            "created": "2011-02-06",
9 9
            "user" : 1
10 10
   	    }
......
22 22
        "model" : "db.Limit",
23 23
        "pk" : 30001,
24 24
        "fields" : {
25
            "name" : "MAX_VIOLATIONS",
25
            "name" : "MIN_CREDITS",
26 26
            "user" : 30000,
27
            "value" : 5
27
            "value" : -100
28 28
        }
29 29
    },
30 30
    {
......
46 46
        }
47 47
    },
48 48
    {
49
        "model": "db.Flavor",
50
        "pk": 30001,
51
        "fields": {
52
            "cpu": 1,
53
            "ram": 1024,
54
            "disk": 20
55
        }
56
    },
57
    {
58
        "model" : "db.FlavorCostHistory",
49
        "model" : "db.FlavorCost",
59 50
        "pk" : 30000,
60 51
        "fields" : {
61 52
            "cost_active" : 10,
......
65 56
        }
66 57
    },
67 58
    {
68
        "model" : "db.FlavorCostHistory",
59
        "model" : "db.FlavorCost",
69 60
        "pk" : 30001,
70 61
        "fields" : {
71 62
            "cost_active" : 2,
......
75 66
        }
76 67
    },
77 68
    {
69
        "model" : "db.FlavorCost",
70
        "pk" : 30002,
71
        "fields" : {
72
            "cost_active" : 5,
73
            "cost_inactive" : 2,
74
            "effective_from" : "2010-08-01",
75
            "flavor" : 30000
76
        }
77
    },
78
    {
79
        "model" : "db.FlavorCost",
80
        "pk" : 30003,
81
        "fields" : {
82
            "cost_active" : 4,
83
            "cost_inactive" : 10,
84
            "effective_from" : "2010-01-01",
85
            "flavor" : 30000
86
        }
87
    },
88
    {
78 89
        "model": "db.VirtualMachine",
79 90
        "pk": 30000,
80 91
        "fields": {
......
91 102
        }
92 103
    },
93 104
    {
94
        "model": "db.VirtualMachine",
95
        "pk": 30001,
96
        "fields": {
97
            "name": "snf-1002",
98
            "created": "2011-02-10 00:00:00",
99
            "charged": "2011-02-10 00:00:00",
100
            "sourceimage": 30000,
101
            "hostid": "HAL-9000",
102
            "description": "mail server",
103
            "ipfour": "192.168.2.2",
104
            "ipsix": "::2",
105
            "flavor": 30000,
106
            "_operstate": "BUILD"
107
        }
108
    },
109
    {
110
        "model": "db.VirtualMachine",
111
        "pk": 30002,
112
        "fields": {
113
            "name": "snf-1003",
114
            "created": "2009-02-10 00:00:00",
115
            "charged": "2010-01-01 00:00:00",
116
            "sourceimage": 30000,
117
            "hostid": "HAL-9000",
118
            "description": "my server",
119
            "ipfour": "192.168.2.3",
120
            "ipsix": "::3",
121
            "flavor": 30000,
122
            "_operstate": "STARTED"
123
        }
124
    },
125
    {
126
        "model": "db.VirtualMachine",
127
        "pk": 30003,
128
        "fields": {
129
            "name": "snf-1004",
130
            "created": "2011-02-10 00:00:00",
131
            "charged": "2011-02-10 00:00:00",
132
            "sourceimage": 30000,
133
            "hostid": "HAL-9000",
134
            "description": "my 2nd server",
135
            "ipfour": "192.168.2.4",
136
            "ipsix": "::4",
137
            "flavor": 30000,
138
            "_operstate": "STARTED"
139
        }
140
    },
141
    {
142
        "model" : "db.Debit",
143
        "pk" : 30000,
144
        "fields" : {
145
            "vm" : 30000,
146
            "date" : "2010-01-01",
147
            "state" : "STARTED"
148
        }
149
    },
150
    {
151
        "model" : "db.Debit",
152
        "pk" : 30001,
153
        "fields" : {
154
            "vm" : 30000,
155
            "date" : "2011-02-01",
156
            "state" : "STARTED"
157
        }
158
    },
159
    {
160
        "model" : "db.Debit",
161
        "pk" : 30002,
162
        "fields" : {
163
            "vm" : 30002,
164
            "date" : "2010-01-01",
165
            "state" : "STARTED"
166
        }
167
    },
168
    {
169
        "model" : "db.Debit",
170
        "pk" : 30003,
171
        "fields" : {
172
            "vm" : 30002,
173
            "date" : "2010-03-01",
174
            "state" : "STOPPED"
175
        }
176
    },
177
    {
178
        "model" : "db.Debit",
179
        "pk" : 30004,
180
        "fields" : {
181
            "vm" : 30002,
182
            "date" : "2011-01-01",
183
            "state" : "STARTED"
184
        }
185
    },
186
    {
187 105
        "model": "db.Image",
188 106
        "pk": 30000,
189 107
        "fields": {
190 108
            "name": "Debian Squeeze",
191 109
            "updated": "2011-02-06 00:00:00",
192 110
            "created": "2011-02-06 00:00:00",
111
            "size" : 2000,
193 112
            "state": "ACTIVE",
194 113
            "description": "Full Debian Squeeze Installation",
195 114
            "owner" : 30000
196 115
        }
197 116
    },
198 117
    {
199
        "model": "db.Image",
200
        "pk": 30001,
201
        "fields": {
202
            "name": "Slackware 13.1",
203
            "updated": "2011-02-10 00:00:00",
204
            "created": "2011-02-10 00:00:00",
205
            "state": "ACTIVE",
206
            "description": "Full Slackware 13.1 Installation",
207
            "owner" : 30000
208
        }
209
    },   
210
    {
211 118
        "model": "db.Disk",
212 119
        "pk": 30000,
213 120
        "fields": {
......
217 124
            "vm" : 30000,
218 125
            "owner" : 30000
219 126
        }
220
    }, 
221
    {
222
        "model": "db.Disk",
223
        "pk": 30001,
224
        "fields": {
225
            "name": "My_Videos",
226
            "created": "2011-02-10 00:00:00",
227
            "size" : 300,
228
            "vm" : 30000,
229
            "owner" : 30000
230
        }
231
    } 
127
    }
232 128
]
b/db/management/commands/charge_users.py
12 12
    help = 'Charge the users for VM usage'
13 13
    
14 14
    def handle_noargs(self, **options):
15
        charger.charge()
15
        charger.periodically_charge()
b/db/models.py
3 3
from django.conf import settings
4 4
from django.db import models
5 5
from django.contrib.auth.models import User
6
from django.utils.translation import gettext_lazy as _
7 6

  
8 7
import datetime
9 8

  
......
50 49
    
51 50
    def debit_account(self, amount, vm, description):
52 51
        """Charges the user with the specified amount of credits for a vm (resource)"""
53
        return
52
        date_now = datetime.datetime.now()
53

  
54
        # first reduce the user's credits and save
55
        self.credit = self.credit - amount
56
        self.save()
57

  
58
        # then write the debit entry!
59
        debit = Debit()
60

  
61
        debit.user = self
62
        debit.vm = vm
63
        debit.when = date_now
64
        debit.description = description
65

  
66
        debit.save()
54 67

  
55 68
    def credit_account(self, amount, creditor, description):
56 69
        """No clue :)"""
......
78 91
        verbose_name = u'Image'
79 92

  
80 93
    def __unicode__(self):
81
        return u'%s' % (self.name)
94
        return u'%s' % ( self.name, )
82 95

  
83 96

  
84 97
class ImageMetadata(models.Model):
......
107 120
        verbose_name = u'Enforced limit for user'
108 121
    
109 122
    def __unicode__(self):
110
        return u'Limit %s for user %s: %d' % (self.limit, self.user, self.value)
123
        return u'Limit %s for user %s: %d' % (self.value, self.user, self.value)
111 124

  
112 125

  
113 126
class Flavor(models.Model):
......
122 135
    def _get_name(self):
123 136
        """Returns flavor name (generated)"""
124 137
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
125
        
126
    def _current_cost_active(self):
138

  
139
    def _current_cost(self, active):
140
        """Returns active/inactive cost value
141

  
142
        set active = True to get active cost and False for the inactive.
143

  
144
        """
127 145
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
128 146
        if len(fch_list) > 0:
129
            return fch_list[0].cost_active
130
        
147
            if active:
148
                return fch_list[0].cost_active
149
            else:
150
                return fch_list[0].cost_inactive
151

  
131 152
        return 0
153
        
154

  
155
    def _current_cost_active(self):
156
        """Returns current active cost (property method)"""
157
        return self._current_cost(True)
132 158

  
133 159
    def _current_cost_inactive(self):
134
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
135
        if len(fch_list) > 0:
136
            return fch_list[0].cost_inactive
137
        
138
        return 0
160
        """Returns current inactive cost (property method)"""
161
        return self._current_cost(False)
139 162

  
140 163
    name = property(_get_name)
141 164
    current_cost_active = property(_current_cost_active)
......
143 166

  
144 167
    def __unicode__(self):
145 168
        return self.name
146
    
169

  
170
    def _get_costs(self, start_datetime, end_datetime, active):
171
        """Return a list with FlavorCost objects for the specified duration"""
172
        def between(enh_fc, a_date):
173
            """Checks if a date is between a FlavorCost duration"""
174
            if enh_fc.effective_from <= a_date and enh_fc.effective_to is None:
175
                return True
176

  
177
            return enh_fc.effective_from <= a_date and enh_fc.effective_to >= a_date
178

  
179
        def calculate_cost(start_date, end_date, cost):
180
            """Calculate the total cost for the specified duration"""
181
            td = end_date - start_date
182
            sec = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
183
            total_hours = float(sec) / float(60.0*60.0)
184
            total_cost = float(cost)*total_hours
185

  
186
            return round(total_cost)
187

  
188
        price_list = FlavorCost.objects.filter(flavor=self).order_by('effective_from')
189

  
190
        for idx in range(0, len(price_list)):
191
            if idx + 1 == len(price_list):
192
                price_list[idx].effective_to = None
193
            else:
194
                price_list[idx].effective_to = price_list[idx + 1].effective_from
195

  
196
        price_result = []
197
        found_start = False
198

  
199
        for p in price_list:
200
            if between(p, start_datetime):
201
                found_start = True
202
                p.effective_from = start_datetime
203
            if between(p, end_datetime):
204
                p.effective_to = end_datetime
205
                price_result.append(p)
206
                break
207
            if found_start:
208
                price_result.append(p)
209

  
210
        results = []
211

  
212
        for p in price_result:
213
            if active:
214
                cost = p.cost_active
215
            else:
216
                cost = p.cost_inactive
217

  
218
            results.append( ( str(p.effective_from), calculate_cost(p.effective_from, p.effective_to, cost)) )
219

  
220
        return results
221

  
147 222
    def get_cost_active(self, start_datetime, end_datetime):
148 223
        """Returns a list with the active costs for the specified duration"""
149
        
150
        return []
151
        
224
        return self._get_costs(start_datetime, end_datetime, True)
225

  
152 226
    def get_cost_inactive(self, start_datetime, end_datetime):
153 227
        """Returns a list with the active costs for the specified duration"""
154
        return []
228
        return self._get_costs(start_datetime, end_datetime, False)
155 229

  
156 230

  
157 231
class FlavorCost(models.Model):
158 232
    cost_active = models.PositiveIntegerField('Active Cost')
159 233
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
160
    effective_from = models.DateField()
234
    effective_from = models.DateTimeField()
161 235
    flavor = models.ForeignKey(Flavor)
162 236
    
163 237
    class Meta:
......
377 451
        Charges the VM owner from vm.charged to datetime.now()
378 452
        
379 453
        """
380
        return
454
        cur_datetime = datetime.datetime.now()
455
        cost_list = []
456

  
457
        if self._operstate == 'STARTED':
458
            cost_list = self.flavor.get_cost_active(self.charged, cur_datetime)
459
        else:
460
            cost_list = self.flavot.get_cost_inactive(self.charged, cur_datetime)
461

  
462
        total_cost = 0
463

  
464
        for cl in cost_list:
465
            total_cost = total_cost + cl[1]
466

  
467
        # still need to set correctly the message
468
        self.owner.debit_account(total_cost, self, "Charged Credits to User")
469

  
470
        self.charged = cur_datetime
471
        self.save()
381 472

  
382 473

  
383 474
class VirtualMachineGroup(models.Model):
......
419 510
        verbose_name = u'Accounting log'
420 511

  
421 512
    def __unicode__(self):
422
        return u'%s - %s - %s' % (self.vm.name, str(self.date), self.state)
513
        return u'%s - %s - %s - %s' % ( self.user.id, self.vm.name, str(self.when), self.description)
423 514

  
424 515

  
425 516
class Disk(models.Model):
b/db/tests.py
6 6
# Copyright 2010 Greek Research and Technology Network
7 7
#
8 8

  
9
import unittest
10

  
11
from datetime import datetime, date, timedelta
12

  
13 9
from db.models import *
14
from db import credit_allocator
15
from db import charger
16 10

  
17
from django.conf import settings
18
from django.contrib.auth.models import User
19 11
from django.test import TestCase
20 12

  
21
class CreditAllocatorTestCase(TestCase):
22
    fixtures = [ 'db_test_data' ]
23
        
24
    def test_credit_allocator(self):
25
        """Credit Allocator unit test method"""
26
        # test the allocator
27
        credit_allocator.allocate_credit()
28
        
29
        user = SynnefoUser.objects.get(pk=30000)
30
        self.assertEquals(user.credit, 10, 'Allocation of credits failed, credit: (%d!=10)' % ( user.credit, ) )
31
        
32
        # get the quota from Limit model and check the answer
33
        limit_quota = user.credit_quota
34
        self.assertEquals(limit_quota, 100, 'User quota has not retrieved correctly (%d!=100)' % ( limit_quota, ))
35
        
36
        # test if the quota policy is endorced
37
        for i in range(1, 10):
38
            credit_allocator.allocate_credit()
39
                
40
        user = SynnefoUser.objects.get(pk=30000)
41
        self.assertEquals(user.credit, limit_quota, 'User exceeded quota! (cr:%d, qu:%d)' % ( user.credit, limit_quota ) )
42

  
43 13

  
44 14
class FlavorTestCase(TestCase):
45 15
    fixtures = [ 'db_test_data' ]
46
    
16

  
47 17
    def test_flavor(self):
48 18
        """Test a flavor object, its internal cost calculation and naming methods"""
49 19
        flavor = Flavor.objects.get(pk=30000)
50
        
51
        flavor_name = u'C%dR%dD%d' % ( flavor.cpu, flavor.ram, flavor.disk )
52
        
53
        self.assertEquals(flavor.cost_active, 10, 'Active cost is not calculated correctly! (%d!=10)' % ( flavor.cost_active, ) )
54
        self.assertEquals(flavor.cost_inactive, 5, 'Inactive cost is not calculated correctly! (%d!=5)' % ( flavor.cost_inactive, ) )
55
        self.assertEquals(flavor.name, flavor_name, 'Invalid flavor name!')
56

  
57
    def test_flavor_cost_history(self):
58
        """Flavor unit test (find_cost method)"""
59
        flavor = Flavor.objects.get(pk=30000)
60
        fch_list = flavor.get_price_list()
61 20

  
62
        self.assertEquals(len(fch_list), 2, 'Price list should have two objects! (%d!=2)' % ( len(fch_list), ))
21
        # test current active/inactive costs
22
        c_active = flavor.current_cost_active
23
        c_inactive = flavor.current_cost_inactive
63 24

  
64
        # 2010-10-10, active should be 2, inactive 1
65
        ex_date = date(year=2010, month=10, day=10)
66
        r = flavor.find_cost(ex_date)
25
        self.assertEqual(c_active, 10, 'flavor.cost_active should be 10! (%d)' % ( c_active, ))
26
        self.assertEqual(c_inactive, 5, 'flavor.cost_inactive should be 5! (%d)' % ( c_inactive, ))
67 27

  
68
        self.assertEquals(r.cost_active, 2, 'Active cost for 2010-10-10 should be 2 (%d!=2)' % ( r.cost_active, ))
69
        self.assertEquals(r.cost_inactive, 1, 'Inactive cosr for 2010-10-10 should be 1 (%d!=1)' % ( r.cost_inactive, ))
28
        # test name property, should be C1R1024D10
29
        f_name = flavor.name
70 30

  
71
        # 2011-11-11, active should be 10, inactive 5
72
        ex_date = date(year=2011, month=11, day=11)
73
        r = flavor.find_cost(ex_date)
74
        self.assertEquals(r.cost_active, 10, 'Active cost for 2011-11-11 should be 10 (%d!=10)' % ( r.cost_active, ))
75
        self.assertEquals(r.cost_inactive, 5, 'Inactive cost for 2011-11-11 should be 5 (%d!=5)' % ( r.cost_inactive, ))
31
        self.assertEqual(f_name, 'C1R1024D10', 'flavor.name is not generated correctly, C1R1024D10! (%s)' % ( f_name, ))
76 32

  
33
    def test_flavor_get_costs(self):
34
        flavor = Flavor.objects.get(pk=30000)
77 35

  
78
class DebitTestCase(TestCase):
79
    fixtures = [ 'db_test_data' ]
80
                
81
    def test_accounting_log(self):
82
        """Test the Accounting Log unit method"""
83
        vm = VirtualMachine.objects.get(pk=30000)
84
        
85
        # get all entries, should be 2
86
        entries = Debit.get_log_entries(vm, datetime.datetime(year=2009, month=01, day=01))
87
        self.assertEquals(len(entries), 2, 'Log entries should be 2 (%d!=2)' % ( len(entries), ))
88 36
        
89
        # get enrties only for 2011, should be 1
90
        entries = Debit.get_log_entries(vm, datetime.datetime(year=2011, month=01, day=01))
91
        self.assertEquals(len(entries), 1, 'Log entries should be 1 (%d!=1)' % ( len(entries), ))
92 37

  
93 38

  
94
class VirtualMachineTestCase(TestCase):
39
class SynnefoUserTestCase(TestCase):
95 40
    fixtures = [ 'db_test_data' ]
96
    
97
    def test_virtual_machine(self):
98
        """Virtual Machine (model) unit test method"""
99
        vm = VirtualMachine.objects.get(pk=30002)
100
        
101
        # should be three
102
        acc_logs = vm.get_accounting_logs()
103
        
104
        self.assertEquals(len(acc_logs), 3, 'Log Entries should be 3 (%d!=3)' % ( len(acc_logs), ))
105 41

  
42
    def test_synnefo_user(self):
43
        """Test a SynnefoUser object"""
44
        s_user = SynnefoUser.objects.get(pk=30000)
45
        v_machine = VirtualMachine.objects.get(pk=30000)
106 46

  
47
        # charge the user
48
        s_user.debit_account(10, v_machine, "This should be a structured debit message!")
107 49

  
108
class ChargerTestCase(TestCase):
109
    fixtures = [ 'db_test_data' ]
50
        # should have only one debit object
51
        d_list = Debit.objects.all()
110 52

  
111
    def test_charger(self):
112
        """Charger unit test method"""
113
        
114
        # user with pk=1 should have 100 credits
115
        user = SynnefoUser.objects.get(pk=1)
116
        user.credit = 100
117
        user.save()
118
        
119
        # charge when the vm is running
120
        charger.charge()
121
        
122
        user = SynnefoUser.objects.get(pk=1)
123
        
124
        self.assertEquals(user.credit, 90, 'Error in charging process (%d!=90, running)' % ( user.credit, ))
125
        
126
        # charge when the vm is stopped
127
        vm = VirtualMachine.objects.get(pk=1)
128
        vm.charged = datetime.datetime.now() - datetime.timedelta(hours=1)
129
        vm.save()
130
        
131
        charger.charge()
132
        
133
        user = SynnefoUser.objects.get(pk=1)
134
        self.assertEquals(user.credit, 85, 'Error in charging process (%d!=85, stopped)' % ( user.credit, ))
135
        
136
        # try charge until the user spends all his credits, see if the charger
137
        vm = VirtualMachine.objects.get(pk=1)
138
        vm.charged = datetime.datetime.now() - datetime.timedelta(hours=1)
139
        vm.save()
140
        
141
        # the user now has 85, charge until his credits drop to zero
142
        for i in range(1, 10):
143
            vm = VirtualMachine.objects.get(pk=1)
144
            vm.charged = datetime.datetime.now() - datetime.timedelta(hours=1)
145
            vm.save()
146
            charger.charge()
147
        
148
        user = SynnefoUser.objects.get(pk=1)
149
        self.assertEquals(user.credit, 0, 'Error in charging process (%d!=0, running)' % ( user.credit, ))
150
        
53
        self.assertEqual(len(d_list), 1, 'SynnefoUser.debit_account() writes more than one debit entries!')
54

  
55
        # retrieve the user, now he/she should have zero credits
56
        s_user = SynnefoUser.objects.get(pk=30000)
57

  
58
        self.assertEqual(0, s_user.credit, 'SynnefoUser (pk=30000) should have zero credits (%d)' % ( s_user.credit, ))

Also available in: Unified diff