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

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    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

        :param image_id: (str) id denoting the OS image to run on the VM

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

        :param personality: a list of (file path, file contents) tuples,
            describing files to be injected into VM upon creation.

        :returns: a dict with the new VMs details

        :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
85
        r = self.servers_post(server_id, 'action', json_data=req, success=202)
        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
96
        r = self.servers_post(server_id, 'action', json_data=req, success=202)
        return r.headers
97

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

        :returns: (dict) info to set a VNC connection to VM
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
104
        req = {'console': {'type': 'vnc'}}
105
        r = self.servers_post(server_id, 'action', 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
135
        r = self.servers_post(server_id, 'action', json_data=req, success=202)
        return r.headers
136

137
138
139
140
141
142
143
144
    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
        """
145
        r = self.servers_get(detail=bool(detail), changes_since=changes_since)
146
        return r.json['servers']
147

148
    def list_server_nics(self, server_id):
149
150
151
152
153
        """
        :param server_id: integer (str or int)

        :returns: (dict) network interface connections
        """
154
        r = self.servers_get(server_id, 'ips')
155
156
        return r.json['attachments']
        #return r.json['addresses']
157

Giorgos Verigakis's avatar
Giorgos Verigakis committed
158
    def get_server_stats(self, server_id):
159
160
161
162
163
        """
        :param server_id: integer (str or int)

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

Giorgos Verigakis's avatar
Giorgos Verigakis committed
167
    def list_networks(self, detail=False):
168
169
170
171
172
        """
        :param detail: (bool)

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

177
    def list_network_nics(self, network_id):
178
179
180
181
182
        """
        :param network_id: integer (str or int)

        :returns: (list)
        """
183
        r = self.networks_get(network_id=network_id)
184
        return r.json['network']['attachments']
185

186
187
    def create_network(
            self, name,
188
            cidr=None, gateway=None, type=None, dhcp=False):
189
190
191
192
193
194
195
        """
        :param name: (str)

        :param cidr: (str)

        :param geteway: (str)

196
197
        :param type: (str) if None, will use MAC_FILTERED as default
            Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
198

199
        :param dhcp: (bool)
200
201

        :returns: (dict) network detailed info
202
203
204
        """
        net = dict(name=name)
        if cidr:
205
            net['cidr'] = cidr
206
        if gateway:
207
            net['gateway'] = gateway
208
        net['type'] = type or 'MAC_FILTERED'
209
        net['dhcp'] = True if dhcp else False
210
        req = dict(network=net)
211
        r = self.networks_post(json_data=req, success=202)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
212
        return r.json['network']
Giorgos Verigakis's avatar
Giorgos Verigakis committed
213
214

    def get_network_details(self, network_id):
215
216
217
218
219
        """
        :param network_id: integer (str or int)

        :returns: (dict)
        """
220
        r = self.networks_get(network_id=network_id)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
221
        return r.json['network']
Giorgos Verigakis's avatar
Giorgos Verigakis committed
222
223

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

        :param new_name: (str)
228
229

        :returns: (dict) response headers
230
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
231
        req = {'network': {'name': new_name}}
232
233
        r = self.networks_put(network_id=network_id, json_data=req)
        return r.headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
234
235

    def delete_network(self, network_id):
236
237
238
        """
        :param network_id: integer (str or int)

239
240
        :returns: (dict) response headers

241
242
        :raises ClientError: 421 Network in use
        """
243
        try:
244
245
            r = self.networks_delete(network_id)
            return r.headers
246
247
        except ClientError as err:
            if err.status == 421:
248
                err.details = [
249
                    'Network may be still connected to at least one server']
250
            raise
Giorgos Verigakis's avatar
Giorgos Verigakis committed
251
252

    def connect_server(self, server_id, network_id):
253
254
255
256
257
        """ Connect a server to a network

        :param server_id: integer (str or int)

        :param network_id: integer (str or int)
258
259

        :returns: (dict) response headers
260
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
261
        req = {'add': {'serverRef': server_id}}
262
263
        r = self.networks_post(network_id, 'action', json_data=req)
        return r.headers
Giorgos Verigakis's avatar
Giorgos Verigakis committed
264

265
    def disconnect_server(self, server_id, nic_id):
266
267
268
269
        """
        :param server_id: integer (str or int)

        :param nic_id: (str)
270
271

        :returns: (int) the number of nics disconnected
272
        """
273
        vm_nets = self.list_server_nics(server_id)
274
        num_of_disconnections = 0
275
        for (nic_id, network_id) in [(
276
277
                net['id'],
                net['network_id']) for net in vm_nets if nic_id == net['id']]:
278
            req = {'remove': {'attachment': '%s' % nic_id}}
279
            self.networks_post(network_id, 'action', json_data=req)
280
281
            num_of_disconnections += 1
        return num_of_disconnections
282
283

    def disconnect_network_nics(self, netid):
284
285
286
287
        """
        :param netid: integer (str or int)
        """
        for nic in self.list_network_nics(netid):
288
            req = dict(remove=dict(attachment=nic))
289
            self.networks_post(netid, 'action', json_data=req)
290

291
292
293
294
    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
295
296
297

        :param server_id: integer (str or int)

298
299
300
301
        :param current_status: (str)

        :param get_status: (method(self, item_id)) if called, returns
            (status, progress %) If no way to tell progress, return None
302
303
304

        :param delay: time interval between retries

305
        :param wait_cb: if set a progress bar is used to show progress
306

307
        :returns: (str) the new mode if successful, (bool) False if timed out
308
        """
309
310
311
        status, progress = get_status(self, item_id)
        if status != current_status:
            return status
312
        old_wait = total_wait = 0
313

314
        if wait_cb:
315
            wait_gen = wait_cb(1 + max_wait // delay)
316
            wait_gen.next()
317

318
319
320
321
        while status == current_status and total_wait <= max_wait:
            if wait_cb:
                try:
                    for i in range(total_wait - old_wait):
322
                        wait_gen.next()
323
324
                except Exception:
                    break
325
            else:
326
327
328
329
                stdout.write('.')
                stdout.flush()
            old_wait = total_wait
            total_wait = progress or (total_wait + 1)
330
            sleep(delay)
331
            status, progress = get_status(self, item_id)
332

333
        if total_wait < max_wait:
334
335
            if wait_cb:
                try:
336
                    for i in range(max_wait):
337
338
339
                        wait_gen.next()
                except:
                    pass
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
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
        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)
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448

    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

449
    def attach_floating_ip(self, server_id, address):
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
        """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)
465
        assert address, 'address is needed for attach_floating_ip'
466
467
468
469
470
        r = self.servers_post(
            server_id, 'action',
            json_data=dict(addFloatingIp=dict(address=address)))
        return r.headers

471
    def detach_floating_ip(self, server_id, address):
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
        """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)
487
        assert address, 'address is needed for detach_floating_ip'
488
489
490
491
        r = self.servers_post(
            server_id, 'action',
            json_data=dict(removeFloatingIp=dict(address=address)))
        return r.headers