__init__.py 14.7 KB
Newer Older
1
# Copyright 2011-2015 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
35
from kamaki.clients.cyclades.rest_api import (
    CycladesComputeRestClient, CycladesBlockStorageRestClient)
36
from kamaki.clients.network import NetworkClient
37
from kamaki.clients.utils import path4url
38
from kamaki.clients import ClientError, Waiter, wait
39

40

41
class CycladesComputeClient(CycladesComputeRestClient, Waiter):
42
    """Synnefo Cyclades Compute API client"""
43

44
45
    CONSOLE_TYPES = ('vnc', 'vnc-ws', 'vnc-wss')

46
47
    def create_server(
            self, name, flavor_id, image_id,
48
49
            metadata=None, personality=None, networks=None, project_id=None,
            response_headers=dict(location=None)):
50
51
52
53
54
55
        """Submit request to create a new server

        :param name: (str)

        :param flavor_id: integer id denoting a preset hardware configuration

56
        :param image_id: (str) id denoting the OS image to run on virt. server
57
58
59
60

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

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

63
64
        :param networks: (list of dicts) Networks to connect to, list this:
            "networks": [
65
66
67
            {"uuid": <network_uuid>},
            {"uuid": <network_uuid>, "fixed_ip": address},
            {"port": <port_id>}, ...]
68
69
            ATTENTION: Empty list is different to None. None means 'apply the
            default server policy', empty list means 'do not attach a network'
70

71
        :param project_id: the project where to assign the server
72

73
        :returns: a dict with the new virtual server details
74
75
76
77
78
79
80

        :raises ClientError: wraps request errors
        """
        image = self.get_image_details(image_id)
        metadata = metadata or dict()
        for key in ('os', 'users'):
            try:
81
                metadata.setdefault(key, image['metadata'][key])
82
83
84
            except KeyError:
                pass

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
        req = {'server': {
            'name': name, 'flavorRef': flavor_id, 'imageRef': image_id}}

        if metadata:
            req['server']['metadata'] = metadata

        if personality:
            req['server']['personality'] = personality

        if networks is not None:
            req['server']['networks'] = networks

        if project_id is not None:
            req['server']['project'] = project_id

        r = self.servers_post(json_data=req, success=(202, ))
        for k, v in response_headers.items():
            response_headers[k] = r.headers.get(k, v)
        return r.json['server']
104

105
106
107
108
109
110
111
112
113
114
115
    def set_firewall_profile(self, server_id, profile, port_id):
        """Set the firewall profile for the public interface of a server
        :param server_id: integer (str or int)
        :param profile: (str) ENABLED | DISABLED | PROTECTED
        :param port_id: (str) This port must connect to a public network
        :returns: (dict) response headers
        """
        req = {'firewallProfile': {'profile': profile, 'nic': port_id}}
        r = self.servers_action_post(server_id, json_data=req, success=202)
        return r.headers

Giorgos Verigakis's avatar
Giorgos Verigakis committed
116
    def start_server(self, server_id):
117
118
119
        """Submit a startup request

        :param server_id: integer (str or int)
120
121

        :returns: (dict) response headers
122
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
123
        req = {'start': {}}
124
        r = self.servers_action_post(server_id, json_data=req, success=202)
125
        return r.headers
126

Giorgos Verigakis's avatar
Giorgos Verigakis committed
127
    def shutdown_server(self, server_id):
128
129
130
        """Submit a shutdown request

        :param server_id: integer (str or int)
131
132

        :returns: (dict) response headers
133
        """
Giorgos Verigakis's avatar
Giorgos Verigakis committed
134
        req = {'shutdown': {}}
135
        r = self.servers_action_post(server_id, json_data=req, success=202)
136
        return r.headers
137

138
    def get_server_console(self, server_id, console_type='vnc'):
139
140
141
        """
        :param server_id: integer (str or int)

142
143
        :param console_type: str (vnc, vnc-ws, vnc-wss, default: vnc)

144
        :returns: (dict) info to set a VNC connection to virtual server
145
        """
146
147
148
        ct = self.CONSOLE_TYPES
        assert console_type in ct, '%s not in %s' % (console_type, ct)
        req = {'console': {'type': console_type}}
149
        r = self.servers_action_post(server_id, json_data=req, success=200)
Giorgos Verigakis's avatar
Giorgos Verigakis committed
150
        return r.json['console']
151

152
153
154
155
156
    def reassign_server(self, server_id, project):
        req = {'reassign': {'project': project}}
        r = self.servers_action_post(server_id, json_data=req, success=200)
        return r.headers

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

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

166
167
168
169
170
171
172
173
174
    def get_server_diagnostics(self, server_id):
        """
        :param server_id: integer (str or int)

        :returns: (list)
        """
        r = self.servers_diagnostics_get(server_id)
        return r.json

175
    def get_server_status(self, server_id):
176
177
        """Deprecated - will be removed in version 0.15
        :returns: (current status, progress percentile if available)"""
178
179
180
        r = self.get_server_details(server_id)
        return r['status'], (r.get('progress', None) if (
            r['status'] in ('BUILD', )) else None)
181

182
183
184
185
    def wait_server_while(
            self, server_id,
            current_status='BUILD', delay=1, max_wait=100, wait_cb=None):
        """Wait for server WHILE its status is current_status
186
187
188
        :param server_id: integer (str or int)
        :param current_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT
        :param delay: time interval between retries
189
        :max_wait: (int) timeout in secconds
190
191
192
        :param wait_cb: if set a progressbar is used to show progress
        :returns: (str) the new mode if succesfull, (bool) False if timed out
        """
193
194
195
        return wait(
            self.get_server_details, (server_id, ),
            lambda i: i['status'] != current_status,
196
            delay, max_wait, wait_cb)
197

198
199
200
201
202
203
204
205
206
207
208
    def wait_server_until(
            self, server_id,
            target_status='ACTIVE', delay=1, max_wait=100, wait_cb=None):
        """Wait for server WHILE its status is target_status
        :param server_id: integer (str or int)
        :param target_status: (str) BUILD|ACTIVE|STOPPED|DELETED|REBOOT
        :param delay: time interval between retries
        :max_wait: (int) timeout in secconds
        :param wait_cb: if set a progressbar is used to show progress
        :returns: (str) the new mode if succesfull, (bool) False if timed out
        """
209
210
211
        return wait(
            self.get_server_details, (server_id, ),
            lambda i: i['status'] == target_status,
212
            delay, max_wait, wait_cb)
213

214
    # Backwards compatibility - deprecated, will be replaced in 0.15
215
    wait_server = wait_server_while
216

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
    # Volume attachment extensions

    def get_volume_attachment(self, server_id, attachment_id):
        """
        :param server_id: (str)
        :param attachment_id: (str)
        :returns: (dict) details on the volume attachment
        """
        r = self.volume_attachment_get(server_id, attachment_id)
        return r.json['volumeAttachment']

    def list_volume_attachments(self, server_id):
        """
        :param server_id: (str)
        :returns: (list) all volume attachments for this server
        """
        r = self.volume_attachment_get(server_id)
        return r.json['volumeAttachments']

    def attach_volume(self, server_id, volume_id):
        """Attach volume on server
        :param server_id: (str)
        :volume_id: (str)
        :returns: (dict) information on attachment (contains volumeId)
        """
        r = self.volume_attachment_post(server_id, volume_id)
        return r.json['volumeAttachment']

    def delete_volume_attachment(self, server_id, attachment_id):
        """Delete a volume attachment. The volume will not be deleted.
        :param server_id: (str)
        :param attachment_id: (str)
        :returns: (dict) HTTP response headers
        """
        r = self.volume_attachment_delete(server_id, attachment_id)
        return r.headers

    def detach_volume(self, server_id, volume_id):
        """Remove volume attachment(s) for this volume and server
        This is not an atomic operation. Use "delete_volume_attachment" for an
        atomic operation with similar semantics.
        :param server_id: (str)
        :param volume_id: (str)
        :returns: (list) the deleted attachments
        """
        all_atts = self.list_volume_attachments(server_id)
        vstr = '%s' % volume_id
        attachments = [a for a in all_atts if ('%s' % a['volumeId']) == vstr]
        for attachment in attachments:
            self.delete_volume_attachment(server_id, attachment['id'])
        return attachments
268

269
# Backwards compatibility - will be removed in 0.15
270
271
272
CycladesClient = CycladesComputeClient


273
class CycladesNetworkClient(NetworkClient):
274
275
276
277
278
    """Cyclades Network API extentions"""

    network_types = (
        'CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')

279
280
281
282
283
    def list_networks(self, detail=None):
        path = path4url('networks', 'detail' if detail else '')
        r = self.get(path, success=200)
        return r.json['networks']

284
    def create_network(self, type, name=None, shared=None, project_id=None):
285
286
287
288
289
        req = dict(network=dict(type=type, admin_state_up=True))
        if name:
            req['network']['name'] = name
        if shared not in (None, ):
            req['network']['shared'] = bool(shared)
290
291
        if project_id is not None:
            req['network']['project'] = project_id
292
293
        r = self.networks_post(json_data=req, success=201)
        return r.json['network']
294

295
    def reassign_network(self, network_id, project_id, **kwargs):
296
        """POST endpoint_url/networks/<network_id>/action
297
298
299
300

        :returns: request response
        """
        path = path4url('networks', network_id, 'action')
301
        req = {'reassign': {'project': project_id}}
302
        r = self.post(path, json=req, success=200, **kwargs)
303
304
        return r.headers

305
306
307
308
309
    def list_ports(self, detail=None):
        path = path4url('ports', 'detail' if detail else '')
        r = self.get(path, success=200)
        return r.json['ports']

Stavros Sachtouris's avatar
Stavros Sachtouris committed
310
    def create_port(
311
312
            self, network_id,
            device_id=None, security_groups=None, name=None, fixed_ips=None):
313
314
315
        """
        :param fixed_ips: (list of dicts) [{"ip_address": IPv4}, ...]
        """
316
317
318
        port = dict(network_id=network_id)
        if device_id:
            port['device_id'] = device_id
319
320
        if security_groups:
            port['security_groups'] = security_groups
Stavros Sachtouris's avatar
Stavros Sachtouris committed
321
322
        if name:
            port['name'] = name
323
        if fixed_ips:
324
            for fixed_ip in fixed_ips or []:
325
                if 'ip_address' not in fixed_ip:
326
                    raise ValueError('Invalid fixed_ip [%s]' % fixed_ip)
327
            port['fixed_ips'] = fixed_ips
328
        r = self.ports_post(json_data=dict(port=port), success=201)
329
        return r.json['port']
Stavros Sachtouris's avatar
Stavros Sachtouris committed
330

331
    def create_floatingip(
332
333
            self,
            floating_network_id=None, floating_ip_address='', project_id=None):
334
335
336
        """
        :param floating_network_id: if not provided, it is assigned
            automatically by the service
337
338
        :param floating_ip_address: only if the IP is availabel in network pool
        :param project_id: specific project to get resource quotas from
339
340
341
342
343
344
        """
        floatingip = {}
        if floating_network_id:
            floatingip['floating_network_id'] = floating_network_id
        if floating_ip_address:
            floatingip['floating_ip_address'] = floating_ip_address
345
        if project_id is not None:
346
            floatingip['project'] = project_id
347
348
349
        r = self.floatingips_post(
            json_data=dict(floatingip=floatingip), success=200)
        return r.json['floatingip']
350
351
352
353
354

    def reassign_floating_ip(self, floating_network_id, project_id):
        """Change the project where this ip is charged"""
        path = path4url('floatingips', floating_network_id, 'action')
        json_data = dict(reassign=dict(project=project_id))
355
        self.post(path, json=json_data, success=200)
356
357
358
359
360
361


class CycladesBlockStorageClient(CycladesBlockStorageRestClient):
    """Cyclades Block Storage REST API Client"""

    def create_volume(
362
363
            self, size, display_name,
            server_id=None,
364
365
366
367
368
369
370
371
            display_description=None,
            snapshot_id=None,
            imageRef=None,
            volume_type=None,
            metadata=None,
            project=None):
        """:returns: (dict) new volumes' details"""
        r = self.volumes_post(
372
373
            size, display_name,
            server_id=server_id,
374
375
376
377
378
379
            display_description=display_description,
            snapshot_id=snapshot_id,
            imageRef=imageRef,
            volume_type=volume_type,
            metadata=metadata,
            project=project)
380
        return r.json['volume']
381
382
383
384
385

    def reassign_volume(self, volume_id, project):
        self.volumes_action_post(volume_id, {"reassign": {"project": project}})

    def create_snapshot(
386
387
            self, volume_id, display_name=None, display_description=None):
        """:returns: (dict) new snapshots' details"""
388
389
390
391
        return super(CycladesBlockStorageClient, self).create_snapshot(
            volume_id,
            display_name=display_name,
            display_description=display_description)