Revision 1af851fd

b/snf-cyclades-app/synnefo/api/actions.py
41 41
from django.utils import simplejson as json
42 42

  
43 43
from snf_django.lib.api import faults
44
from synnefo.api import util
44 45
from synnefo.api.util import (random_password, get_vm, get_nic_from_index,
45 46
                              get_network_free_address)
46 47
from synnefo.db.models import NetworkInterface
......
168 169
    #                       serverCapacityUnavailable (503),
169 170
    #                       overLimit (413),
170 171
    #                       resizeNotAllowed (403)
172
    log.debug("resize %s: %s", vm, args)
173
    flavorRef = args.get("flavorRef", None)
174
    if flavorRef is None:
175
        raise faults.BadRequest("Missing 'flavorRef' attribute")
176

  
177
    new_flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
178
    old_flavor = vm.flavor
179
    # User requested the same flavor
180
    if old_flavor.id == new_flavor.id:
181
        return HttpResponse(status=200)
182
    # Check that resize can be performed
183
    if old_flavor.disk != new_flavor.disk:
184
        raise faults.BadRequest("Can not resize instance disk")
185
    if old_flavor.disk_template != new_flavor.disk_template:
186
        raise faults.BadRequest("Can not change instance disk template")
187

  
188
    if not vm_is_stopped(vm):
189
        raise faults.BadRequest("Can not resize running instance")
190

  
191
    vcpus, memory = new_flavor.cpu, new_flavor.ram
192
    jobId = backend.resize_instance(vm, vcpus=vcpus, memory=memory)
193

  
194
    log.info("User '%s' resized VM from flavor '%s' to '%s', job %s",
195
              request.user_uniq, old_flavor, new_flavor, jobId)
196

  
197
    # Save operstate now, since you don't want any other action when an
198
    # an instance is resizing
199
    vm.operstate = "RESIZE"
200
    util.start_action(vm, "RESIZE", jobId)
201

  
202
    vm.save()
203
    return HttpResponse(status=202)
171 204

  
172
    raise faults.NotImplemented('Resize not supported.')
205

  
206
def vm_is_stopped(vm):
207
    """Check if a VirtualMachine is currently stopped.
208

  
209
    A server is stopped if it's operstate is 'STOPPED'. Also, you must check
210
    that no other job is currently running, because this job may start the
211
    instance
212
    """
213
    return vm.operstate == "STOPPED" and vm.backendjobstatus == "success"
173 214

  
174 215

  
175 216
@server_action('confirmResize')
b/snf-cyclades-app/synnefo/api/test/servers.py
443 443
        self.assertBadRequest(response)
444 444
        self.assertFalse(mrapi.mock_calls)
445 445

  
446
    def test_resize_vm(self, mrapi, mimage):
447
        flavor = mfactory.FlavorFactory(cpu=1, ram=1024)
448
        # Check building VM
449
        vm = self.get_vm(flavor=flavor, operstate="BUILD")
450
        request = {'resize': {'flavorRef': flavor.id}}
451
        response = self.post('/api/v1.1/servers/%d/action' % vm.id,
452
                             vm.userid, json.dumps(request), 'json')
453
        self.assertFault(response, 409, "buildInProgress")
454
        # Check same Flavor
455
        vm = self.get_vm(flavor=flavor, operstate="STOPPED")
456
        request = {'resize': {'flavorRef': flavor.id}}
457
        response = self.post('/api/v1.1/servers/%d/action' % vm.id,
458
                             vm.userid, json.dumps(request), 'json')
459
        self.assertSuccess(response)
460
        # Check flavor with different disk
461
        flavor2 = mfactory.FlavorFactory(disk=1024)
462
        flavor3 = mfactory.FlavorFactory(disk=2048)
463
        vm = self.get_vm(flavor=flavor2, operstate="STOPPED")
464
        request = {'resize': {'flavorRef': flavor3.id}}
465
        response = self.post('/api/v1.1/servers/%d/action' % vm.id,
466
                             vm.userid, json.dumps(request), 'json')
467
        self.assertBadRequest(response)
468
        flavor2 = mfactory.FlavorFactory(disk_template="foo")
469
        flavor3 = mfactory.FlavorFactory(disk_template="baz")
470
        vm = self.get_vm(flavor=flavor2, operstate="STOPPED")
471
        request = {'resize': {'flavorRef': flavor3.id}}
472
        response = self.post('/api/v1.1/servers/%d/action' % vm.id,
473
                             vm.userid, json.dumps(request), 'json')
474
        self.assertBadRequest(response)
475
        # Check success
476
        vm = self.get_vm(flavor=flavor, operstate="STOPPED")
477
        flavor4 = mfactory.FlavorFactory(disk_template=flavor.disk_template,
478
                                         disk=flavor.disk,
479
                                         cpu=4, ram=2048)
480
        request = {'resize': {'flavorRef': flavor4.id}}
481
        mrapi().ModifyInstance.return_value = 42
482
        response = self.post('/api/v1.1/servers/%d/action' % vm.id,
483
                             vm.userid, json.dumps(request), 'json')
484
        self.assertEqual(response.status_code, 202)
485
        vm = VirtualMachine.objects.get(id=vm.id)
486
        self.assertEqual(vm.backendjobid, 42)
487
        name, args, kwargs = mrapi().ModifyInstance.mock_calls[0]
488
        self.assertEqual(kwargs["beparams"]["vcpus"], 4)
489
        self.assertEqual(kwargs["beparams"]["minmem"], 2048)
490
        self.assertEqual(kwargs["beparams"]["maxmem"], 2048)
491

  
492
    def get_vm(self, flavor, operstate):
493
        vm = mfactory.VirtualMachineFactory(flavor=flavor)
494
        vm.operstate = operstate
495
        vm.backendjobstatus = "success"
496
        vm.save()
497
        return vm
498

  
446 499

  
447 500
class ServerVNCConsole(ComputeAPITest):
448 501
    def test_not_active_server(self):
b/snf-cyclades-app/synnefo/db/models.py
240 240
        ('STOP', 'Shutdown VM'),
241 241
        ('SUSPEND', 'Admin Suspend VM'),
242 242
        ('REBOOT', 'Reboot VM'),
243
        ('DESTROY', 'Destroy VM')
243
        ('DESTROY', 'Destroy VM'),
244
        ('RESIZE', 'Resize a VM'),
244 245
    )
245 246

  
246 247
    # The internal operating state of a VM
......
249 250
        ('ERROR', 'Creation failed'),
250 251
        ('STOPPED', 'Stopped'),
251 252
        ('STARTED', 'Started'),
252
        ('DESTROYED', 'Destroyed')
253
        ('DESTROYED', 'Destroyed'),
254
        ('RESIZE', 'Resizing')
253 255
    )
254 256

  
255 257
    # The list of possible operations on the backend
......
304 306
        "ERROR": "ERROR",
305 307
        "STOPPED": "STOPPED",
306 308
        "STARTED": "ACTIVE",
307
        "DESTROYED": "DELETED"
309
        "DESTROYED": "DELETED",
310
        "RESIZE": "RESIZE"
308 311
    }
309 312

  
310 313
    name = models.CharField('Virtual Machine Name', max_length=255)

Also available in: Unified diff