Revision 22f54174

b/snf-cyclades-app/synnefo/logic/backend.py
89 89
        # failed server
90 90
        serial = vm.serial
91 91
        if job_status == rapi.JOB_STATUS_SUCCESS:
92
            quotas.accept_serial(serial)
92
            quotas.accept_resource_serial(vm)
93 93
        elif job_status in [rapi.JOB_STATUS_ERROR, rapi.JOB_STATUS_CANCELED]:
94 94
            log.debug("Job %s failed. Rejecting related serial %s", job_id,
95 95
                      serial)
96
            quotas.reject_serial(serial)
97
        vm.serial = None
96
            quotas.reject_resource_serial(vm)
98 97
    elif job_status == rapi.JOB_STATUS_SUCCESS:
99 98
        commission_info = quotas.get_commission_info(resource=vm,
100 99
                                                     action=action,
......
103 102
            # Commission for this change has not been issued, or the issued
104 103
            # commission was unaware of the current change. Reject all previous
105 104
            # commissions and create a new one in forced mode!
106
            log.debug("Expected job was %s. Processing job %s.",
107
                      vm.task_job_id, job_id)
105
            log.debug("Expected job was %s. Processing job %s. "
106
                      "Attached serial %s",
107
                      vm.task_job_id, job_id, vm.serial)
108 108
            reason = ("client: dispatcher, resource: %s, ganeti_job: %s"
109 109
                      % (vm, job_id))
110
            quotas.handle_resource_commission(vm, action,
111
                                              action_fields=job_fields,
112
                                              commission_name=reason,
113
                                              force=True,
114
                                              auto_accept=True)
115
            log.debug("Issued new commission: %s", vm.serial)
116
            # NOTE: Since we rejected the serial that was associated with the
117
            # 'vm.task_job_id' job, we must also clear the 'vm.serial' field.
118
            # If not, there will be no new commission for the 'vm.task_job_id'
119
            # job!
120
            vm.serial = None
121

  
110
            serial = quotas.handle_resource_commission(
111
                vm, action,
112
                action_fields=job_fields,
113
                commission_name=reason,
114
                force=True,
115
                auto_accept=True)
116
            log.debug("Issued new commission: %s", serial)
122 117
    return vm
123 118

  
124 119

  
b/snf-cyclades-app/synnefo/logic/servers.py
138 138
                    log.debug("Rejecting commission: '%s', could not perform"
139 139
                              " action '%s': %s" % (vm.serial,  action, e))
140 140
                    transaction.rollback()
141
                    quotas.reject_serial(vm.serial)
141
                    quotas.reject_resource_serial(vm)
142 142
                    transaction.commit()
143 143
                raise
144 144

  
......
147 147
                # commission because the VM has been stored in DB. Also, if
148 148
                # communication with Ganeti fails, the job will never reach
149 149
                # Ganeti, and the commission will never be resolved.
150
                quotas.accept_serial(vm.serial)
150
                quotas.accept_resource_serial(vm)
151 151

  
152 152
            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
153 153
                     user_id, vm.id, action, job_id, vm.serial)
b/snf-cyclades-app/synnefo/logic/tests/servers.py
197 197
        vm.task_job_id = None
198 198
        vm.save()
199 199
        with mocked_quotaholder():
200
            quotas.accept_serial(vm.serial)
200
            quotas.accept_resource_serial(vm)
201 201
        mrapi().RebootInstance.return_value = 1
202 202
        with mocked_quotaholder():
203 203
            servers.reboot(vm, "HARD")
b/snf-cyclades-app/synnefo/quotas/__init__.py
43 43
log = logging.getLogger(__name__)
44 44

  
45 45

  
46
QUOTABLE_RESOURCES = [VirtualMachine, Network, IPAddress]
47

  
48

  
46 49
DEFAULT_SOURCE = 'system'
47 50
RESOURCES = [
48 51
    "cyclades.vm",
......
115 118
                                             force=force,
116 119
                                             auto_accept=auto_accept)
117 120

  
118
    if serial:
119
        serial_info = {"serial": serial}
120
        if auto_accept:
121
            serial_info["pending"] = False
122
            serial_info["accept"] = True
123
            serial_info["resolved"] = True
124
        return QuotaHolderSerial.objects.create(**serial_info)
125
    else:
121
    if not serial:
126 122
        raise Exception("No serial")
127 123

  
124
    serial = QuotaHolderSerial.objects.create(serial=serial)
125
    if auto_accept:
126
        serial.pending = False
127
        serial.accept = True
128
        serial.resolved = True
129
        serial.save()
130
    else:
131
        resource.serial = serial
132
        resource.save()
133
    return serial
134

  
128 135

  
129
def accept_serial(serial, strict=True):
130
    assert serial.pending or serial.accept
131
    response = resolve_commissions(accept=[serial.serial], strict=strict)
136
def mark_accept_serial(serial):
132 137
    serial.pending = False
133 138
    serial.accept = True
134
    serial.resolved = True
135 139
    serial.save()
136
    return response
137 140

  
138 141

  
139
def reject_serial(serial, strict=True):
140
    assert serial.pending or not serial.accept
141
    response = resolve_commissions(reject=[serial.serial], strict=strict)
142
    serial.pending = False
143
    serial.accept = False
144
    serial.resolved = True
145
    serial.save()
146
    return response
142
def accept_resource_serial(resource, strict=True):
143
    serial = resource.serial
144
    assert serial.pending or serial.accept, "%s can't be accepted" % serial
145
    response = _resolve_commissions(accept=[serial.serial], strict=strict)
146
    if serial.serial in response.get("accepted", []):
147
        resource.serial = None
148
        resource.save()
147 149

  
148 150

  
149
def resolve_commissions(accept=None, reject=None, strict=True):
151
def reject_resource_serial(resource, strict=True):
152
    serial = resource.serial
153
    assert serial.pending or not serial.accept, "%s can't be rejected" % serial
154
    response = _resolve_commissions(reject=[serial.serial], strict=strict)
155
    if serial.serial in response.get("rejected", []):
156
        resource.serial = None
157
        resource.save()
158

  
159

  
160
def _resolve_commissions(accept=None, reject=None, strict=True):
150 161
    if accept is None:
151 162
        accept = []
152 163
    if reject is None:
......
156 167
    with AstakosClientExceptionHandler():
157 168
        response = qh.resolve_commissions(accept, reject)
158 169

  
159
    # Update correspodning entries in DB
160
    QuotaHolderSerial.objects.filter(serial__in=accept).update(accept=True,
161
                                                               pending=False,
162
                                                               resolved=True)
163
    QuotaHolderSerial.objects.filter(serial__in=reject).update(accept=False,
164
                                                               pending=False,
165
                                                               resolved=True)
170
    accepted = response.get("accepted", [])
171
    rejected = response.get("rejected", [])
172

  
173
    if accepted:
174
        QuotaHolderSerial.objects.filter(serial__in=accepted).update(
175
            accept=True, pending=False, resolved=True)
176
    if rejected:
177
        QuotaHolderSerial.objects.filter(serial__in=rejected).update(
178
            accept=False, pending=False, resolved=True)
166 179

  
167 180
    if strict:
168 181
        failed = response["failed"]
......
173 186
    return response
174 187

  
175 188

  
189
def reconcile_resolve_commissions(accept=None, reject=None, strict=True):
190
    response = _resolve_commissions(accept=accept,
191
                                    reject=reject,
192
                                    strict=strict)
193
    affected = response.get("accepted", []) + response.get("rejected", [])
194
    for resource in QUOTABLE_RESOURCES:
195
        resource.objects.filter(serial__in=affected).update(serial=None)
196

  
197

  
176 198
def resolve_pending_commissions():
177 199
    """Resolve quotaholder pending commissions.
178 200

  
......
249 271
    """
250 272
    commission_reason = ("client: api, resource: %s, action: %s"
251 273
                         % (resource, action))
252
    serial = handle_resource_commission(resource=resource, action=action,
253
                                        action_fields=action_fields,
254
                                        commission_name=commission_reason)
274
    handle_resource_commission(resource=resource, action=action,
275
                               action_fields=action_fields,
276
                               commission_name=commission_reason)
255 277

  
256 278
    # Mark the serial as one to accept and associate it with the resource
257
    serial.pending = False
258
    serial.accept = True
259
    serial.save()
279
    mark_accept_serial(resource.serial)
260 280
    transaction.commit()
261 281

  
262 282
    try:
263 283
        # Accept the commission to quotaholder
264
        accept_serial(serial)
284
        accept_resource_serial(resource)
265 285
    except:
266 286
        # Do not crash if we can not accept commission to Quotaholder. Quotas
267 287
        # have already been reserved and the resource already exists in DB.
268 288
        # Just log the error
269
        log.exception("Failed to accept commission: %s", serial)
270

  
271
    return serial
289
        log.exception("Failed to accept commission: %s", resource.serial)
272 290

  
273 291

  
274 292
def get_commission_info(resource, action, action_fields=None):
......
348 366
    # The one who succeeds will be finally accepted, and all other will be
349 367
    # rejected
350 368
    force = force or (action == "DESTROY")
351
    resolve_commission(resource.serial, force=force)
369
    resolve_resource_commission(resource, force=force)
352 370

  
353 371
    serial = issue_commission(resource, action, name=commission_name,
354 372
                              force=force, auto_accept=auto_accept,
355 373
                              action_fields=action_fields)
356
    resource.serial = serial
357
    resource.save()
358 374
    return serial
359 375

  
360 376

  
......
362 378
    pass
363 379

  
364 380

  
365
def resolve_commission(serial, force=False):
381
def resolve_resource_commission(resource, force=False):
382
    serial = resource.serial
366 383
    if serial is None or serial.resolved:
367 384
        return
368 385
    if serial.pending and not force:
......
370 387
        raise ResolveError(m)
371 388
    log.warning("Resolving pending commission: %s", serial)
372 389
    if not serial.pending and serial.accept:
373
        accept_serial(serial)
390
        accept_resource_serial(resource)
374 391
    else:
375
        reject_serial(serial)
392
        reject_resource_serial(resource)
b/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-commissions-cyclades.py
63 63

  
64 64
        if fix and (accepted or rejected):
65 65
            self.stdout.write("Fixing pending commissions..\n")
66
            quotas.resolve_commissions(accept=accepted, reject=rejected,
67
                                       strict=False)
66
            quotas.reconcile_resolve_commissions(accept=accepted,
67
                                                 reject=rejected,
68
                                                 strict=False)
68 69

  
69 70

  
70 71
def list_to_string(l):
b/snf-django-lib/snf_django/utils/testing.py
204 204
            return (len(astakos.return_value.issue_one_commission.mock_calls) +
205 205
                    serial)
206 206
        astakos.return_value.issue_one_commission.side_effect = foo
207
        astakos.return_value.resolve_commissions.return_value = {"failed": []}
207
        def resolve_mock(*args, **kwargs):
208
            return {"failed": [],
209
                    "accepted": args[0],
210
                    "rejected": args[1],
211
                    }
212
        astakos.return_value.resolve_commissions.side_effect = resolve_mock
208 213
        yield astakos.return_value
209 214

  
210 215

  

Also available in: Unified diff