Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / cyclades / __init__.py @ 264a13f7

History | View | Annotate | Download (17.8 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, networks=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
        :param networks: (list of dicts) Networks to connect to, list this:
62
            "networks": [
63
                {"network": <network_uuid>},
64
                {"network": <network_uuid>, "fixed_ip": address},
65
                {"port": <port_id>}, ...]
66

67
        :returns: a dict with the new virtual server details
68

69
        :raises ClientError: wraps request errors
70
        """
71
        image = self.get_image_details(image_id)
72
        metadata = metadata or dict()
73
        for key in ('os', 'users'):
74
            try:
75
                metadata[key] = image['metadata'][key]
76
            except KeyError:
77
                pass
78

    
79
        return super(CycladesClient, self).create_server(
80
            name, flavor_id, image_id,
81
            metadata=metadata, personality=personality)
82

    
83
    def start_server(self, server_id):
84
        """Submit a startup request
85

86
        :param server_id: integer (str or int)
87

88
        :returns: (dict) response headers
89
        """
90
        req = {'start': {}}
91
        r = self.servers_action_post(server_id, json_data=req, success=202)
92
        return r.headers
93

    
94
    def shutdown_server(self, server_id):
95
        """Submit a shutdown request
96

97
        :param server_id: integer (str or int)
98

99
        :returns: (dict) response headers
100
        """
101
        req = {'shutdown': {}}
102
        r = self.servers_action_post(server_id, json_data=req, success=202)
103
        return r.headers
104

    
105
    def get_server_console(self, server_id):
106
        """
107
        :param server_id: integer (str or int)
108

109
        :returns: (dict) info to set a VNC connection to virtual server
110
        """
111
        req = {'console': {'type': 'vnc'}}
112
        r = self.servers_action_post(server_id, json_data=req, success=200)
113
        return r.json['console']
114

    
115
    def get_firewall_profile(self, server_id):
116
        """
117
        :param server_id: integer (str or int)
118

119
        :returns: (str) ENABLED | DISABLED | PROTECTED
120

121
        :raises ClientError: 520 No Firewall Profile
122
        """
123
        r = self.get_server_details(server_id)
124
        try:
125
            return r['attachments'][0]['firewallProfile']
126
        except KeyError:
127
            raise ClientError(
128
                'No Firewall Profile',
129
                details='Server %s is missing a firewall profile' % server_id)
130

    
131
    def set_firewall_profile(self, server_id, profile):
132
        """Set the firewall profile for the public interface of a server
133

134
        :param server_id: integer (str or int)
135

136
        :param profile: (str) ENABLED | DISABLED | PROTECTED
137

138
        :returns: (dict) response headers
139
        """
140
        req = {'firewallProfile': {'profile': profile}}
141
        r = self.servers_action_post(server_id, json_data=req, success=202)
142
        return r.headers
143

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

148
        :returns: (dict) network interface connections
149
        """
150
        r = self.servers_ips_get(server_id)
151
        return r.json['attachments']
152

    
153
    def get_server_stats(self, server_id):
154
        """
155
        :param server_id: integer (str or int)
156

157
        :returns: (dict) auto-generated graphs of statistics (urls)
158
        """
159
        r = self.servers_stats_get(server_id)
160
        return r.json['stats']
161

    
162
    def list_networks(self, detail=False):
163
        """
164
        :param detail: (bool)
165

166
        :returns: (list) id,name if not detail else full info per network
167
        """
168
        detail = 'detail' if detail else ''
169
        r = self.networks_get(command=detail)
170
        return r.json['networks']
171

    
172
    def list_network_nics(self, network_id):
173
        """
174
        :param network_id: integer (str or int)
175

176
        :returns: (list)
177
        """
178
        r = self.networks_get(network_id=network_id)
179
        return r.json['network']['attachments']
180

    
181
    def create_network(
182
            self, name,
183
            cidr=None, gateway=None, type=None, dhcp=False):
184
        """
185
        :param name: (str)
186

187
        :param cidr: (str)
188

189
        :param geteway: (str)
190

191
        :param type: (str) if None, will use MAC_FILTERED as default
192
            Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
193

194
        :param dhcp: (bool)
195

196
        :returns: (dict) network detailed info
197
        """
198
        net = dict(name=name)
199
        if cidr:
200
            net['cidr'] = cidr
201
        if gateway:
202
            net['gateway'] = gateway
203
        net['type'] = type or 'MAC_FILTERED'
204
        net['dhcp'] = True if dhcp else False
205
        req = dict(network=net)
206
        r = self.networks_post(json_data=req, success=202)
207
        return r.json['network']
208

    
209
    def get_network_details(self, network_id):
210
        """
211
        :param network_id: integer (str or int)
212

213
        :returns: (dict)
214
        """
215
        r = self.networks_get(network_id=network_id)
216
        return r.json['network']
217

    
218
    def update_network_name(self, network_id, new_name):
219
        """
220
        :param network_id: integer (str or int)
221

222
        :param new_name: (str)
223

224
        :returns: (dict) response headers
225
        """
226
        req = {'network': {'name': new_name}}
227
        r = self.networks_put(network_id=network_id, json_data=req)
228
        return r.headers
229

    
230
    def delete_network(self, network_id):
231
        """
232
        :param network_id: integer (str or int)
233

234
        :returns: (dict) response headers
235

236
        :raises ClientError: 421 Network in use
237
        """
238
        try:
239
            r = self.networks_delete(network_id)
240
            return r.headers
241
        except ClientError as err:
242
            if err.status == 421:
243
                err.details = [
244
                    'Network may be still connected to at least one server']
245
            raise
246

    
247
    def connect_server(self, server_id, network_id):
248
        """ Connect a server to a network
249

250
        :param server_id: integer (str or int)
251

252
        :param network_id: integer (str or int)
253

254
        :returns: (dict) response headers
255
        """
256
        req = {'add': {'serverRef': server_id}}
257
        r = self.networks_post(network_id, 'action', json_data=req)
258
        return r.headers
259

    
260
    def disconnect_server(self, server_id, nic_id):
261
        """
262
        :param server_id: integer (str or int)
263

264
        :param nic_id: (str)
265

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

    
278
    def disconnect_network_nics(self, netid):
279
        """
280
        :param netid: integer (str or int)
281
        """
282
        for nic in self.list_network_nics(netid):
283
            req = dict(remove=dict(attachment=nic))
284
            self.networks_post(netid, 'action', json_data=req)
285

    
286
    def _wait(
287
            self, item_id, current_status, get_status,
288
            delay=1, max_wait=100, wait_cb=None):
289
        """Wait for item while its status is current_status
290

291
        :param server_id: integer (str or int)
292

293
        :param current_status: (str)
294

295
        :param get_status: (method(self, item_id)) if called, returns
296
            (status, progress %) If no way to tell progress, return None
297

298
        :param delay: time interval between retries
299

300
        :param wait_cb: if set a progress bar is used to show progress
301

302
        :returns: (str) the new mode if successful, (bool) False if timed out
303
        """
304
        status, progress = get_status(self, item_id)
305

    
306
        if wait_cb:
307
            wait_gen = wait_cb(max_wait // delay)
308
            wait_gen.next()
309

    
310
        if status != current_status:
311
            if wait_cb:
312
                try:
313
                    wait_gen.next()
314
                except Exception:
315
                    pass
316
            return status
317
        old_wait = total_wait = 0
318

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

    
331
        if total_wait < max_wait:
332
            if wait_cb:
333
                try:
334
                    for i in range(max_wait):
335
                        wait_gen.next()
336
                except:
337
                    pass
338
        return status if status != current_status else False
339

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

346
        :param server_id: integer (str or int)
347

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

350
        :param delay: time interval between retries
351

352
        :max_wait: (int) timeout in secconds
353

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

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

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

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

    
367
    def wait_network(
368
            self, net_id,
369
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
370
        """Wait for network while its status is current_status
371

372
        :param net_id: integer (str or int)
373

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

376
        :param delay: time interval between retries
377

378
        :max_wait: (int) timeout in secconds
379

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

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

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

    
389
        return self._wait(
390
            net_id, current_status, get_status, delay, max_wait, wait_cb)
391

    
392
    def wait_firewall(
393
            self, server_id,
394
            current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
395
        """Wait while the public network firewall status is current_status
396

397
        :param server_id: integer (str or int)
398

399
        :param current_status: (str) DISABLED | ENABLED | PROTECTED
400

401
        :param delay: time interval between retries
402

403
        :max_wait: (int) timeout in secconds
404

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

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

    
410
        def get_status(self, server_id):
411
            return self.get_firewall_profile(server_id), None
412

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

    
416
    def get_floating_ip_pools(self):
417
        """
418
        :returns: (dict) {floating_ip_pools:[{name: ...}, ...]}
419
        """
420
        r = self.floating_ip_pools_get()
421
        return r.json
422

    
423
    def get_floating_ips(self):
424
        """
425
        :returns: (dict) {floating_ips: [fixed_ip: , id: , ip: , pool: ]}
426
        """
427
        r = self.floating_ips_get()
428
        return r.json
429

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

434
        :param address: (str) ip address to request
435

436
        :returns: (dict) {
437
            fixed_ip: ..., id: ..., instance_id: ..., ip: ..., pool: ...}
438
        """
439
        json_data = dict()
440
        if pool:
441
            json_data['pool'] = pool
442
        if address:
443
            json_data['address'] = address
444
        r = self.floating_ips_post(json_data)
445
        return r.json['floating_ip']
446

    
447
    def get_floating_ip(self, fip_id):
448
        """
449
        :param fip_id: (str) floating ip id
450

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

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

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

464
        :returns: (dict) request headers
465

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

    
472
    def attach_floating_ip(self, server_id, address):
473
        """Associate the address ip to server with server_id
474

475
        :param server_id: (int)
476

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

479
        :returns: (dict) request headers
480

481
        :raises ValueError: if server_id cannot be converted to int
482

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

485
        :raises AssertionError: if address is emtpy
486
        """
487
        server_id = int(server_id)
488
        assert address, 'address is needed for attach_floating_ip'
489
        req = dict(addFloatingIp=dict(address=address))
490
        r = self.servers_action_post(server_id, json_data=req)
491
        return r.headers
492

    
493
    def detach_floating_ip(self, server_id, address):
494
        """Disassociate an address ip from the server with server_id
495

496
        :param server_id: (int)
497

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

500
        :returns: (dict) request headers
501

502
        :raises ValueError: if server_id cannot be converted to int
503

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

506
        :raises AssertionError: if address is emtpy
507
        """
508
        server_id = int(server_id)
509
        assert address, 'address is needed for detach_floating_ip'
510
        req = dict(removeFloatingIp=dict(address=address))
511
        r = self.servers_action_post(server_id, json_data=req)
512
        return r.headers
513

    
514

    
515
class CycladesNetworkClient(NetworkClient):
516
    """Cyclades Network API extentions"""
517

    
518
    network_types = (
519
        'CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
520

    
521
    def list_networks(self, detail=None):
522
        path = path4url('networks', 'detail' if detail else '')
523
        r = self.get(path, success=200)
524
        return r.json['networks']
525

    
526
    def create_network(self, type, name=None, shared=None):
527
        req = dict(network=dict(type=type, admin_state_up=True))
528
        if name:
529
            req['network']['name'] = name
530
        if shared not in (None, ):
531
            req['network']['shared'] = bool(shared)
532
        r = self.networks_post(json_data=req, success=201)
533
        return r.json['network']
534

    
535
    def create_port(
536
            self, network_id, device_id,
537
            security_groups=None, name=None, fixed_ips=None):
538
        port = dict(network_id=network_id, device_id=device_id)
539
        if security_groups:
540
            port['security_groups'] = security_groups
541
        if name:
542
            port['name'] = name
543
        for fixed_ip in fixed_ips:
544
            diff = set(['subnet_id', 'ip_address']).difference(fixed_ip)
545
            if diff:
546
                raise ValueError(
547
                    'Invalid format for "fixed_ips", %s missing' % diff)
548
        if fixed_ips:
549
            port['fixed_ips'] = fixed_ips
550
        r = self.ports_post(json_data=dict(port=port), success=201)
551
        return r.json['port']