__init__.py 14.6 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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
44
    def start_server(self, server_id):
45
46
47
        """Submit a startup request

        :param server_id: integer (str or int)
48
49

        :returns: (dict) response headers
50
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
51
        req = {'start': {}}
52
53
        r = self.servers_post(server_id, 'action', json_data=req, success=202)
        return r.headers
54

Giorgos Verigakis's avatar
Giorgos Verigakis committed
55
    def shutdown_server(self, server_id):
56
57
58
        """Submit a shutdown request

        :param server_id: integer (str or int)
59
60

        :returns: (dict) response headers
61
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
62
        req = {'shutdown': {}}
63
64
        r = self.servers_post(server_id, 'action', json_data=req, success=202)
        return r.headers
65

Giorgos Verigakis's avatar
Giorgos Verigakis committed
66
    def get_server_console(self, server_id):
67
68
69
70
71
        """
        :param server_id: integer (str or int)

        :returns: (dict) info to set a VNC connection to VM
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
72
        req = {'console': {'type': 'vnc'}}
73
        r = self.servers_post(server_id, 'action', json_data=req, success=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
74
        return r.json['console']
75
76

    def get_firewall_profile(self, server_id):
77
78
79
80
81
82
83
        """
        :param server_id: integer (str or int)

        :returns: (str) ENABLED | DISABLED | PROTECTED

        :raises ClientError: 520 No Firewall Profile
        """
84
85
        r = self.get_server_details(server_id)
        try:
86
            return r['attachments'][0]['firewallProfile']
87
        except KeyError:
88
            raise ClientError(
89
                'No Firewall Profile',
90
                details='Server %s is missing a firewall profile' % server_id)
91

Giorgos Verigakis's avatar
Giorgos Verigakis committed
92
93
    def set_firewall_profile(self, server_id, profile):
        """Set the firewall profile for the public interface of a server
94
95
96
97

        :param server_id: integer (str or int)

        :param profile: (str) ENABLED | DISABLED | PROTECTED
98
99

        :returns: (dict) response headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
100
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
101
        req = {'firewallProfile': {'profile': profile}}
102
103
        r = self.servers_post(server_id, 'action', json_data=req, success=202)
        return r.headers
104

105
106
107
108
109
110
111
112
113
114
    def list_servers(self, detail=False, changes_since=None):
        """
        :param detail: (bool) append full server details to each item if true

        :param changes_since: (date)

        :returns: list of server ids and names
        """
        detail = 'detail' if detail else ''
        r = self.servers_get(command=detail, changes_since=changes_since)
115
        return r.json['servers']
116

117
    def list_server_nics(self, server_id):
118
119
120
121
122
        """
        :param server_id: integer (str or int)

        :returns: (dict) network interface connections
        """
123
        r = self.servers_get(server_id, 'ips')
124
125
        return r.json['attachments']
        #return r.json['addresses']
126

Giorgos Verigakis's avatar
Giorgos Verigakis committed
127
    def get_server_stats(self, server_id):
128
129
130
131
132
        """
        :param server_id: integer (str or int)

        :returns: (dict) auto-generated graphs of statistics (urls)
        """
133
        r = self.servers_get(server_id, 'stats')
Giorgos Verigakis's avatar
Giorgos Verigakis committed
134
        return r.json['stats']
135

Giorgos Verigakis's avatar
Giorgos Verigakis committed
136
    def list_networks(self, detail=False):
137
138
139
140
141
        """
        :param detail: (bool)

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

146
    def list_network_nics(self, network_id):
147
148
149
150
151
        """
        :param network_id: integer (str or int)

        :returns: (list)
        """
152
        r = self.networks_get(network_id=network_id)
153
        return r.json['network']['attachments']
154

155
156
    def create_network(
            self, name,
157
            cidr=None, gateway=None, type=None, dhcp=False):
158
159
160
161
162
163
164
        """
        :param name: (str)

        :param cidr: (str)

        :param geteway: (str)

165
166
        :param type: (str) if None, will use MAC_FILTERED as default
            Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
167

168
        :param dhcp: (bool)
169
170

        :returns: (dict) network detailed info
171
172
173
        """
        net = dict(name=name)
        if cidr:
174
            net['cidr'] = cidr
175
        if gateway:
176
            net['gateway'] = gateway
177
        net['type'] = type or 'MAC_FILTERED'
178
        net['dhcp'] = True if dhcp else False
179
        req = dict(network=net)
180
        r = self.networks_post(json_data=req, success=202)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
181
        return r.json['network']
Giorgos Verigakis's avatar
Giorgos Verigakis committed
182
183

    def get_network_details(self, network_id):
184
185
186
187
188
        """
        :param network_id: integer (str or int)

        :returns: (dict)
        """
189
        r = self.networks_get(network_id=network_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
190
        return r.json['network']
Giorgos Verigakis's avatar
Giorgos Verigakis committed
191
192

    def update_network_name(self, network_id, new_name):
193
194
195
196
        """
        :param network_id: integer (str or int)

        :param new_name: (str)
197
198

        :returns: (dict) response headers
199
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
200
        req = {'network': {'name': new_name}}
201
202
        r = self.networks_put(network_id=network_id, json_data=req)
        return r.headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
203
204

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

208
209
        :returns: (dict) response headers

210
211
        :raises ClientError: 421 Network in use
        """
212
        try:
213
214
            r = self.networks_delete(network_id)
            return r.headers
215
216
        except ClientError as err:
            if err.status == 421:
217
                err.details = [
218
                    'Network may be still connected to at least one server']
219
            raise
Giorgos Verigakis's avatar
Giorgos Verigakis committed
220
221

    def connect_server(self, server_id, network_id):
222
223
224
225
226
        """ Connect a server to a network

        :param server_id: integer (str or int)

        :param network_id: integer (str or int)
227
228

        :returns: (dict) response headers
229
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
230
        req = {'add': {'serverRef': server_id}}
231
232
        r = self.networks_post(network_id, 'action', json_data=req)
        return r.headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
233

234
    def disconnect_server(self, server_id, nic_id):
235
236
237
238
        """
        :param server_id: integer (str or int)

        :param nic_id: (str)
239
240

        :returns: (int) the number of nics disconnected
241
        """
242
        vm_nets = self.list_server_nics(server_id)
243
        num_of_disconnections = 0
244
        for (nic_id, network_id) in [(
245
246
                net['id'],
                net['network_id']) for net in vm_nets if nic_id == net['id']]:
247
            req = {'remove': {'attachment': '%s' % nic_id}}
248
            self.networks_post(network_id, 'action', json_data=req)
249
250
            num_of_disconnections += 1
        return num_of_disconnections
251
252

    def disconnect_network_nics(self, netid):
253
254
255
256
        """
        :param netid: integer (str or int)
        """
        for nic in self.list_network_nics(netid):
257
            req = dict(remove=dict(attachment=nic))
258
            self.networks_post(netid, 'action', json_data=req)
259

260
261
262
263
    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
264
265
266

        :param server_id: integer (str or int)

267
268
269
270
        :param current_status: (str)

        :param get_status: (method(self, item_id)) if called, returns
            (status, progress %) If no way to tell progress, return None
271
272
273

        :param delay: time interval between retries

274
        :param wait_cb: if set a progress bar is used to show progress
275

276
        :returns: (str) the new mode if successful, (bool) False if timed out
277
        """
278
279
280
        status, progress = get_status(self, item_id)
        if status != current_status:
            return status
281
        old_wait = total_wait = 0
282

283
        if wait_cb:
284
            wait_gen = wait_cb(1 + max_wait // delay)
285
            wait_gen.next()
286

287
288
289
290
        while status == current_status and total_wait <= max_wait:
            if wait_cb:
                try:
                    for i in range(total_wait - old_wait):
291
                        wait_gen.next()
292
293
                except Exception:
                    break
294
            else:
295
296
297
298
                stdout.write('.')
                stdout.flush()
            old_wait = total_wait
            total_wait = progress or (total_wait + 1)
299
            sleep(delay)
300
            status, progress = get_status(self, item_id)
301

302
        if total_wait < max_wait:
303
304
            if wait_cb:
                try:
305
                    for i in range(max_wait):
306
307
308
                        wait_gen.next()
                except:
                    pass
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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
        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,
            current_status='LALA', delay=1, max_wait=100, wait_cb=None):
        """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)
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417

    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):
        """
        :returns: (dict) {floating_ips:[
            {fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...},
            ... ]}
        """
        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) {
                fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...
            }
        """
        json_data = dict()
        if pool:
            json_data['pool'] = pool
            if address:
                json_data['address'] = address
        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

418
    def attach_floating_ip(self, server_id, address):
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
        """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)
434
        assert address, 'address is needed for attach_floating_ip'
435
436
437
438
439
        r = self.servers_post(
            server_id, 'action',
            json_data=dict(addFloatingIp=dict(address=address)))
        return r.headers

440
    def detach_floating_ip(self, server_id, address):
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
        """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)
456
        assert address, 'address is needed for detach_floating_ip'
457
458
459
460
        r = self.servers_post(
            server_id, 'action',
            json_data=dict(removeFloatingIp=dict(address=address)))
        return r.headers