Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / cyclades / __init__.py @ 6c068db6

History | View | Annotate | Download (16 kB)

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

    
34
from time import sleep
35

    
36
from kamaki.clients.cyclades.rest_api import CycladesRestClient
37
from kamaki.clients import ClientError
38

    
39

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

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

48
        :param name: (str)
49

50
        :param flavor_id: integer id denoting a preset hardware configuration
51

52
        :param image_id: (str) id denoting the OS image to run on virt. server
53

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

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

59
        :returns: a dict with the new virtual server details
60

61
        :raises ClientError: wraps request errors
62
        """
63
        image = self.get_image_details(image_id)
64
        metadata = metadata or dict()
65
        for key in ('os', 'users'):
66
            try:
67
                metadata[key] = image['metadata'][key]
68
            except KeyError:
69
                pass
70

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

    
75
    def start_server(self, server_id):
76
        """Submit a startup request
77

78
        :param server_id: integer (str or int)
79

80
        :returns: (dict) response headers
81
        """
82
        req = {'start': {}}
83
        r = self.servers_action_post(server_id, json_data=req, success=202)
84
        return r.headers
85

    
86
    def shutdown_server(self, server_id):
87
        """Submit a shutdown request
88

89
        :param server_id: integer (str or int)
90

91
        :returns: (dict) response headers
92
        """
93
        req = {'shutdown': {}}
94
        r = self.servers_action_post(server_id, json_data=req, success=202)
95
        return r.headers
96

    
97
    def get_server_console(self, server_id):
98
        """
99
        :param server_id: integer (str or int)
100

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

    
107
    def get_firewall_profile(self, server_id):
108
        """
109
        :param server_id: integer (str or int)
110

111
        :returns: (str) ENABLED | DISABLED | PROTECTED
112

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

    
123
    def set_firewall_profile(self, server_id, profile):
124
        """Set the firewall profile for the public interface of a server
125

126
        :param server_id: integer (str or int)
127

128
        :param profile: (str) ENABLED | DISABLED | PROTECTED
129

130
        :returns: (dict) response headers
131
        """
132
        req = {'firewallProfile': {'profile': profile}}
133
        r = self.servers_action_post(server_id, json_data=req, success=202)
134
        return r.headers
135

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

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

    
145
    def get_server_stats(self, server_id):
146
        """
147
        :param server_id: integer (str or int)
148

149
        :returns: (dict) auto-generated graphs of statistics (urls)
150
        """
151
        r = self.servers_stats_get(server_id)
152
        return r.json['stats']
153

    
154
    def list_networks(self, detail=False):
155
        """
156
        :param detail: (bool)
157

158
        :returns: (list) id,name if not detail else full info per network
159
        """
160
        detail = 'detail' if detail else ''
161
        r = self.networks_get(command=detail)
162
        return r.json['networks']
163

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

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

    
173
    def create_network(
174
            self, name,
175
            cidr=None, gateway=None, type=None, dhcp=False):
176
        """
177
        :param name: (str)
178

179
        :param cidr: (str)
180

181
        :param geteway: (str)
182

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

186
        :param dhcp: (bool)
187

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

    
201
    def get_network_details(self, network_id):
202
        """
203
        :param network_id: integer (str or int)
204

205
        :returns: (dict)
206
        """
207
        r = self.networks_get(network_id=network_id)
208
        return r.json['network']
209

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

214
        :param new_name: (str)
215

216
        :returns: (dict) response headers
217
        """
218
        req = {'network': {'name': new_name}}
219
        r = self.networks_put(network_id=network_id, json_data=req)
220
        return r.headers
221

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

226
        :returns: (dict) response headers
227

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

    
239
    def connect_server(self, server_id, network_id):
240
        """ Connect a server to a network
241

242
        :param server_id: integer (str or int)
243

244
        :param network_id: integer (str or int)
245

246
        :returns: (dict) response headers
247
        """
248
        req = {'add': {'serverRef': server_id}}
249
        r = self.networks_post(network_id, 'action', json_data=req)
250
        return r.headers
251

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

256
        :param nic_id: (str)
257

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

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

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

283
        :param server_id: integer (str or int)
284

285
        :param current_status: (str)
286

287
        :param get_status: (method(self, item_id)) if called, returns
288
            (status, progress %) If no way to tell progress, return None
289

290
        :param delay: time interval between retries
291

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

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

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

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

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

    
323
        if total_wait < max_wait:
324
            if wait_cb:
325
                try:
326
                    for i in range(max_wait):
327
                        wait_gen.next()
328
                except:
329
                    pass
330
        return status if status != current_status else False
331

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

338
        :param server_id: integer (str or int)
339

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

342
        :param delay: time interval between retries
343

344
        :max_wait: (int) timeout in secconds
345

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

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

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

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

    
359
    def wait_network(
360
            self, net_id,
361
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
362
        """Wait for network while its status is current_status
363

364
        :param net_id: integer (str or int)
365

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

368
        :param delay: time interval between retries
369

370
        :max_wait: (int) timeout in secconds
371

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

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

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

    
381
        return self._wait(
382
            net_id, current_status, get_status, delay, max_wait, wait_cb)
383

    
384
    def wait_firewall(
385
            self, server_id,
386
            current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
387
        """Wait while the public network firewall status is current_status
388

389
        :param server_id: integer (str or int)
390

391
        :param current_status: (str) DISABLED | ENABLED | PROTECTED
392

393
        :param delay: time interval between retries
394

395
        :max_wait: (int) timeout in secconds
396

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

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

    
402
        def get_status(self, server_id):
403
            return self.get_firewall_profile(server_id), None
404

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

    
408
    def get_floating_ip_pools(self):
409
        """
410
        :returns: (dict) {floating_ip_pools:[{name: ...}, ...]}
411
        """
412
        r = self.floating_ip_pools_get()
413
        return r.json
414

    
415
    def get_floating_ips(self):
416
        """
417
        :returns: (dict) {floating_ips: [fixed_ip: , id: , ip: , pool: ]}
418
        """
419
        r = self.floating_ips_get()
420
        return r.json
421

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

426
        :param address: (str) ip address to request
427

428
        :returns: (dict) {
429
            fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}
430
        """
431
        json_data = dict()
432
        if pool:
433
            json_data['pool'] = pool
434
        if address:
435
            json_data['address'] = address
436
        r = self.floating_ips_post(json_data)
437
        return r.json['floating_ip']
438

    
439
    def get_floating_ip(self, fip_id):
440
        """
441
        :param fip_id: (str) floating ip id
442

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

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

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

456
        :returns: (dict) request headers
457

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

    
464
    def attach_floating_ip(self, server_id, address):
465
        """Associate the address ip to server with server_id
466

467
        :param server_id: (int)
468

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

471
        :returns: (dict) request headers
472

473
        :raises ValueError: if server_id cannot be converted to int
474

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

477
        :raises AssertionError: if address is emtpy
478
        """
479
        server_id = int(server_id)
480
        assert address, 'address is needed for attach_floating_ip'
481
        req = dict(addFloatingIp=dict(address=address))
482
        r = self.servers_action_post(server_id, json_data=req)
483
        return r.headers
484

    
485
    def detach_floating_ip(self, server_id, address):
486
        """Disassociate an address ip from the server with server_id
487

488
        :param server_id: (int)
489

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

492
        :returns: (dict) request headers
493

494
        :raises ValueError: if server_id cannot be converted to int
495

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

498
        :raises AssertionError: if address is emtpy
499
        """
500
        server_id = int(server_id)
501
        assert address, 'address is needed for detach_floating_ip'
502
        req = dict(removeFloatingIp=dict(address=address))
503
        r = self.servers_action_post(server_id, json_data=req)
504
        return r.headers