1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2011 Citrix Systems
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
10
# http://www.apache.org/licenses/LICENSE-2.0
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
17
# @author: Tyler Smith, Cisco Systems
25
# FIXME(danwent): All content in this file should be removed once the
26
# packaging work for the quantum client libraries is complete.
27
# At that point, we will be able to just install the libraries as a
28
# dependency and import from quantum.client.* and quantum.common.*
29
# Until then, we have simplified versions of these classes in this file.
31
class JSONSerializer(object):
32
"""This is a simple json-only serializer to use until we can just grab
33
the standard serializer from the quantum library.
35
def serialize(self, data, content_type):
37
return json.dumps(data)
40
return json.dumps(to_primitive(data))
42
def deserialize(self, data, content_type):
43
return json.loads(data)
46
# The full client lib will expose more
47
# granular exceptions, for now, just try to distinguish
48
# between the cases we care about.
49
class QuantumNotFoundException(Exception):
50
"""Indicates that Quantum Server returned 404"""
54
class QuantumServerException(Exception):
55
"""Indicates any non-404 error from Quantum Server"""
59
class QuantumIOException(Exception):
60
"""Indicates network IO trouble reaching Quantum Server"""
64
class api_call(object):
65
"""A Decorator to add support for format and tenant overriding"""
66
def __init__(self, func):
69
def __get__(self, instance, owner):
70
def with_params(*args, **kwargs):
71
"""Temporarily set format and tenant for this request"""
72
(format, tenant) = (instance.format, instance.tenant)
74
if 'format' in kwargs:
75
instance.format = kwargs['format']
76
if 'tenant' in kwargs:
77
instance.tenant = kwargs['tenant']
81
ret = self.func(instance, *args)
83
(instance.format, instance.tenant) = (format, tenant)
89
"""A base client class - derived from Glance.BaseClient"""
91
action_prefix = '/v1.0/tenants/{tenant_id}'
93
"""Action query strings"""
94
networks_path = "/networks"
95
network_path = "/networks/%s"
96
ports_path = "/networks/%s/ports"
97
port_path = "/networks/%s/ports/%s"
98
attachment_path = "/networks/%s/ports/%s/attachment"
100
def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None,
101
format="xml", testing_stub=None, key_file=None,
102
cert_file=None, logger=None):
103
"""Creates a new client to some service.
105
:param host: The host where service resides
106
:param port: The port where service resides
107
:param use_ssl: True to use SSL, False to use HTTP
108
:param tenant: The tenant ID to make requests with
109
:param format: The format to query the server with
110
:param testing_stub: A class that stubs basic server methods for tests
111
:param key_file: The SSL key file to use if use_ssl is true
112
:param cert_file: The SSL cert file to use if use_ssl is true
116
self.use_ssl = use_ssl
119
self.connection = None
120
self.testing_stub = testing_stub
121
self.key_file = key_file
122
self.cert_file = cert_file
125
def get_connection_type(self):
126
"""Returns the proper connection type"""
127
if self.testing_stub:
128
return self.testing_stub
130
return httplib.HTTPSConnection
132
return httplib.HTTPConnection
134
def do_request(self, method, action, body=None,
135
headers=None, params=None):
136
"""Connects to the server and issues a request.
137
Returns the result data, or raises an appropriate exception if
138
HTTP status code is not 2xx
140
:param method: HTTP method ("GET", "POST", "PUT", etc...)
141
:param body: string of data to send, or None (default)
142
:param headers: mapping of key/value pairs to add as headers
143
:param params: dictionary of key/value pairs to add to append
147
# Ensure we have a tenant id
149
raise Exception(_("Tenant ID not set"))
151
# Add format and tenant_id
152
action += ".%s" % self.format
153
action = Client.action_prefix + action
154
action = action.replace('{tenant_id}', self.tenant)
156
if type(params) is dict:
157
action += '?' + urllib.urlencode(params)
160
connection_type = self.get_connection_type()
161
headers = headers or {"Content-Type":
162
"application/%s" % self.format}
164
# Open connection and send request, handling SSL certs
165
certs = {'key_file': self.key_file, 'cert_file': self.cert_file}
166
certs = dict((x, certs[x]) for x in certs if certs[x] != None)
168
if self.use_ssl and len(certs):
169
c = connection_type(self.host, self.port, **certs)
171
c = connection_type(self.host, self.port)
175
_("Quantum Client Request:\n%(method)s %(action)s\n" %
178
self.logger.debug(body)
180
c.request(method, action, body, headers)
181
res = c.getresponse()
182
status_code = self.get_status_code(res)
186
self.logger.debug("Quantum Client Reply (code = %s) :\n %s" \
187
% (str(status_code), data))
189
if status_code == httplib.NOT_FOUND:
190
raise QuantumNotFoundException(
191
_("Quantum entity not found: %s" % data))
193
if status_code in (httplib.OK,
197
if data is not None and len(data):
198
return self.deserialize(data, status_code)
200
raise QuantumServerException(
201
_("Server %(status_code)s error: %(data)s"
204
except (socket.error, IOError), e:
205
raise QuantumIOException(_("Unable to connect to "
206
"server. Got error: %s" % e))
208
def get_status_code(self, response):
209
"""Returns the integer status code from the response, which
210
can be either a Webob.Response (used in testing) or httplib.Response
212
if hasattr(response, 'status_int'):
213
return response.status_int
215
return response.status
217
def serialize(self, data):
220
elif type(data) is dict:
221
return JSONSerializer().serialize(data, self.content_type())
223
raise Exception(_("unable to deserialize object of type = '%s'" %
226
def deserialize(self, data, status_code):
227
if status_code == 202:
229
return JSONSerializer().deserialize(data, self.content_type())
231
def content_type(self, format=None):
234
return "application/%s" % (format)
237
def list_networks(self):
238
"""Fetches a list of all networks for a tenant"""
239
return self.do_request("GET", self.networks_path)
242
def show_network_details(self, network):
243
"""Fetches the details of a certain network"""
244
return self.do_request("GET", self.network_path % (network))
247
def create_network(self, body=None):
248
"""Creates a new network"""
249
body = self.serialize(body)
250
return self.do_request("POST", self.networks_path, body=body)
253
def update_network(self, network, body=None):
254
"""Updates a network"""
255
body = self.serialize(body)
256
return self.do_request("PUT", self.network_path % (network), body=body)
259
def delete_network(self, network):
260
"""Deletes the specified network"""
261
return self.do_request("DELETE", self.network_path % (network))
264
def list_ports(self, network):
265
"""Fetches a list of ports on a given network"""
266
return self.do_request("GET", self.ports_path % (network))
269
def show_port_details(self, network, port):
270
"""Fetches the details of a certain port"""
271
return self.do_request("GET", self.port_path % (network, port))
274
def create_port(self, network, body=None):
275
"""Creates a new port on a given network"""
276
body = self.serialize(body)
277
return self.do_request("POST", self.ports_path % (network), body=body)
280
def delete_port(self, network, port):
281
"""Deletes the specified port from a network"""
282
return self.do_request("DELETE", self.port_path % (network, port))
285
def set_port_state(self, network, port, body=None):
286
"""Sets the state of the specified port"""
287
body = self.serialize(body)
288
return self.do_request("PUT",
289
self.port_path % (network, port), body=body)
292
def show_port_attachment(self, network, port):
293
"""Fetches the attachment-id associated with the specified port"""
294
return self.do_request("GET", self.attachment_path % (network, port))
297
def attach_resource(self, network, port, body=None):
298
"""Sets the attachment-id of the specified port"""
299
body = self.serialize(body)
300
return self.do_request("PUT",
301
self.attachment_path % (network, port), body=body)
304
def detach_resource(self, network, port):
305
"""Removes the attachment-id of the specified port"""
306
return self.do_request("DELETE",
307
self.attachment_path % (network, port))