Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / cyclades / __init__.py @ ccdd1b82

History | View | Annotate | Download (17.1 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.network import NetworkClient
38
from kamaki.clients.utils import path4url
39
from kamaki.clients import ClientError
40

    
41

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

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

50
        :param name: (str)
51

52
        :param flavor_id: integer id denoting a preset hardware configuration
53

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

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

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

61
        :returns: a dict with the new virtual server details
62

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

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

    
77
    def start_server(self, server_id):
78
        """Submit a startup request
79

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

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

    
88
    def shutdown_server(self, server_id):
89
        """Submit a shutdown request
90

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

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

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

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

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

113
        :returns: (str) ENABLED | DISABLED | PROTECTED
114

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

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

128
        :param server_id: integer (str or int)
129

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

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

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

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

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

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

    
156
    def list_networks(self, detail=False):
157
        """
158
        :param detail: (bool)
159

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

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

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

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

181
        :param cidr: (str)
182

183
        :param geteway: (str)
184

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

188
        :param dhcp: (bool)
189

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

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

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

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

216
        :param new_name: (str)
217

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

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

228
        :returns: (dict) response headers
229

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

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

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

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

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

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

258
        :param nic_id: (str)
259

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

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

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

285
        :param server_id: integer (str or int)
286

287
        :param current_status: (str)
288

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

292
        :param delay: time interval between retries
293

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

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

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

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

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

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

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

340
        :param server_id: integer (str or int)
341

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

344
        :param delay: time interval between retries
345

346
        :max_wait: (int) timeout in secconds
347

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

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

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

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

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

366
        :param net_id: integer (str or int)
367

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

370
        :param delay: time interval between retries
371

372
        :max_wait: (int) timeout in secconds
373

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

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

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

    
383
        return self._wait(
384
            net_id, current_status, get_status, delay, max_wait, wait_cb)
385

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

391
        :param server_id: integer (str or int)
392

393
        :param current_status: (str) DISABLED | ENABLED | PROTECTED
394

395
        :param delay: time interval between retries
396

397
        :max_wait: (int) timeout in secconds
398

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

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

    
404
        def get_status(self, server_id):
405
            return self.get_firewall_profile(server_id), None
406

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

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

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

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

428
        :param address: (str) ip address to request
429

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

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

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

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

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

458
        :returns: (dict) request headers
459

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

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

469
        :param server_id: (int)
470

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

473
        :returns: (dict) request headers
474

475
        :raises ValueError: if server_id cannot be converted to int
476

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

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

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

490
        :param server_id: (int)
491

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

494
        :returns: (dict) request headers
495

496
        :raises ValueError: if server_id cannot be converted to int
497

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

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

    
508

    
509
class CycladesNetworkClient(NetworkClient):
510
    """Cyclades Network API extentions"""
511

    
512
    network_types = (
513
        'CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
514

    
515
    def list_networks(self, detail=None):
516
        path = path4url('networks', 'detail' if detail else '')
517
        r = self.get(path, success=200)
518
        return r.json['networks']
519

    
520
    def create_network(self, type, name=None, shared=None):
521
        req = dict(network=dict(type=type, admin_state_up=True))
522
        if name:
523
            req['network']['name'] = name
524
        if shared not in (None, ):
525
            req['network']['shared'] = bool(shared)
526
        r = self.networks_post(json_data=req, success=201)
527
        return r.json['network']
528

    
529
    def create_port(self, network_id, device_id, security_groups=None):
530
        port = dict(network_id=network_id, device_id=device_id)
531
        if security_groups:
532
            port['security_groups'] = security_groups
533
        r = self.ports_post(json_data=dict(port=port), success=201)
534
        return r.json['port']