__init__.py 12.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
# Backwards compatibility - will be removed in 0.15
219
220
221
CycladesClient = CycladesComputeClient


222
class CycladesNetworkClient(NetworkClient):
223
224
225
226
227
    """Cyclades Network API extentions"""

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

228
229
230
231
232
    def list_networks(self, detail=None):
        path = path4url('networks', 'detail' if detail else '')
        r = self.get(path, success=200)
        return r.json['networks']

233
    def create_network(self, type, name=None, shared=None, project_id=None):
234
235
236
237
238
        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)
239
240
        if project_id is not None:
            req['network']['project'] = project_id
241
242
        r = self.networks_post(json_data=req, success=201)
        return r.json['network']
243

244
    def reassign_network(self, network_id, project_id, **kwargs):
245
        """POST endpoint_url/networks/<network_id>/action
246
247
248
249

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

254
255
256
257
258
    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
259
    def create_port(
260
261
            self, network_id,
            device_id=None, security_groups=None, name=None, fixed_ips=None):
262
263
264
        """
        :param fixed_ips: (list of dicts) [{"ip_address": IPv4}, ...]
        """
265
266
267
        port = dict(network_id=network_id)
        if device_id:
            port['device_id'] = device_id
268
269
        if security_groups:
            port['security_groups'] = security_groups
Stavros Sachtouris's avatar
Stavros Sachtouris committed
270
271
        if name:
            port['name'] = name
272
        if fixed_ips:
273
            for fixed_ip in fixed_ips or []:
274
                if 'ip_address' not in fixed_ip:
275
                    raise ValueError('Invalid fixed_ip [%s]' % fixed_ip)
276
            port['fixed_ips'] = fixed_ips
277
        r = self.ports_post(json_data=dict(port=port), success=201)
278
        return r.json['port']
Stavros Sachtouris's avatar
Stavros Sachtouris committed
279

280
    def create_floatingip(
281
282
            self,
            floating_network_id=None, floating_ip_address='', project_id=None):
283
284
285
        """
        :param floating_network_id: if not provided, it is assigned
            automatically by the service
286
287
        :param floating_ip_address: only if the IP is availabel in network pool
        :param project_id: specific project to get resource quotas from
288
289
290
291
292
293
        """
        floatingip = {}
        if floating_network_id:
            floatingip['floating_network_id'] = floating_network_id
        if floating_ip_address:
            floatingip['floating_ip_address'] = floating_ip_address
294
        if project_id is not None:
295
            floatingip['project'] = project_id
296
297
298
        r = self.floatingips_post(
            json_data=dict(floatingip=floatingip), success=200)
        return r.json['floatingip']
299
300
301
302
303

    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))
304
        self.post(path, json=json_data, success=200)
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326


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

    def create_volume(
            self, size, server_id, display_name,
            display_description=None,
            snapshot_id=None,
            imageRef=None,
            volume_type=None,
            metadata=None,
            project=None):
        """:returns: (dict) new volumes' details"""
        r = self.volumes_post(
            size, server_id, display_name,
            display_description=display_description,
            snapshot_id=snapshot_id,
            imageRef=imageRef,
            volume_type=volume_type,
            metadata=metadata,
            project=project)
327
        return r.json['volume']
328
329
330
331
332
333
334
335
336
337
338
339

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

    def create_snapshot(
            self, volume_id, display_name,
            force=None, display_description=None):
        return super(CycladesBlockStorageClient, self).create_snapshot(
            volume_id,
            display_name=display_name,
            force=force,
            display_description=display_description)