|
1 # Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. |
|
2 # |
|
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may |
|
4 # not use this file except in compliance with the License. You may obtain |
|
5 # a copy of the License at |
|
6 # |
|
7 # http://www.apache.org/licenses/LICENSE-2.0 |
|
8 # |
|
9 # Unless required by applicable law or agreed to in writing, software |
|
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|
12 # License for the specific language governing permissions and limitations |
|
13 # under the License. |
|
14 """ |
|
15 ZFS Storage Appliance REST API Client Programmatic Interface |
|
16 """ |
|
17 |
|
18 import httplib |
|
19 import json |
|
20 import time |
|
21 import urllib2 |
|
22 import StringIO |
|
23 |
|
24 from cinder.openstack.common import log |
|
25 |
|
26 LOG = log.getLogger(__name__) |
|
27 |
|
28 |
|
29 class Status: |
|
30 """Result HTTP Status""" |
|
31 |
|
32 def __init__(self): |
|
33 pass |
|
34 |
|
35 #: Request return OK |
|
36 OK = httplib.OK |
|
37 |
|
38 #: New resource created successfully |
|
39 CREATED = httplib.CREATED |
|
40 |
|
41 #: Command accepted |
|
42 ACCEPTED = httplib.ACCEPTED |
|
43 |
|
44 #: Command returned OK but no data will be returned |
|
45 NO_CONTENT = httplib.NO_CONTENT |
|
46 |
|
47 #: Bad Request |
|
48 BAD_REQUEST = httplib.BAD_REQUEST |
|
49 |
|
50 #: User is not authorized |
|
51 UNAUTHORIZED = httplib.UNAUTHORIZED |
|
52 |
|
53 #: The request is not allowed |
|
54 FORBIDDEN = httplib.FORBIDDEN |
|
55 |
|
56 #: The requested resource was not found |
|
57 NOT_FOUND = httplib.NOT_FOUND |
|
58 |
|
59 #: The request is not allowed |
|
60 NOT_ALLOWED = httplib.METHOD_NOT_ALLOWED |
|
61 |
|
62 #: Request timed out |
|
63 TIMEOUT = httplib.REQUEST_TIMEOUT |
|
64 |
|
65 #: Invalid request |
|
66 CONFLICT = httplib.CONFLICT |
|
67 |
|
68 #: Service Unavailable |
|
69 BUSY = httplib.SERVICE_UNAVAILABLE |
|
70 |
|
71 |
|
72 class RestResult(object): |
|
73 """Result from a REST API operation""" |
|
74 def __init__(self, response=None, err=None): |
|
75 """Initialize a RestResult containing the results from a REST call |
|
76 :param response: HTTP response |
|
77 """ |
|
78 self.response = response |
|
79 self.error = err |
|
80 self.data = "" |
|
81 self.status = 0 |
|
82 if self.response is not None: |
|
83 self.status = self.response.getcode() |
|
84 result = self.response.read() |
|
85 while result: |
|
86 self.data += result |
|
87 result = self.response.read() |
|
88 |
|
89 if self.error is not None: |
|
90 self.status = self.error.code |
|
91 self.data = httplib.responses[self.status] |
|
92 |
|
93 LOG.debug('response code: %s' % self.status) |
|
94 LOG.debug('response data: %s' % self.data) |
|
95 |
|
96 def get_header(self, name): |
|
97 """Get an HTTP header with the given name from the results |
|
98 |
|
99 :param name: HTTP header name |
|
100 :return: The header value or None if no value is found |
|
101 """ |
|
102 if self.response is None: |
|
103 return None |
|
104 info = self.response.info() |
|
105 return info.getheader(name) |
|
106 |
|
107 |
|
108 class RestClientError(Exception): |
|
109 """Exception for ZFS REST API client errors""" |
|
110 def __init__(self, status, name="ERR_INTERNAL", message=None): |
|
111 |
|
112 """Create a REST Response exception |
|
113 |
|
114 :param status: HTTP response status |
|
115 :param name: The name of the REST API error type |
|
116 :param message: Descriptive error message returned from REST call |
|
117 """ |
|
118 Exception.__init__(self, message) |
|
119 self.code = status |
|
120 self.name = name |
|
121 self.msg = message |
|
122 if status in httplib.responses: |
|
123 self.msg = httplib.responses[status] |
|
124 |
|
125 def __str__(self): |
|
126 return "%d %s %s" % (self.code, self.name, self.msg) |
|
127 |
|
128 |
|
129 class RestClientURL(object): |
|
130 """ZFSSA urllib2 client""" |
|
131 def __init__(self, url, **kwargs): |
|
132 """ |
|
133 Initialize a REST client. |
|
134 |
|
135 :param url: The ZFSSA REST API URL |
|
136 :key session: HTTP Cookie value of x-auth-session obtained from a |
|
137 normal BUI login. |
|
138 :key timeout: Time in seconds to wait for command to complete. |
|
139 (Default is 60 seconds) |
|
140 """ |
|
141 self.url = url |
|
142 self.local = kwargs.get("local", False) |
|
143 self.base_path = kwargs.get("base_path", "/api") |
|
144 self.timeout = kwargs.get("timeout", 60) |
|
145 self.headers = None |
|
146 if kwargs.get('session'): |
|
147 self.headers['x-auth-session'] = kwargs.get('session') |
|
148 |
|
149 self.headers = {"content-type": "application/json"} |
|
150 self.do_logout = False |
|
151 self.auth_str = None |
|
152 |
|
153 def _path(self, path, base_path=None): |
|
154 """build rest url path""" |
|
155 if path.startswith("http://") or path.startswith("https://"): |
|
156 return path |
|
157 if base_path is None: |
|
158 base_path = self.base_path |
|
159 if not path.startswith(base_path) and not ( |
|
160 self.local and ("/api" + path).startswith(base_path)): |
|
161 path = "%s%s" % (base_path, path) |
|
162 if self.local and path.startswith("/api"): |
|
163 path = path[4:] |
|
164 return self.url + path |
|
165 |
|
166 def authorize(self): |
|
167 """Performs authorization setting x-auth-session""" |
|
168 self.headers['authorization'] = 'Basic %s' % self.auth_str |
|
169 if 'x-auth-session' in self.headers: |
|
170 del self.headers['x-auth-session'] |
|
171 |
|
172 try: |
|
173 result = self.post("/access/v1") |
|
174 del self.headers['authorization'] |
|
175 if result.status == httplib.CREATED: |
|
176 self.headers['x-auth-session'] = \ |
|
177 result.get_header('x-auth-session') |
|
178 self.do_logout = True |
|
179 LOG.info('ZFSSA version: %s' % |
|
180 result.get_header('x-zfssa-version')) |
|
181 |
|
182 elif result.status == httplib.NOT_FOUND: |
|
183 raise RestClientError(result.status, name="ERR_RESTError", |
|
184 message="REST Not Available: \ |
|
185 Please Upgrade") |
|
186 |
|
187 except RestClientError as err: |
|
188 del self.headers['authorization'] |
|
189 raise err |
|
190 |
|
191 def login(self, auth_str): |
|
192 """ |
|
193 Login to an appliance using a user name and password and start |
|
194 a session like what is done logging into the BUI. This is not a |
|
195 requirement to run REST commands, since the protocol is stateless. |
|
196 What is does is set up a cookie session so that some server side |
|
197 caching can be done. If login is used remember to call logout when |
|
198 finished. |
|
199 |
|
200 :param auth_str: Authorization string (base64) |
|
201 """ |
|
202 self.auth_str = auth_str |
|
203 self.authorize() |
|
204 |
|
205 def logout(self): |
|
206 """Logout of an appliance""" |
|
207 result = None |
|
208 try: |
|
209 result = self.delete("/access/v1", base_path="/api") |
|
210 except RestClientError: |
|
211 pass |
|
212 |
|
213 self.headers.clear() |
|
214 self.do_logout = False |
|
215 return result |
|
216 |
|
217 def islogin(self): |
|
218 """return if client is login""" |
|
219 return self.do_logout |
|
220 |
|
221 @staticmethod |
|
222 def mkpath(*args, **kwargs): |
|
223 """Make a path?query string for making a REST request |
|
224 |
|
225 :cmd_params args: The path part |
|
226 :cmd_params kwargs: The query part |
|
227 """ |
|
228 buf = StringIO() |
|
229 query = "?" |
|
230 for arg in args: |
|
231 buf.write("/") |
|
232 buf.write(arg) |
|
233 for k in kwargs: |
|
234 buf.write(query) |
|
235 if query == "?": |
|
236 query = "&" |
|
237 buf.write(k) |
|
238 buf.write("=") |
|
239 buf.write(kwargs[k]) |
|
240 return buf.getvalue() |
|
241 |
|
242 def request(self, path, request, body=None, **kwargs): |
|
243 """Make an HTTP request and return the results |
|
244 |
|
245 :param path: Path used with the initiazed URL to make a request |
|
246 :param request: HTTP request type (GET, POST, PUT, DELETE) |
|
247 :param body: HTTP body of request |
|
248 :key accept: Set HTTP 'Accept' header with this value |
|
249 :key base_path: Override the base_path for this request |
|
250 :key content: Set HTTP 'Content-Type' header with this value |
|
251 """ |
|
252 out_hdrs = dict.copy(self.headers) |
|
253 if kwargs.get("accept"): |
|
254 out_hdrs['accept'] = kwargs.get("accept") |
|
255 |
|
256 if body is not None: |
|
257 if isinstance(body, dict): |
|
258 body = str(json.dumps(body)) |
|
259 |
|
260 if body and len(body): |
|
261 out_hdrs['content-length'] = len(body) |
|
262 |
|
263 zfssaurl = self._path(path, kwargs.get("base_path")) |
|
264 req = urllib2.Request(zfssaurl, body, out_hdrs) |
|
265 req.get_method = lambda: request |
|
266 maxreqretries = kwargs.get("maxreqretries", 10) |
|
267 retry = 0 |
|
268 response = None |
|
269 |
|
270 LOG.debug('request: %s %s' % (request, zfssaurl)) |
|
271 LOG.debug('out headers: %s' % out_hdrs) |
|
272 if body is not None and body != '': |
|
273 LOG.debug('body: %s' % body) |
|
274 |
|
275 while retry < maxreqretries: |
|
276 try: |
|
277 response = urllib2.urlopen(req, timeout=self.timeout) |
|
278 except urllib2.HTTPError as err: |
|
279 LOG.error('REST Not Available: %s' % err.code) |
|
280 if err.code == httplib.SERVICE_UNAVAILABLE and \ |
|
281 retry < maxreqretries: |
|
282 retry += 1 |
|
283 time.sleep(1) |
|
284 LOG.error('Server Busy retry request: %s' % retry) |
|
285 continue |
|
286 if (err.code == httplib.UNAUTHORIZED or |
|
287 err.code == httplib.INTERNAL_SERVER_ERROR) and \ |
|
288 '/access/v1' not in zfssaurl: |
|
289 try: |
|
290 LOG.error('Authorizing request retry: %s, %s' % |
|
291 (zfssaurl, retry)) |
|
292 self.authorize() |
|
293 req.add_header('x-auth-session', |
|
294 self.headers['x-auth-session']) |
|
295 except RestClientError: |
|
296 pass |
|
297 retry += 1 |
|
298 time.sleep(1) |
|
299 continue |
|
300 |
|
301 return RestResult(err=err) |
|
302 |
|
303 except urllib2.URLError as err: |
|
304 LOG.error('URLError: %s' % err.reason) |
|
305 raise RestClientError(-1, name="ERR_URLError", |
|
306 message=err.reason) |
|
307 |
|
308 break |
|
309 |
|
310 if response and response.getcode() == httplib.SERVICE_UNAVAILABLE and \ |
|
311 retry >= maxreqretries: |
|
312 raise RestClientError(response.getcode(), name="ERR_HTTPError", |
|
313 message="REST Not Available: Disabled") |
|
314 |
|
315 return RestResult(response=response) |
|
316 |
|
317 def get(self, path, **kwargs): |
|
318 """ |
|
319 Make an HTTP GET request |
|
320 |
|
321 :param path: Path to resource. |
|
322 """ |
|
323 return self.request(path, "GET", **kwargs) |
|
324 |
|
325 def post(self, path, body="", **kwargs): |
|
326 """Make an HTTP POST request |
|
327 |
|
328 :param path: Path to resource. |
|
329 :param body: Post data content |
|
330 """ |
|
331 return self.request(path, "POST", body, **kwargs) |
|
332 |
|
333 def put(self, path, body="", **kwargs): |
|
334 """Make an HTTP PUT request |
|
335 |
|
336 :param path: Path to resource. |
|
337 :param body: Put data content |
|
338 """ |
|
339 return self.request(path, "PUT", body, **kwargs) |
|
340 |
|
341 def delete(self, path, **kwargs): |
|
342 """Make an HTTP DELETE request |
|
343 |
|
344 :param path: Path to resource that will be deleted. |
|
345 """ |
|
346 return self.request(path, "DELETE", **kwargs) |
|
347 |
|
348 def head(self, path, **kwargs): |
|
349 """Make an HTTP HEAD request |
|
350 |
|
351 :param path: Path to resource. |
|
352 """ |
|
353 return self.request(path, "HEAD", **kwargs) |