Coverage for manila/share/drivers/zfssa/restclient.py: 17%
182 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
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"""
15ZFS Storage Appliance REST API Client Programmatic Interface
16TODO(diemtran): this module needs to be placed in a library common to OpenStack
17 services. When this happens, the file should be removed from Manila code
18 base and imported from the relevant library.
19"""
21from http import client as http_client
22import io
23import time
24from urllib import error as urlerror
25from urllib import request as urlrequest
27from oslo_serialization import jsonutils
30def log_debug_msg(obj, message):
31 if obj.log_function:
32 obj.log_function(message)
35class Status(object):
36 """Result HTTP Status."""
38 #: Request return OK
39 OK = http_client.OK # pylint: disable=invalid-name
41 #: New resource created successfully
42 CREATED = http_client.CREATED
44 #: Command accepted
45 ACCEPTED = http_client.ACCEPTED
47 #: Command returned OK but no data will be returned
48 NO_CONTENT = http_client.NO_CONTENT
50 #: Bad Request
51 BAD_REQUEST = http_client.BAD_REQUEST
53 #: User is not authorized
54 UNAUTHORIZED = http_client.UNAUTHORIZED
56 #: The request is not allowed
57 FORBIDDEN = http_client.FORBIDDEN
59 #: The requested resource was not found
60 NOT_FOUND = http_client.NOT_FOUND
62 #: The request is not allowed
63 NOT_ALLOWED = http_client.METHOD_NOT_ALLOWED
65 #: Request timed out
66 TIMEOUT = http_client.REQUEST_TIMEOUT
68 #: Invalid request
69 CONFLICT = http_client.CONFLICT
71 #: Service Unavailable
72 BUSY = http_client.SERVICE_UNAVAILABLE
75class RestResult(object):
76 """Result from a REST API operation."""
77 def __init__(self, logfunc=None, response=None, err=None):
78 """Initialize a RestResult containing the results from a REST call.
80 :param logfunc: debug log function.
81 :param response: HTTP response.
82 :param err: HTTP error.
83 """
84 self.response = response
85 self.log_function = logfunc
86 self.error = err
87 self.data = ""
88 self.status = 0
89 if self.response:
90 self.status = self.response.getcode()
91 result = self.response.read()
92 while result:
93 self.data += result
94 result = self.response.read()
96 if self.error:
97 self.status = self.error.code
98 self.data = http_client.responses[self.status]
100 log_debug_msg(self, 'Response code: %s' % self.status)
101 log_debug_msg(self, 'Response data: %s' % self.data)
103 def get_header(self, name):
104 """Get an HTTP header with the given name from the results.
106 :param name: HTTP header name.
107 :return: The header value or None if no value is found.
108 """
109 if self.response is None:
110 return None
111 info = self.response.info()
112 return info.getheader(name)
115class RestClientError(Exception):
116 """Exception for ZFS REST API client errors."""
117 def __init__(self, status, name="ERR_INTERNAL", message=None):
119 """Create a REST Response exception.
121 :param status: HTTP response status.
122 :param name: The name of the REST API error type.
123 :param message: Descriptive error message returned from REST call.
124 """
125 super(RestClientError, self).__init__(message)
126 self.code = status
127 self.name = name
128 self.msg = message
129 if status in http_client.responses:
130 self.msg = http_client.responses[status]
132 def __str__(self):
133 return "%d %s %s" % (self.code, self.name, self.msg)
136class RestClientURL(object): # pylint: disable=R0902
137 """ZFSSA urllib client."""
138 def __init__(self, url, logfunc=None, **kwargs):
139 """Initialize a REST client.
141 :param url: The ZFSSA REST API URL.
142 :key session: HTTP Cookie value of x-auth-session obtained from a
143 normal BUI login.
144 :key timeout: Time in seconds to wait for command to complete.
145 (Default is 60 seconds).
146 """
147 self.url = url
148 self.log_function = logfunc
149 self.local = kwargs.get("local", False)
150 self.base_path = kwargs.get("base_path", "/api")
151 self.timeout = kwargs.get("timeout", 60)
152 self.headers = None
153 if kwargs.get('session'):
154 self.headers['x-auth-session'] = kwargs.get('session')
156 self.headers = {"content-type": "application/json"}
157 self.do_logout = False
158 self.auth_str = None
160 def _path(self, path, base_path=None):
161 """Build rest url path."""
162 if path.startswith("http://") or path.startswith("https://"):
163 return path
164 if base_path is None:
165 base_path = self.base_path
166 if not path.startswith(base_path) and not (
167 self.local and ("/api" + path).startswith(base_path)):
168 path = "%s%s" % (base_path, path)
169 if self.local and path.startswith("/api"):
170 path = path[4:]
171 return self.url + path
173 def _authorize(self):
174 """Performs authorization setting x-auth-session."""
175 self.headers['authorization'] = 'Basic %s' % self.auth_str
176 if 'x-auth-session' in self.headers:
177 del self.headers['x-auth-session']
179 try:
180 result = self.post("/access/v1")
181 del self.headers['authorization']
182 if result.status == http_client.CREATED:
183 self.headers['x-auth-session'] = (
184 result.get_header('x-auth-session'))
185 self.do_logout = True
186 log_debug_msg(self, ('ZFSSA version: %s')
187 % result.get_header('x-zfssa-version'))
189 elif result.status == http_client.NOT_FOUND:
190 raise RestClientError(result.status, name="ERR_RESTError",
191 message=("REST Not Available:"
192 "Please Upgrade"))
194 except RestClientError:
195 del self.headers['authorization']
196 raise
198 def login(self, auth_str):
199 """Login to an appliance using a user name and password.
201 Start a session like what is done logging into the BUI. This is not a
202 requirement to run REST commands, since the protocol is stateless.
203 What is does is set up a cookie session so that some server side
204 caching can be done. If login is used remember to call logout when
205 finished.
207 :param auth_str: Authorization string (base64).
208 """
209 self.auth_str = auth_str
210 self._authorize()
212 def logout(self):
213 """Logout of an appliance."""
214 result = None
215 try:
216 result = self.delete("/access/v1", base_path="/api")
217 except RestClientError:
218 pass
220 self.headers.clear()
221 self.do_logout = False
222 return result
224 def islogin(self):
225 """return if client is login."""
226 return self.do_logout
228 @staticmethod
229 def mkpath(*args, **kwargs):
230 """Make a path?query string for making a REST request.
232 :cmd_params args: The path part.
233 :cmd_params kwargs: The query part.
234 """
235 buf = io.StringIO()
236 query = "?"
237 for arg in args:
238 buf.write("/")
239 buf.write(arg)
240 for k in kwargs:
241 buf.write(query)
242 if query == "?":
243 query = "&"
244 buf.write(k)
245 buf.write("=")
246 buf.write(kwargs[k])
247 return buf.getvalue()
249 # pylint: disable=R0912
250 def request(self, path, request, body=None, **kwargs):
251 """Make an HTTP request and return the results.
253 :param path: Path used with the initialized URL to make a request.
254 :param request: HTTP request type (GET, POST, PUT, DELETE).
255 :param body: HTTP body of request.
256 :key accept: Set HTTP 'Accept' header with this value.
257 :key base_path: Override the base_path for this request.
258 :key content: Set HTTP 'Content-Type' header with this value.
259 """
260 out_hdrs = dict.copy(self.headers)
261 if kwargs.get("accept"):
262 out_hdrs['accept'] = kwargs.get("accept")
264 if body:
265 if isinstance(body, dict):
266 body = str(jsonutils.dumps(body))
268 if body and len(body):
269 out_hdrs['content-length'] = len(body)
271 zfssaurl = self._path(path, kwargs.get("base_path"))
272 req = urlrequest.Request(zfssaurl, body, out_hdrs)
273 req.get_method = lambda: request
274 maxreqretries = kwargs.get("maxreqretries", 10)
275 retry = 0
276 response = None
278 log_debug_msg(self, 'Request: %s %s' % (request, zfssaurl))
279 log_debug_msg(self, 'Out headers: %s' % out_hdrs)
280 if body and body != '':
281 log_debug_msg(self, 'Body: %s' % body)
283 while retry < maxreqretries:
284 try:
285 response = urlrequest.urlopen(req, # nosec B310
286 timeout=self.timeout)
287 except urlerror.HTTPError as err:
288 if err.code == http_client.NOT_FOUND:
289 log_debug_msg(self, 'REST Not Found: %s' % err.code)
290 else:
291 log_debug_msg(self, ('REST Not Available: %s') % err.code)
293 if (err.code == http_client.SERVICE_UNAVAILABLE and
294 retry < maxreqretries):
295 retry += 1
296 time.sleep(1)
297 log_debug_msg(self, ('Server Busy retry request: %s')
298 % retry)
299 continue
300 if ((err.code == http_client.UNAUTHORIZED or
301 err.code == http_client.INTERNAL_SERVER_ERROR) and
302 '/access/v1' not in zfssaurl):
303 try:
304 log_debug_msg(self, ('Authorizing request: '
305 '%(zfssaurl)s '
306 'retry: %(retry)d .')
307 % {'zfssaurl': zfssaurl,
308 'retry': retry})
309 self._authorize()
310 req.add_header('x-auth-session',
311 self.headers['x-auth-session'])
312 except RestClientError:
313 log_debug_msg(self, ('Cannot authorize.'))
314 retry += 1
315 time.sleep(1)
316 continue
318 return RestResult(self.log_function, err=err)
320 except urlerror.URLError as err:
321 log_debug_msg(self, ('URLError: %s') % err.reason)
322 raise RestClientError(-1, name="ERR_URLError",
323 message=err.reason)
324 break
326 if ((response and
327 response.getcode() == http_client.SERVICE_UNAVAILABLE) and
328 retry >= maxreqretries):
329 raise RestClientError(response.getcode(), name="ERR_HTTPError",
330 message="REST Not Available: Disabled")
332 return RestResult(self.log_function, response=response)
334 def get(self, path, **kwargs):
335 """Make an HTTP GET request.
337 :param path: Path to resource.
338 """
339 return self.request(path, "GET", **kwargs)
341 def post(self, path, body="", **kwargs):
342 """Make an HTTP POST request.
344 :param path: Path to resource.
345 :param body: Post data content.
346 """
347 return self.request(path, "POST", body, **kwargs)
349 def put(self, path, body="", **kwargs):
350 """Make an HTTP PUT request.
352 :param path: Path to resource.
353 :param body: Put data content.
354 """
355 return self.request(path, "PUT", body, **kwargs)
357 def delete(self, path, **kwargs):
358 """Make an HTTP DELETE request.
360 :param path: Path to resource that will be deleted.
361 """
362 return self.request(path, "DELETE", **kwargs)
364 def head(self, path, **kwargs):
365 """Make an HTTP HEAD request.
367 :param path: Path to resource.
368 """
369 return self.request(path, "HEAD", **kwargs)