~registry/neutron/github

« back to all changes in this revision

Viewing changes to client/lib/quantum/client.py

  • Committer: Brad Hall
  • Date: 2011-11-28 18:33:52 UTC
  • Revision ID: git-v1:6a08320031d03913981c444cce97c7ccd08c8169
Second round of packaging changes

This change condenses the directory structure to something more similar to
what we had before while producing similar packages.

It also introduces version.py which allows us to get the version from git tags
(or a fallback version if not available).

Fixes lp bug 889336
Fixes lp bug 888795

Change-Id: I86136bd9dbabb5eb1f8366ed665ed9b54f695124

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
 
 
3
 
# Copyright 2011 Citrix Systems
4
 
# All Rights Reserved.
5
 
#
6
 
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
7
 
#    not use this file except in compliance with the License. You may obtain
8
 
#    a copy of the License at
9
 
#
10
 
#         http://www.apache.org/licenses/LICENSE-2.0
11
 
#
12
 
#    Unless required by applicable law or agreed to in writing, software
13
 
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
 
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
 
#    License for the specific language governing permissions and limitations
16
 
#    under the License.
17
 
#    @author: Tyler Smith, Cisco Systems
18
 
 
19
 
import logging
20
 
import httplib
21
 
import socket
22
 
import urllib
23
 
 
24
 
from quantum.common import exceptions
25
 
from quantum.common.serializer import Serializer
26
 
 
27
 
LOG = logging.getLogger('quantum.client')
28
 
EXCEPTIONS = {
29
 
    400: exceptions.BadInputError,
30
 
    401: exceptions.NotAuthorized,
31
 
    420: exceptions.NetworkNotFound,
32
 
    421: exceptions.NetworkInUse,
33
 
    430: exceptions.PortNotFound,
34
 
    431: exceptions.StateInvalid,
35
 
    432: exceptions.PortInUseClient,
36
 
    440: exceptions.AlreadyAttachedClient}
37
 
AUTH_TOKEN_HEADER = "X-Auth-Token"
38
 
 
39
 
 
40
 
class ApiCall(object):
41
 
    """A Decorator to add support for format and tenant overriding"""
42
 
    def __init__(self, function):
43
 
        self.function = function
44
 
 
45
 
    def __get__(self, instance, owner):
46
 
        def with_params(*args, **kwargs):
47
 
            """
48
 
            Temporarily sets the format and tenant for this request
49
 
            """
50
 
            (format, tenant) = (instance.format, instance.tenant)
51
 
 
52
 
            if 'format' in kwargs:
53
 
                instance.format = kwargs['format']
54
 
            if 'tenant' in kwargs:
55
 
                instance.tenant = kwargs['tenant']
56
 
 
57
 
            ret = self.function(instance, *args)
58
 
            (instance.format, instance.tenant) = (format, tenant)
59
 
            return ret
60
 
        return with_params
61
 
 
62
 
 
63
 
class Client(object):
64
 
 
65
 
    """A base client class - derived from Glance.BaseClient"""
66
 
 
67
 
    #Metadata for deserializing xml
68
 
    _serialization_metadata = {
69
 
        "application/xml": {
70
 
            "attributes": {
71
 
                "network": ["id", "name"],
72
 
                "port": ["id", "state"],
73
 
                "attachment": ["id"]},
74
 
            "plurals": {"networks": "network",
75
 
                        "ports": "port"}},
76
 
    }
77
 
 
78
 
    # Action query strings
79
 
    networks_path = "/networks"
80
 
    network_path = "/networks/%s"
81
 
    ports_path = "/networks/%s/ports"
82
 
    port_path = "/networks/%s/ports/%s"
83
 
    attachment_path = "/networks/%s/ports/%s/attachment"
84
 
 
85
 
    def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None,
86
 
                format="xml", testingStub=None, key_file=None, cert_file=None,
87
 
                auth_token=None, logger=None,
88
 
                action_prefix="/v1.0/tenants/{tenant_id}"):
89
 
        """
90
 
        Creates a new client to some service.
91
 
 
92
 
        :param host: The host where service resides
93
 
        :param port: The port where service resides
94
 
        :param use_ssl: True to use SSL, False to use HTTP
95
 
        :param tenant: The tenant ID to make requests with
96
 
        :param format: The format to query the server with
97
 
        :param testingStub: A class that stubs basic server methods for tests
98
 
        :param key_file: The SSL key file to use if use_ssl is true
99
 
        :param cert_file: The SSL cert file to use if use_ssl is true
100
 
        :param auth_token: authentication token to be passed to server
101
 
        :param logger: Logger object for the client library
102
 
        :param action_prefix: prefix for request URIs
103
 
        """
104
 
        self.host = host
105
 
        self.port = port
106
 
        self.use_ssl = use_ssl
107
 
        self.tenant = tenant
108
 
        self.format = format
109
 
        self.connection = None
110
 
        self.testingStub = testingStub
111
 
        self.key_file = key_file
112
 
        self.cert_file = cert_file
113
 
        self.logger = logger
114
 
        self.auth_token = auth_token
115
 
        self.action_prefix = action_prefix
116
 
 
117
 
    def get_connection_type(self):
118
 
        """
119
 
        Returns the proper connection type
120
 
        """
121
 
        if self.testingStub:
122
 
            return self.testingStub
123
 
        if self.use_ssl:
124
 
            return httplib.HTTPSConnection
125
 
        else:
126
 
            return httplib.HTTPConnection
127
 
 
128
 
    def _send_request(self, conn, method, action, body, headers):
129
 
        # Salvatore: Isolating this piece of code in its own method to
130
 
        # facilitate stubout for testing
131
 
        if self.logger:
132
 
            self.logger.debug("Quantum Client Request:\n" \
133
 
                    + method + " " + action + "\n")
134
 
            if body:
135
 
                self.logger.debug(body)
136
 
        conn.request(method, action, body, headers)
137
 
        return conn.getresponse()
138
 
 
139
 
    def do_request(self, method, action, body=None,
140
 
                   headers=None, params=None, exception_args={}):
141
 
        """
142
 
        Connects to the server and issues a request.
143
 
        Returns the result data, or raises an appropriate exception if
144
 
        HTTP status code is not 2xx
145
 
 
146
 
        :param method: HTTP method ("GET", "POST", "PUT", etc...)
147
 
        :param body: string of data to send, or None (default)
148
 
        :param headers: mapping of key/value pairs to add as headers
149
 
        :param params: dictionary of key/value pairs to add to append
150
 
                             to action
151
 
 
152
 
        """
153
 
        LOG.debug("Client issuing request: %s", action)
154
 
        # Ensure we have a tenant id
155
 
        if not self.tenant:
156
 
            raise Exception("Tenant ID not set")
157
 
 
158
 
        # Add format and tenant_id
159
 
        action += ".%s" % self.format
160
 
        action = self.action_prefix + action
161
 
        action = action.replace('{tenant_id}', self.tenant)
162
 
 
163
 
        if type(params) is dict:
164
 
            action += '?' + urllib.urlencode(params)
165
 
        if body:
166
 
            body = self.serialize(body)
167
 
 
168
 
        try:
169
 
            connection_type = self.get_connection_type()
170
 
            headers = headers or {"Content-Type":
171
 
                                      "application/%s" % self.format}
172
 
            # if available, add authentication token
173
 
            if self.auth_token:
174
 
                headers[AUTH_TOKEN_HEADER] = self.auth_token
175
 
            # Open connection and send request, handling SSL certs
176
 
            certs = {'key_file': self.key_file, 'cert_file': self.cert_file}
177
 
            certs = dict((x, certs[x]) for x in certs if certs[x] != None)
178
 
 
179
 
            if self.use_ssl and len(certs):
180
 
                conn = connection_type(self.host, self.port, **certs)
181
 
            else:
182
 
                conn = connection_type(self.host, self.port)
183
 
            res = self._send_request(conn, method, action, body, headers)
184
 
            status_code = self.get_status_code(res)
185
 
            data = res.read()
186
 
 
187
 
            if self.logger:
188
 
                self.logger.debug("Quantum Client Reply (code = %s) :\n %s" \
189
 
                        % (str(status_code), data))
190
 
            if status_code in (httplib.OK,
191
 
                               httplib.CREATED,
192
 
                               httplib.ACCEPTED,
193
 
                               httplib.NO_CONTENT):
194
 
                return self.deserialize(data, status_code)
195
 
            else:
196
 
                error_message = res.read()
197
 
                LOG.debug("Server returned error: %s", status_code)
198
 
                LOG.debug("Error message: %s", error_message)
199
 
                # Create exception with HTTP status code and message
200
 
                if res.status in EXCEPTIONS:
201
 
                    raise EXCEPTIONS[res.status](**exception_args)
202
 
                # Add error code and message to exception arguments
203
 
                ex = Exception("Server returned error: %s" % status_code)
204
 
                ex.args = ([dict(status_code=status_code,
205
 
                                 message=error_message)],)
206
 
                raise ex
207
 
        except (socket.error, IOError), e:
208
 
            msg = "Unable to connect to server. Got error: %s" % e
209
 
            LOG.exception(msg)
210
 
            raise Exception(msg)
211
 
 
212
 
    def get_status_code(self, response):
213
 
        """
214
 
        Returns the integer status code from the response, which
215
 
        can be either a Webob.Response (used in testing) or httplib.Response
216
 
        """
217
 
        if hasattr(response, 'status_int'):
218
 
            return response.status_int
219
 
        else:
220
 
            return response.status
221
 
 
222
 
    def serialize(self, data):
223
 
        """
224
 
        Serializes a dictionary with a single key (which can contain any
225
 
        structure) into either xml or json
226
 
        """
227
 
        if data is None:
228
 
            return None
229
 
        elif type(data) is dict:
230
 
            return Serializer().serialize(data, self.content_type())
231
 
        else:
232
 
            raise Exception("unable to serialize object of type = '%s'" \
233
 
                                % type(data))
234
 
 
235
 
    def deserialize(self, data, status_code):
236
 
        """
237
 
        Deserializes a an xml or json string into a dictionary
238
 
        """
239
 
        if status_code == 204:
240
 
            return data
241
 
        return Serializer(self._serialization_metadata).\
242
 
                    deserialize(data, self.content_type())
243
 
 
244
 
    def content_type(self, format=None):
245
 
        """
246
 
        Returns the mime-type for either 'xml' or 'json'.  Defaults to the
247
 
        currently set format
248
 
        """
249
 
        if not format:
250
 
            format = self.format
251
 
        return "application/%s" % (format)
252
 
 
253
 
    @ApiCall
254
 
    def list_networks(self):
255
 
        """
256
 
        Fetches a list of all networks for a tenant
257
 
        """
258
 
        return self.do_request("GET", self.networks_path)
259
 
 
260
 
    @ApiCall
261
 
    def show_network_details(self, network):
262
 
        """
263
 
        Fetches the details of a certain network
264
 
        """
265
 
        return self.do_request("GET", self.network_path % (network),
266
 
                                        exception_args={"net_id": network})
267
 
 
268
 
    @ApiCall
269
 
    def create_network(self, body=None):
270
 
        """
271
 
        Creates a new network
272
 
        """
273
 
        return self.do_request("POST", self.networks_path, body=body)
274
 
 
275
 
    @ApiCall
276
 
    def update_network(self, network, body=None):
277
 
        """
278
 
        Updates a network
279
 
        """
280
 
        return self.do_request("PUT", self.network_path % (network), body=body,
281
 
                                        exception_args={"net_id": network})
282
 
 
283
 
    @ApiCall
284
 
    def delete_network(self, network):
285
 
        """
286
 
        Deletes the specified network
287
 
        """
288
 
        return self.do_request("DELETE", self.network_path % (network),
289
 
                                        exception_args={"net_id": network})
290
 
 
291
 
    @ApiCall
292
 
    def list_ports(self, network):
293
 
        """
294
 
        Fetches a list of ports on a given network
295
 
        """
296
 
        return self.do_request("GET", self.ports_path % (network))
297
 
 
298
 
    @ApiCall
299
 
    def show_port_details(self, network, port):
300
 
        """
301
 
        Fetches the details of a certain port
302
 
        """
303
 
        return self.do_request("GET", self.port_path % (network, port),
304
 
                       exception_args={"net_id": network, "port_id": port})
305
 
 
306
 
    @ApiCall
307
 
    def create_port(self, network, body=None):
308
 
        """
309
 
        Creates a new port on a given network
310
 
        """
311
 
        return self.do_request("POST", self.ports_path % (network), body=body,
312
 
                       exception_args={"net_id": network})
313
 
 
314
 
    @ApiCall
315
 
    def delete_port(self, network, port):
316
 
        """
317
 
        Deletes the specified port from a network
318
 
        """
319
 
        return self.do_request("DELETE", self.port_path % (network, port),
320
 
                       exception_args={"net_id": network, "port_id": port})
321
 
 
322
 
    @ApiCall
323
 
    def update_port(self, network, port, body=None):
324
 
        """
325
 
        Sets the attributes of the specified port
326
 
        """
327
 
        return self.do_request("PUT",
328
 
            self.port_path % (network, port), body=body,
329
 
                       exception_args={"net_id": network,
330
 
                                       "port_id": port})
331
 
 
332
 
    @ApiCall
333
 
    def show_port_attachment(self, network, port):
334
 
        """
335
 
        Fetches the attachment-id associated with the specified port
336
 
        """
337
 
        return self.do_request("GET", self.attachment_path % (network, port),
338
 
                       exception_args={"net_id": network, "port_id": port})
339
 
 
340
 
    @ApiCall
341
 
    def attach_resource(self, network, port, body=None):
342
 
        """
343
 
        Sets the attachment-id of the specified port
344
 
        """
345
 
        return self.do_request("PUT",
346
 
            self.attachment_path % (network, port), body=body,
347
 
                       exception_args={"net_id": network,
348
 
                                       "port_id": port,
349
 
                                       "attach_id": str(body)})
350
 
 
351
 
    @ApiCall
352
 
    def detach_resource(self, network, port):
353
 
        """
354
 
        Removes the attachment-id of the specified port
355
 
        """
356
 
        return self.do_request("DELETE",
357
 
                               self.attachment_path % (network, port),
358
 
                    exception_args={"net_id": network, "port_id": port})