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

34
from sys import stdout
35
36
from time import sleep

37
from kamaki.clients.cyclades.rest_api import CycladesRestClient
38
from kamaki.clients import ClientError
Giorgos Verigakis's avatar
Giorgos Verigakis committed
39

40

41
class CycladesClient(CycladesRestClient):
42
    """Synnefo Cyclades Compute API client"""
43

44
45
46
47
48
49
50
51
52
    def create_server(
            self, name, flavor_id, image_id,
            metadata=None, personality=None):
        """Submit request to create a new server

        :param name: (str)

        :param flavor_id: integer id denoting a preset hardware configuration

53
        :param image_id: (str) id denoting the OS image to run on virt. server
54
55
56
57

        :param metadata: (dict) vm metadata updated by os/users image metadata

        :param personality: a list of (file path, file contents) tuples,
58
            describing files to be injected into virtual server upon creation
59

60
        :returns: a dict with the new virtual server details
61
62
63
64
65
66
67
68
69
70
71

        :raises ClientError: wraps request errors
        """
        image = self.get_image_details(image_id)
        metadata = metadata or dict()
        for key in ('os', 'users'):
            try:
                metadata[key] = image['metadata'][key]
            except KeyError:
                pass

72
73
74
        return super(CycladesClient, self).create_server(
            name, flavor_id, image_id,
            metadata=metadata, personality=personality)
75

Giorgos Verigakis's avatar
Giorgos Verigakis committed
76
    def start_server(self, server_id):
77
78
79
        """Submit a startup request

        :param server_id: integer (str or int)
80
81

        :returns: (dict) response headers
82
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
83
        req = {'start': {}}
84
        r = self.servers_action_post(server_id, json_data=req, success=202)
85
        return r.headers
86

Giorgos Verigakis's avatar
Giorgos Verigakis committed
87
    def shutdown_server(self, server_id):
88
89
90
        """Submit a shutdown request

        :param server_id: integer (str or int)
91
92

        :returns: (dict) response headers
93
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
94
        req = {'shutdown': {}}
95
        r = self.servers_action_post(server_id, json_data=req, success=202)
96
        return r.headers
97

Giorgos Verigakis's avatar
Giorgos Verigakis committed
98
    def get_server_console(self, server_id):
99
100
101
        """
        :param server_id: integer (str or int)

102
        :returns: (dict) info to set a VNC connection to virtual server
103
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
104
        req = {'console': {'type': 'vnc'}}
105
        r = self.servers_action_post(server_id, json_data=req, success=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
106
        return r.json['console']
107
108

    def get_firewall_profile(self, server_id):
109
110
111
112
113
114
115
        """
        :param server_id: integer (str or int)

        :returns: (str) ENABLED | DISABLED | PROTECTED

        :raises ClientError: 520 No Firewall Profile
        """
116
117
        r = self.get_server_details(server_id)
        try:
118
            return r['attachments'][0]['firewallProfile']
119
        except KeyError:
120
            raise ClientError(
121
                'No Firewall Profile',
122
                details='Server %s is missing a firewall profile' % server_id)
123

Giorgos Verigakis's avatar
Giorgos Verigakis committed
124
125
    def set_firewall_profile(self, server_id, profile):
        """Set the firewall profile for the public interface of a server
126
127
128
129

        :param server_id: integer (str or int)

        :param profile: (str) ENABLED | DISABLED | PROTECTED
130
131

        :returns: (dict) response headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
132
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
133
        req = {'firewallProfile': {'profile': profile}}
134
        r = self.servers_action_post(server_id, json_data=req, success=202)
135
        return r.headers
136

137
    def list_server_nics(self, server_id):
138
139
140
141
142
        """
        :param server_id: integer (str or int)

        :returns: (dict) network interface connections
        """
143
        r = self.servers_ips_get(server_id)
144
        return r.json['attachments']
145

Giorgos Verigakis's avatar
Giorgos Verigakis committed
146
    def get_server_stats(self, server_id):
147
148
149
150
151
        """
        :param server_id: integer (str or int)

        :returns: (dict) auto-generated graphs of statistics (urls)
        """
152
        r = self.servers_stats_get(server_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
153
        return r.json['stats']
154

Giorgos Verigakis's avatar
Giorgos Verigakis committed
155
    def list_networks(self, detail=False):
156
157
158
159
160
        """
        :param detail: (bool)

        :returns: (list) id,name if not detail else full info per network
        """
161
162
        detail = 'detail' if detail else ''
        r = self.networks_get(command=detail)
163
        return r.json['networks']
Giorgos Verigakis's avatar
Giorgos Verigakis committed
164

165
    def list_network_nics(self, network_id):
166
167
168
169
170
        """
        :param network_id: integer (str or int)

        :returns: (list)
        """
171
        r = self.networks_get(network_id=network_id)
172
        return r.json['network']['attachments']
173

174
175
    def create_network(
            self, name,
176
            cidr=None, gateway=None, type=None, dhcp=False):
177
178
179
180
181
182
183
        """
        :param name: (str)

        :param cidr: (str)

        :param geteway: (str)

184
185
        :param type: (str) if None, will use MAC_FILTERED as default
            Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
186

187
        :param dhcp: (bool)
188
189

        :returns: (dict) network detailed info
190
191
192
        """
        net = dict(name=name)
        if cidr:
193
            net['cidr'] = cidr
194
        if gateway:
195
            net['gateway'] = gateway
196
        net['type'] = type or 'MAC_FILTERED'
197
        net['dhcp'] = True if dhcp else False
198
        req = dict(network=net)
199
        r = self.networks_post(json_data=req, success=202)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
200
        return r.json['network']
Giorgos Verigakis's avatar
Giorgos Verigakis committed
201
202

    def get_network_details(self, network_id):
203
204
205
206
207
        """
        :param network_id: integer (str or int)

        :returns: (dict)
        """
208
        r = self.networks_get(network_id=network_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
209
        return r.json['network']
Giorgos Verigakis's avatar
Giorgos Verigakis committed
210
211

    def update_network_name(self, network_id, new_name):
212
213
214
215
        """
        :param network_id: integer (str or int)

        :param new_name: (str)
216
217

        :returns: (dict) response headers
218
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
219
        req = {'network': {'name': new_name}}
220
221
        r = self.networks_put(network_id=network_id, json_data=req)
        return r.headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
222
223

    def delete_network(self, network_id):
224
225
226
        """
        :param network_id: integer (str or int)

227
228
        :returns: (dict) response headers

229
230
        :raises ClientError: 421 Network in use
        """
231
        try:
232
233
            r = self.networks_delete(network_id)
            return r.headers
234
235
        except ClientError as err:
            if err.status == 421:
236
                err.details = [
237
                    'Network may be still connected to at least one server']
238
            raise
Giorgos Verigakis's avatar
Giorgos Verigakis committed
239
240

    def connect_server(self, server_id, network_id):
241
242
243
244
245
        """ Connect a server to a network

        :param server_id: integer (str or int)

        :param network_id: integer (str or int)
246
247

        :returns: (dict) response headers
248
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
249
        req = {'add': {'serverRef': server_id}}
250
251
        r = self.networks_post(network_id, 'action', json_data=req)
        return r.headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
252

253
    def disconnect_server(self, server_id, nic_id):
254
255
256
257
        """
        :param server_id: integer (str or int)

        :param nic_id: (str)
258
259

        :returns: (int) the number of nics disconnected
260
        """
261
        vm_nets = self.list_server_nics(server_id)
262
        num_of_disconnections = 0
263
        for (nic_id, network_id) in [(
264
265
                net['id'],
                net['network_id']) for net in vm_nets if nic_id == net['id']]:
266
            req = {'remove': {'attachment': '%s' % nic_id}}
267
            self.networks_post(network_id, 'action', json_data=req)
268
269
            num_of_disconnections += 1
        return num_of_disconnections
270
271

    def disconnect_network_nics(self, netid):
272
273
274
275
        """
        :param netid: integer (str or int)
        """
        for nic in self.list_network_nics(netid):
276
            req = dict(remove=dict(attachment=nic))
277
            self.networks_post(netid, 'action', json_data=req)
278

279
280
281
282
    def _wait(
            self, item_id, current_status, get_status,
            delay=1, max_wait=100, wait_cb=None):
        """Wait for item while its status is current_status
283
284
285

        :param server_id: integer (str or int)

286
287
288
289
        :param current_status: (str)

        :param get_status: (method(self, item_id)) if called, returns
            (status, progress %) If no way to tell progress, return None
290
291
292

        :param delay: time interval between retries

293
        :param wait_cb: if set a progress bar is used to show progress
294

295
        :returns: (str) the new mode if successful, (bool) False if timed out
296
        """
297
        status, progress = get_status(self, item_id)
298

299
        if wait_cb:
300
            wait_gen = wait_cb(max_wait // delay)
301
            wait_gen.next()
302

303
304
305
306
307
308
309
310
311
        if status != current_status:
            if wait_cb:
                try:
                    wait_gen.next()
                except Exception:
                    pass
            return status
        old_wait = total_wait = 0

312
313
314
315
        while status == current_status and total_wait <= max_wait:
            if wait_cb:
                try:
                    for i in range(total_wait - old_wait):
316
                        wait_gen.next()
317
318
319
                except Exception:
                    break
            old_wait = total_wait
320
            total_wait = progress or total_wait + 1
321
            sleep(delay)
322
            status, progress = get_status(self, item_id)
323

324
        if total_wait < max_wait:
325
326
            if wait_cb:
                try:
327
                    for i in range(max_wait):
328
329
330
                        wait_gen.next()
                except:
                    pass
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
        return status if status != current_status else False

    def wait_server(
            self, server_id,
            current_status='BUILD',
            delay=1, max_wait=100, wait_cb=None):
        """Wait for server while its status is current_status

        :param server_id: integer (str or int)

        :param current_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT

        :param delay: time interval between retries

        :param wait_cb: if set a progressbar is used to show progress

        :returns: (str) the new mode if succesfull, (bool) False if timed out
        """

        def get_status(self, server_id):
            r = self.get_server_details(server_id)
            return r['status'], (r.get('progress', None) if (
                            current_status in ('BUILD', )) else None)

        return self._wait(
            server_id, current_status, get_status, delay, max_wait, wait_cb)

    def wait_network(
            self, net_id,
360
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
        """Wait for network while its status is current_status

        :param net_id: integer (str or int)

        :param current_status: (str) PENDING | ACTIVE | DELETED

        :param delay: time interval between retries

        :param wait_cb: if set a progressbar is used to show progress

        :returns: (str) the new mode if succesfull, (bool) False if timed out
        """

        def get_status(self, net_id):
            r = self.get_network_details(net_id)
            return r['status'], None

        return self._wait(
            net_id, current_status, get_status, delay, max_wait, wait_cb)
380
381
382
383
384
385
386
387
388
389

    def get_floating_ip_pools(self):
        """
        :returns: (dict) {floating_ip_pools:[{name: ...}, ...]}
        """
        r = self.floating_ip_pools_get()
        return r.json

    def get_floating_ips(self):
        """
Stavros Sachtouris's avatar
Stavros Sachtouris committed
390
        :returns: (dict) {floating_ips: [fixed_ip: , id: , ip: , pool: ]}
391
392
393
394
395
396
397
398
399
400
401
        """
        r = self.floating_ips_get()
        return r.json

    def alloc_floating_ip(self, pool=None, address=None):
        """
        :param pool: (str) pool of ips to allocate from

        :param address: (str) ip address to request

        :returns: (dict) {
402
            fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}
403
404
405
406
        """
        json_data = dict()
        if pool:
            json_data['pool'] = pool
407
408
        if address:
            json_data['address'] = address
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
        r = self.floating_ips_post(json_data)
        return r.json['floating_ip']

    def get_floating_ip(self, fip_id):
        """
        :param fip_id: (str) floating ip id

        :returns: (dict)
            {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...},

        :raises AssertionError: if fip_id is emtpy
        """
        assert fip_id, 'floating ip id is needed for get_floating_ip'
        r = self.floating_ips_get(fip_id)
        return r.json['floating_ip']

    def delete_floating_ip(self, fip_id=None):
        """
        :param fip_id: (str) floating ip id (if None, all ips are deleted)

        :returns: (dict) request headers

        :raises AssertionError: if fip_id is emtpy
        """
        assert fip_id, 'floating ip id is needed for delete_floating_ip'
        r = self.floating_ips_delete(fip_id)
        return r.headers

437
    def attach_floating_ip(self, server_id, address):
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
        """Associate the address ip to server with server_id

        :param server_id: (int)

        :param address: (str) the ip address to assign to server (vm)

        :returns: (dict) request headers

        :raises ValueError: if server_id cannot be converted to int

        :raises ValueError: if server_id is not of a int-convertable type

        :raises AssertionError: if address is emtpy
        """
        server_id = int(server_id)
453
        assert address, 'address is needed for attach_floating_ip'
454
455
        req = dict(addFloatingIp=dict(address=address))
        r = self.servers_action_post(server_id, json_data=req)
456
457
        return r.headers

458
    def detach_floating_ip(self, server_id, address):
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
        """Disassociate an address ip from the server with server_id

        :param server_id: (int)

        :param address: (str) the ip address to assign to server (vm)

        :returns: (dict) request headers

        :raises ValueError: if server_id cannot be converted to int

        :raises ValueError: if server_id is not of a int-convertable type

        :raises AssertionError: if address is emtpy
        """
        server_id = int(server_id)
474
        assert address, 'address is needed for detach_floating_ip'
475
476
        req = dict(removeFloatingIp=dict(address=address))
        r = self.servers_action_post(server_id, json_data=req)
477
        return r.headers