Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / commands.py @ 5d805533

History | View | Annotate | Download (6.8 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above copyright
11
#     notice, this list of conditions and the following disclaimer in the
12
#     documentation and/or other materials provided with the distribution.
13
#
14
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
# SUCH DAMAGE.
25
#
26
# The views and conclusions contained in the software and documentation are
27
# those of the authors and should not be interpreted as representing official
28
# policies, either expressed or implied, of GRNET S.A.
29

    
30
import logging
31

    
32
from functools import wraps
33
from django.db import transaction
34

    
35
from django.conf import settings
36
from snf_django.lib.api import faults
37
from synnefo import quotas
38
from synnefo.db.models import VirtualMachine
39

    
40

    
41
log = logging.getLogger(__name__)
42

    
43

    
44
def validate_server_action(vm, action):
45
    if vm.deleted:
46
        raise faults.BadRequest("Server '%s' has been deleted." % vm.id)
47

    
48
    # Destroyin a server should always be permitted
49
    if action == "DESTROY":
50
        return
51

    
52
    # Check that there is no pending action
53
    pending_action = vm.task
54
    if pending_action:
55
        if pending_action == "BUILD":
56
            raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
57
        raise faults.BadRequest("Cannot perform '%s' action while there is a"
58
                                " pending '%s'." % (action, pending_action))
59

    
60
    # Check if action can be performed to VM's operstate
61
    operstate = vm.operstate
62
    if operstate == "ERROR":
63
        raise faults.BadRequest("Cannot perform '%s' action while server is"
64
                                " in 'ERROR' state." % action)
65
    elif operstate == "BUILD" and action != "BUILD":
66
        raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
67
    elif (action == "START" and operstate != "STOPPED") or\
68
         (action == "STOP" and operstate != "STARTED") or\
69
         (action == "RESIZE" and operstate != "STOPPED") or\
70
         (action in ["CONNECT", "DISCONNECT"]
71
          and operstate != "STOPPED"
72
          and not settings.GANETI_USE_HOTPLUG) or \
73
         (action in ["ATTACH_VOLUME", "DETACH_VOLUME"]
74
          and operstate != "STOPPED"
75
          and not settings.GANETI_USE_HOTPLUG):
76
        raise faults.BadRequest("Cannot perform '%s' action while server is"
77
                                " in '%s' state." % (action, operstate))
78
    return
79

    
80

    
81
def server_command(action, action_fields=None):
82
    """Handle execution of a server action.
83

84
    Helper function to validate and execute a server action, handle quota
85
    commission and update the 'task' of the VM in the DB.
86

87
    1) Check if action can be performed. If it can, then there must be no
88
       pending task (with the exception of DESTROY).
89
    2) Handle previous commission if unresolved:
90
       * If it is not pending and it to accept, then accept
91
       * If it is not pending and to reject or is pending then reject it. Since
92
       the action can be performed only if there is no pending task, then there
93
       can be no pending commission. The exception is DESTROY, but in this case
94
       the commission can safely be rejected, and the dispatcher will generate
95
       the correct ones!
96
    3) Issue new commission and associate it with the VM. Also clear the task.
97
    4) Send job to ganeti
98
    5) Update task and commit
99
    """
100
    def decorator(func):
101
        @wraps(func)
102
        @transaction.commit_on_success
103
        def wrapper(vm, *args, **kwargs):
104
            user_id = vm.userid
105
            validate_server_action(vm, action)
106
            vm.action = action
107

    
108
            commission_name = "client: api, resource: %s" % vm
109
            quotas.handle_resource_commission(vm, action=action,
110
                                              action_fields=action_fields,
111
                                              commission_name=commission_name)
112
            vm.save()
113

    
114
            # XXX: Special case for server creation!
115
            if action == "BUILD":
116
                # Perform a commit, because the VirtualMachine must be saved to
117
                # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti.
118
                # Otherwise, messages will arrive from snf-dispatcher about
119
                # this instance, before the VM is stored in DB.
120
                transaction.commit()
121
                # After committing the locks are released. Refetch the instance
122
                # to guarantee x-lock.
123
                vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
124

    
125
            # Send the job to Ganeti and get the associated jobID
126
            try:
127
                job_id = func(vm, *args, **kwargs)
128
            except Exception as e:
129
                if vm.serial is not None:
130
                    # Since the job never reached Ganeti, reject the commission
131
                    log.debug("Rejecting commission: '%s', could not perform"
132
                              " action '%s': %s" % (vm.serial,  action, e))
133
                    transaction.rollback()
134
                    quotas.reject_resource_serial(vm)
135
                    transaction.commit()
136
                raise
137

    
138
            if action == "BUILD" and vm.serial is not None:
139
                # XXX: Special case for server creation: we must accept the
140
                # commission because the VM has been stored in DB. Also, if
141
                # communication with Ganeti fails, the job will never reach
142
                # Ganeti, and the commission will never be resolved.
143
                quotas.accept_resource_serial(vm)
144

    
145
            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
146
                     user_id, vm.id, action, job_id, vm.serial)
147

    
148
            # store the new task in the VM
149
            if job_id is not None:
150
                vm.task = action
151
                vm.task_job_id = job_id
152
            vm.save()
153

    
154
            return vm
155
        return wrapper
156
    return decorator