Revision 529178b1

b/api/actions.py
9 9
from django.template.loader import render_to_string
10 10
from django.utils import simplejson as json
11 11

  
12
from synnefo.api.faults import BadRequest, ResizeNotAllowed, ServiceUnavailable
12
from synnefo.api.faults import BadRequest, ServiceUnavailable
13 13
from synnefo.api.util import random_password
14
from synnefo.util.rapi import GanetiRapiClient
15 14
from synnefo.util.vapclient import request_forwarding as request_vnc_forwarding
16
from synnefo.logic import backend
15
from synnefo.logic.backend import (reboot_instance, startup_instance, shutdown_instance,
16
                                    get_instance_console)
17 17
from synnefo.logic.utils import get_rsapi_state
18 18

  
19 19

  
20 20
server_actions = {}
21 21

  
22
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
23

  
24 22

  
25 23
def server_action(name):
26 24
    '''Decorator for functions implementing server actions.
27 25
    
28
       `name` is the key in the dict passed by the client.
26
    `name` is the key in the dict passed by the client.
29 27
    '''
30 28
    
31 29
    def decorator(func):
......
33 31
        return func
34 32
    return decorator
35 33

  
36
@server_action('console')
37
def get_console(request, vm, args):
38
    """Arrange for an OOB console of the specified type
39

  
40
    This method arranges for an OOB console of the specified type.
41
    Only consoles of type "vnc" are supported for now.
42
    
43
    It uses a running instance of vncauthproxy to setup proper
44
    VNC forwarding with a random password, then returns the necessary
45
    VNC connection info to the caller.
46

  
47
    JSON Request: {
48
        "console": {
49
            "type": "vnc"
50
        }
51
    }
52

  
53
    JSON Reply: {
54
        "vnc": {
55
            "host": "fqdn_here",
56
            "port": a_port_here,
57
            "password": "a_password_here"
58
        }
59
    }
60

  
61
    """
62
    # Normal Response Code: 200
63
    # Error Response Codes: computeFault (400, 500),
64
    #                       serviceUnavailable (503),
65
    #                       unauthorized (401),
66
    #                       badRequest (400),
67
    #                       badMediaType(415),
68
    #                       itemNotFound (404),
69
    #                       buildInProgress (409),
70
    #                       overLimit (413)
71
    try:
72
        console_type = args.get('type', '')
73
        if console_type != 'vnc':
74
            raise BadRequest(message="type can only be 'vnc'")
75
    except KeyError:
76
        raise BadRequest()
77

  
78
    # Use RAPI to get VNC console information for this instance
79
    if get_rsapi_state(vm) != 'ACTIVE':
80
        raise BadRequest(message="Server not in ACTIVE state")
81
    if settings.TEST:
82
        console_data = { 'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000 }
83
    else:
84
        console_data = rapi.GetInstanceConsole(vm.backend_id)
85
    if console_data['kind'] != 'vnc':
86
        raise ServiceUnavailable()
87

  
88
    # Let vncauthproxy decide on the source port.
89
    # The alternative: static allocation, e.g.
90
    # sport = console_data['port'] - 1000
91
    sport = 0
92
    daddr = console_data['host']
93
    dport = console_data['port']
94
    passwd = random_password()
95

  
96
    try:
97
        if settings.TEST:
98
            fwd = { 'source_port': 1234, 'status': 'OK' }
99
        else:
100
            fwd = request_vnc_forwarding(sport, daddr, dport, passwd)
101
        if fwd['status'] != "OK":
102
            raise ServiceUnavailable()
103
        vnc = { 'host': getfqdn(), 'port': fwd['source_port'], 'password': passwd }
104
    except Exception:
105
        raise ServiceUnavailable("Could not allocate VNC console port")
106

  
107
    # Format to be reviewed by [verigak], FIXME
108
    if request.serialization == 'xml':
109
        mimetype = 'application/xml'
110
        data = render_to_string('vnc.xml', {'vnc': vnc})
111
    else:
112
        mimetype = 'application/json'
113
        data = json.dumps({'vnc': vnc})
114

  
115
    return HttpResponse(data, mimetype=mimetype, status=200)
116

  
117 34

  
118 35
@server_action('changePassword')
119 36
def change_password(request, vm, args):
......
128 45
    #                       overLimit (413)
129 46
    
130 47
    try:
131
        adminPass = args['adminPass']
48
        password = args['adminPass']
132 49
    except KeyError:
133
        raise BadRequest()
50
        raise BadRequest('Malformed request.')
134 51

  
135
    raise ServiceUnavailable()
52
    raise ServiceUnavailable('Changing password is not supported.')
136 53

  
137 54
@server_action('reboot')
138 55
def reboot(request, vm, args):
......
148 65
    
149 66
    reboot_type = args.get('type', '')
150 67
    if reboot_type not in ('SOFT', 'HARD'):
151
        raise BadRequest()
152
    
153
    backend.start_action(vm, 'REBOOT')
154
    rapi.RebootInstance(vm.backend_id, reboot_type.lower())
68
        raise BadRequest('Malformed Request.')
69
    reboot_instance(vm, reboot_type.lower())
155 70
    return HttpResponse(status=202)
156 71

  
157 72
@server_action('start')
......
159 74
    # Normal Response Code: 202
160 75
    # Error Response Codes: serviceUnavailable (503),
161 76
    #                       itemNotFound (404)
162

  
163
    backend.start_action(vm, 'START')
164
    rapi.StartupInstance(vm.backend_id)
77
    
78
    if args:
79
        raise BadRequest('Malformed Request.')
80
    startup_instance(vm)
165 81
    return HttpResponse(status=202)
166 82

  
167 83
@server_action('shutdown')
......
170 86
    # Error Response Codes: serviceUnavailable (503),
171 87
    #                       itemNotFound (404)
172 88
    
173
    backend.start_action(vm, 'STOP')
174
    rapi.ShutdownInstance(vm.backend_id)
89
    if args:
90
        raise BadRequest('Malformed Request.')
91
    shutdown_instance(vm)
175 92
    return HttpResponse(status=202)
176 93

  
177 94
@server_action('rebuild')
......
187 104
    #                       serverCapacityUnavailable (503),
188 105
    #                       overLimit (413)
189 106

  
190
    raise ServiceUnavailable()
107
    raise ServiceUnavailable('Rebuild not supported.')
191 108

  
192 109
@server_action('resize')
193 110
def resize(request, vm, args):
......
203 120
    #                       overLimit (413),
204 121
    #                       resizeNotAllowed (403)
205 122
    
206
    raise ResizeNotAllowed()
123
    raise ServiceUnavailable('Resize not supported.')
207 124

  
208 125
@server_action('confirmResize')
209 126
def confirm_resize(request, vm, args):
......
219 136
    #                       overLimit (413),
220 137
    #                       resizeNotAllowed (403)
221 138
    
222
    raise ResizeNotAllowed()
139
    raise ServiceUnavailable('Resize not supported.')
223 140

  
224 141
@server_action('revertResize')
225 142
def revert_resize(request, vm, args):
......
235 152
    #                       overLimit (413),
236 153
    #                       resizeNotAllowed (403)
237 154

  
238
    raise ResizeNotAllowed()
155
    raise ServiceUnavailable('Resize not supported.')
156

  
157
@server_action('console')
158
def get_console(request, vm, args):
159
    """Arrange for an OOB console of the specified type
160

  
161
    This method arranges for an OOB console of the specified type.
162
    Only consoles of type "vnc" are supported for now.
163

  
164
    It uses a running instance of vncauthproxy to setup proper
165
    VNC forwarding with a random password, then returns the necessary
166
    VNC connection info to the caller.
167
    """
168
    # Normal Response Code: 200
169
    # Error Response Codes: computeFault (400, 500),
170
    #                       serviceUnavailable (503),
171
    #                       unauthorized (401),
172
    #                       badRequest (400),
173
    #                       badMediaType(415),
174
    #                       itemNotFound (404),
175
    #                       buildInProgress (409),
176
    #                       overLimit (413)
177
    
178
    console_type = args.get('type', '')
179
    if console_type != 'vnc':
180
        raise BadRequest('Type can only be "vnc".')
181

  
182
    # Use RAPI to get VNC console information for this instance
183
    if get_rsapi_state(vm) != 'ACTIVE':
184
        raise BadRequest('Server not in ACTIVE state.')
185
    
186
    if settings.TEST:
187
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
188
    else:
189
        console_data = get_instance_console(vm)
190
    
191
    if console_data['kind'] != 'vnc':
192
        raise ServiceUnavailable('Could not create a console of requested type.')
193
    
194
    # Let vncauthproxy decide on the source port.
195
    # The alternative: static allocation, e.g.
196
    # sport = console_data['port'] - 1000
197
    sport = 0
198
    daddr = console_data['host']
199
    dport = console_data['port']
200
    password = random_password()
201
    
202
    try:
203
        if settings.TEST:
204
            fwd = {'source_port': 1234, 'status': 'OK'}
205
        else:
206
            fwd = request_vnc_forwarding(sport, daddr, dport, password)
207
    except Exception:
208
        raise ServiceUnavailable('Could not allocate VNC console port.')
209

  
210
    if fwd['status'] != "OK":
211
        raise ServiceUnavailable('Could not allocate VNC console.')
212
    
213
    console = {
214
        'type': 'vnc',
215
        'host': getfqdn(),
216
        'port': fwd['source_port'],
217
        'password': password}
218
    
219
    if request.serialization == 'xml':
220
        mimetype = 'application/xml'
221
        data = render_to_string('console.xml', {'console': console})
222
    else:
223
        mimetype = 'application/json'
224
        data = json.dumps({'console': console})
225
    
226
    return HttpResponse(data, mimetype=mimetype, status=200)
/dev/null
1
[
2
    {
3
        "model": "db.Image",
4
        "pk": 1,
5
        "fields": {
6
            "name": "Debian Unstable",
7
            "created": "2011-02-06 00:00:00",
8
            "updated": "2011-02-06 00:00:00",
9
            "state": "ACTIVE"
10
        }
11
    },
12
    {
13
        "model": "db.Image",
14
        "pk": 2,
15
        "fields": {
16
            "name": "Red Hat Enterprise Linux",
17
            "created": "2011-02-06 00:00:00",
18
            "updated": "2011-02-06 00:00:00",
19
            "state": "ACTIVE"
20
        }
21
    },
22
    {
23
        "model": "db.Image",
24
        "pk": 3,
25
        "fields": {
26
            "name": "Ubuntu 10.10",
27
            "created": "2011-02-06 00:00:00",
28
            "updated": "2011-02-06 00:00:00",
29
            "state": "ACTIVE"
30
        }
31
    },
32
    
33
    {
34
        "model": "db.Flavor",
35
        "pk": 1,
36
        "fields": {
37
            "cpu": 1,
38
            "ram": 1024,
39
            "disk": 20 
40
        }
41
    },
42
    {
43
        "model": "db.Flavor",
44
        "pk": 2,
45
        "fields": {
46
            "cpu": 1,
47
            "ram": 1024,
48
            "disk": 40 
49
        }
50
    },
51
    {
52
        "model": "db.Flavor",
53
        "pk": 3,
54
        "fields": {
55
            "cpu": 1,
56
            "ram": 1024,
57
            "disk": 80 
58
        }
59
    }
60
]
b/api/flavors.py
7 7
from django.template.loader import render_to_string
8 8
from django.utils import simplejson as json
9 9

  
10
from synnefo.api.faults import ItemNotFound
11
from synnefo.api.util import get_user, get_request_dict, api_method
10
from synnefo.api.util import get_flavor, api_method
12 11
from synnefo.db.models import Flavor
13 12

  
14 13

  
......
56 55
    #                       badRequest (400),
57 56
    #                       itemNotFound (404),
58 57
    #                       overLimit (413)
59

  
60
    try:
61
        falvor_id = int(flavor_id)
62
        flavor = flavor_to_dict(Flavor.objects.get(id=flavor_id))
63
    except Flavor.DoesNotExist:
64
        raise ItemNotFound
58
    
59
    flavor = get_flavor(flavor_id)
60
    flavordict = flavor_to_dict(flavor, detail=True)
65 61
    
66 62
    if request.serialization == 'xml':
67
        data = render_to_string('flavor.xml', {'flavor': flavor})
63
        data = render_to_string('flavor.xml', {'flavor': flavordict})
68 64
    else:
69
        data = json.dumps({'flavor': flavor})
65
        data = json.dumps({'flavor': flavordict})
70 66
    
71 67
    return HttpResponse(data, status=200)
b/api/images.py
3 3
#
4 4

  
5 5
from synnefo.api.common import method_not_allowed
6
from synnefo.api.util import *
7
from synnefo.db.models import Image, ImageMetadata, VirtualMachine
6
from synnefo.api.faults import BadRequest, Unauthorized
7
from synnefo.api.util import (isoformat, isoparse, get_user, get_vm, get_image, get_image_meta,
8
                                get_request_dict, render_metadata, render_meta, api_method)
9
from synnefo.db.models import Image, ImageMetadata
8 10

  
9 11
from django.conf.urls.defaults import patterns
10 12
from django.http import HttpResponse
......
173 175
    
174 176
    image = get_image(image_id)
175 177
    if image.owner != get_user():
176
        raise Unauthorized()
178
        raise Unauthorized('Image does not belong to user.')
177 179
    image.delete()
178 180
    return HttpResponse(status=204)
179 181

  
b/api/servers.py
2 2
# Copyright (c) 2010 Greek Research and Technology Network
3 3
#
4 4

  
5
from django.conf import settings
5
import logging
6

  
6 7
from django.conf.urls.defaults import patterns
7 8
from django.http import HttpResponse
8 9
from django.template.loader import render_to_string
......
10 11

  
11 12
from synnefo.api.actions import server_actions
12 13
from synnefo.api.common import method_not_allowed
13
from synnefo.api.faults import BadRequest, ItemNotFound
14
from synnefo.api.util import *
15
from synnefo.db.models import Image, Flavor, VirtualMachine, VirtualMachineMetadata
14
from synnefo.api.faults import BadRequest, ItemNotFound, ServiceUnavailable
15
from synnefo.api.util import (isoformat, isoparse, random_password,
16
                                get_user, get_vm, get_vm_meta, get_image, get_flavor,
17
                                get_request_dict, render_metadata, render_meta, api_method)
18
from synnefo.db.models import VirtualMachine, VirtualMachineMetadata
19
from synnefo.logic.backend import create_instance, delete_instance
16 20
from synnefo.logic.utils import get_rsapi_state
17
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError
18
from synnefo.logic import backend
19

  
20
import logging
21

  
21
from synnefo.util.rapi import GanetiApiError
22 22

  
23
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
24 23

  
25 24
urlpatterns = patterns('synnefo.api.servers',
26 25
    (r'^(?:/|.json|.xml)?$', 'demux'),
......
153 152
        name = server['name']
154 153
        metadata = server.get('metadata', {})
155 154
        assert isinstance(metadata, dict)
156
        sourceimage = Image.objects.get(id=server['imageRef'])
157
        flavor = Flavor.objects.get(id=server['flavorRef'])
155
        image_id = server['imageRef']
156
        flavor_id = server['flavorRef']
158 157
    except (KeyError, AssertionError):
159 158
        raise BadRequest('Malformed request.')
160
    except Image.DoesNotExist:
161
        raise ItemNotFound
162
    except Flavor.DoesNotExist:
163
        raise ItemNotFound
164 159
    
165
    vm = VirtualMachine(
160
    image = get_image(image_id)
161
    flavor = get_flavor(flavor_id)
162
    
163
    # We must save the VM instance now, so that it gets a valid vm.backend_id.
164
    vm = VirtualMachine.objects.create(
166 165
        name=name,
167 166
        owner=get_user(),
168
        sourceimage=sourceimage,
167
        sourceimage=image,
169 168
        ipfour='0.0.0.0',
170 169
        ipsix='::1',
171 170
        flavor=flavor)
172

  
173
    # Pick a random password for the VM.
174
    # FIXME: This must be passed to the Ganeti OS provider via CreateInstance()
175
    passwd = random_password()
176

  
177
    # We *must* save the VM instance now,
178
    # so that it gets a vm.id and vm.backend_id is valid.
179
    vm.save() 
171
    
172
    password = random_password()
180 173
                
181 174
    try:
182
        jobId = rapi.CreateInstance(
183
            mode='create',
184
            name=vm.backend_id,
185
            disk_template='plain',
186
            disks=[{"size": 2000}],         #FIXME: Always ask for a 2GB disk for now
187
            nics=[{}],
188
            os='debootstrap+default',       #TODO: select OS from imageRef
189
            ip_check=False,
190
            name_check=False,
191
            pnode=rapi.GetNodes()[0],       #TODO: verify if this is necessary
192
            dry_run=settings.TEST,
193
            beparams=dict(auto_balance=True, vcpus=flavor.cpu, memory=flavor.ram))
175
        create_instance(vm, flavor, password)
194 176
    except GanetiApiError:
195 177
        vm.delete()
196 178
        raise ServiceUnavailable('Could not create server.')
......
198 180
    for key, val in metadata.items():
199 181
        VirtualMachineMetadata.objects.create(meta_key=key, meta_value=val, vm=vm)
200 182
    
201
    logging.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
183
    logging.info('created vm with %s cpus, %s ram and %s storage',
184
                    flavor.cpu, flavor.ram, flavor.disk)
202 185
    
203 186
    server = vm_to_dict(vm, detail=True)
204 187
    server['status'] = 'BUILD'
205
    server['adminPass'] = passwd
188
    server['adminPass'] = password
206 189
    return render_server(request, server, status=202)
207 190

  
208 191
@api_method('GET')
......
235 218
    
236 219
    try:
237 220
        name = req['server']['name']
238
    except KeyError:
221
    except (TypeError, KeyError):
239 222
        raise BadRequest('Malformed request.')
240 223
    
241 224
    vm = get_vm(server_id)
......
256 239
    #                       overLimit (413)
257 240
    
258 241
    vm = get_vm(server_id)
259
    backend.start_action(vm, 'DESTROY')
260
    rapi.DeleteInstance(vm.backend_id)
242
    delete_instance(vm)
261 243
    return HttpResponse(status=204)
262 244

  
263 245
@api_method('POST')
b/api/templates/console.xml
1
<?xml version="1.0" encoding="UTF-8"?>
2
<console xmlns="http://docs.openstack.org/compute/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" type="{{ console.type }}" host="{{ console.host }}" port="{{ console.port }}" password="{{ console.password }}">
3
</console>
/dev/null
1
<?xml version="1.0" encoding="UTF-8"?>
2
<vnc xmlns="http://docs.openstack.org/compute/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" host="{{ vnc.host }}" port="{{ vnc.port }}" password="{{ vnc.password }}">
3
</vnc>
b/api/tests.py
721 721
        response = self.client.post(path, data, content_type='application/json')
722 722
        self.assertEqual(response.status_code, 200)
723 723
        reply = json.loads(response.content)
724
        self.assertEqual(reply.keys(), ['vnc'])
725
        self.assertEqual(set(reply['vnc'].keys()), set(['host', 'port', 'password']))
726

  
724
        self.assertEqual(reply.keys(), ['console'])
725
        console = reply['console']
726
        self.assertEqual(console['type'], 'vnc')
727
        self.assertEqual(set(console.keys()), set(['type', 'host', 'port', 'password']))
b/api/util.py
10 10
from traceback import format_exc
11 11
from wsgiref.handlers import format_date_time
12 12

  
13
import datetime
14
import dateutil.parser
15
import logging
16

  
13 17
from django.conf import settings
14 18
from django.http import HttpResponse
15 19
from django.template.loader import render_to_string
16 20
from django.utils import simplejson as json
17 21

  
18
from synnefo.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable
19
from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata
20

  
21
import datetime
22
import dateutil.parser
23
import logging
22
from synnefo.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable, Unauthorized
23
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
24
                                VirtualMachine, VirtualMachineMetadata)
24 25

  
25 26

  
26 27
class UTC(tzinfo):
......
35 36

  
36 37

  
37 38
def isoformat(d):
38
    """Return an ISO8601 date string that includes a timezon."""
39
    """Return an ISO8601 date string that includes a timezone."""
39 40
    
40 41
    return d.replace(tzinfo=UTC()).isoformat()
41 42

  
......
70 71
    try:
71 72
        return SynnefoUser.objects.all()[0]
72 73
    except IndexError:
73
        raise Unauthorized
74
        raise Unauthorized('No users found.')
74 75

  
75 76
def get_vm(server_id):
76 77
    """Return a VirtualMachine instance or raise ItemNotFound."""
......
110 111
    except ImageMetadata.DoesNotExist:
111 112
        raise ItemNotFound('Metadata key not found.')
112 113

  
114
def get_flavor(flavor_id):
115
    """Return a Flavor instance or raise ItemNotFound."""
116
    
117
    try:
118
        flavor_id = int(flavor_id)
119
        return Flavor.objects.get(id=flavor_id)
120
    except Flavor.DoesNotExist:
121
        raise ItemNotFound('Flavor not found.')
113 122

  
114 123
def get_request_dict(request):
115 124
    """Returns data sent by the client as a python dict."""
......
168 177
def request_serialization(request, atom_allowed=False):
169 178
    """Return the serialization format requested.
170 179
       
171
       Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
180
    Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
172 181
    """
173 182
    
174 183
    path = request.path
......
209 218
            except Fault, fault:
210 219
                return render_fault(request, fault)
211 220
            except BaseException, e:
212
                logging.exception('Unexpected error: %s' % e)
213
                fault = ServiceUnavailable('Unexpected error')
221
                logging.exception('Unexpected error: %s', e)
222
                fault = ServiceUnavailable('Unexpected error.')
214 223
                return render_fault(request, fault)
215 224
        return wrapper
216 225
    return decorator
b/logic/backend.py
4 4
# Copyright 2010 Greek Research and Technology Network
5 5
#
6 6

  
7
from django.conf import settings
7 8
from synnefo.db.models import VirtualMachine
8 9
from synnefo.logic import utils
10
from synnefo.util.rapi import GanetiRapiClient
11

  
12

  
13
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
14

  
9 15

  
10 16
def process_backend_msg(vm, jobid, opcode, status, logmsg):
11 17
    """Process a job progress notification from the backend.
......
61 67
    elif action == "START":
62 68
        vm.suspended = False
63 69
    vm.save()
70

  
71
def create_instance(vm, flavor, password):
72
    # FIXME: `password` must be passed to the Ganeti OS provider via CreateInstance()
73
    return rapi.CreateInstance(
74
        mode='create',
75
        name='verigak-8',
76
        disk_template='plain',
77
        disks=[{"size": 2000}],         #FIXME: Always ask for a 2GB disk for now
78
        nics=[{}],
79
        os='debootstrap+default',       #TODO: select OS from imageRef
80
        ip_check=False,
81
        name_check=False,
82
        pnode=rapi.GetNodes()[0],       #TODO: verify if this is necessary
83
        dry_run=settings.TEST,
84
        beparams=dict(auto_balance=True, vcpus=flavor.cpu, memory=flavor.ram))
85

  
86
def delete_instance(vm):
87
    start_action(vm, 'DESTROY')
88
    rapi.DeleteInstance(vm.backend_id)
89

  
90
def reboot_instance(vm, reboot_type):
91
    assert reboot_type in ('soft', 'hard')
92
    rapi.RebootInstance(vm.backend_id, reboot_type)
93

  
94
def startup_instance(vm):
95
    start_action(vm, 'START')
96
    rapi.StartupInstance(vm.backend_id)
97

  
98
def shutdown_instance(vm):
99
    start_action(vm, 'STOP')
100
    rapi.ShutdownInstance(vm.backend_id)
101

  
102
def get_instance_console(vm):
103
    return rapi.GetInstanceConsole(vm.backend_id)
b/tools/cloud
244 244
    
245 245
    def execute(self, server_id):
246 246
        path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
247
        body = json.dumps({'console':{'type':'vnc'}})
247
        body = json.dumps({'console': {'type': 'vnc'}})
248 248
        reply = self.http_cmd('POST', path, body, 200)
249
        print_dict(reply['vnc'])
249
        print_dict(reply['console'])
250 250

  
251 251

  
252 252
@command_name('lsaddr')

Also available in: Unified diff