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

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""" 

20 

21from http import client as http_client 

22import io 

23import time 

24from urllib import error as urlerror 

25from urllib import request as urlrequest 

26 

27from oslo_serialization import jsonutils 

28 

29 

30def log_debug_msg(obj, message): 

31 if obj.log_function: 

32 obj.log_function(message) 

33 

34 

35class Status(object): 

36 """Result HTTP Status.""" 

37 

38 #: Request return OK 

39 OK = http_client.OK # pylint: disable=invalid-name 

40 

41 #: New resource created successfully 

42 CREATED = http_client.CREATED 

43 

44 #: Command accepted 

45 ACCEPTED = http_client.ACCEPTED 

46 

47 #: Command returned OK but no data will be returned 

48 NO_CONTENT = http_client.NO_CONTENT 

49 

50 #: Bad Request 

51 BAD_REQUEST = http_client.BAD_REQUEST 

52 

53 #: User is not authorized 

54 UNAUTHORIZED = http_client.UNAUTHORIZED 

55 

56 #: The request is not allowed 

57 FORBIDDEN = http_client.FORBIDDEN 

58 

59 #: The requested resource was not found 

60 NOT_FOUND = http_client.NOT_FOUND 

61 

62 #: The request is not allowed 

63 NOT_ALLOWED = http_client.METHOD_NOT_ALLOWED 

64 

65 #: Request timed out 

66 TIMEOUT = http_client.REQUEST_TIMEOUT 

67 

68 #: Invalid request 

69 CONFLICT = http_client.CONFLICT 

70 

71 #: Service Unavailable 

72 BUSY = http_client.SERVICE_UNAVAILABLE 

73 

74 

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. 

79 

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() 

95 

96 if self.error: 

97 self.status = self.error.code 

98 self.data = http_client.responses[self.status] 

99 

100 log_debug_msg(self, 'Response code: %s' % self.status) 

101 log_debug_msg(self, 'Response data: %s' % self.data) 

102 

103 def get_header(self, name): 

104 """Get an HTTP header with the given name from the results. 

105 

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) 

113 

114 

115class RestClientError(Exception): 

116 """Exception for ZFS REST API client errors.""" 

117 def __init__(self, status, name="ERR_INTERNAL", message=None): 

118 

119 """Create a REST Response exception. 

120 

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] 

131 

132 def __str__(self): 

133 return "%d %s %s" % (self.code, self.name, self.msg) 

134 

135 

136class RestClientURL(object): # pylint: disable=R0902 

137 """ZFSSA urllib client.""" 

138 def __init__(self, url, logfunc=None, **kwargs): 

139 """Initialize a REST client. 

140 

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') 

155 

156 self.headers = {"content-type": "application/json"} 

157 self.do_logout = False 

158 self.auth_str = None 

159 

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 

172 

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'] 

178 

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')) 

188 

189 elif result.status == http_client.NOT_FOUND: 

190 raise RestClientError(result.status, name="ERR_RESTError", 

191 message=("REST Not Available:" 

192 "Please Upgrade")) 

193 

194 except RestClientError: 

195 del self.headers['authorization'] 

196 raise 

197 

198 def login(self, auth_str): 

199 """Login to an appliance using a user name and password. 

200 

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. 

206 

207 :param auth_str: Authorization string (base64). 

208 """ 

209 self.auth_str = auth_str 

210 self._authorize() 

211 

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 

219 

220 self.headers.clear() 

221 self.do_logout = False 

222 return result 

223 

224 def islogin(self): 

225 """return if client is login.""" 

226 return self.do_logout 

227 

228 @staticmethod 

229 def mkpath(*args, **kwargs): 

230 """Make a path?query string for making a REST request. 

231 

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() 

248 

249 # pylint: disable=R0912 

250 def request(self, path, request, body=None, **kwargs): 

251 """Make an HTTP request and return the results. 

252 

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") 

263 

264 if body: 

265 if isinstance(body, dict): 

266 body = str(jsonutils.dumps(body)) 

267 

268 if body and len(body): 

269 out_hdrs['content-length'] = len(body) 

270 

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 

277 

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) 

282 

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) 

292 

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 

317 

318 return RestResult(self.log_function, err=err) 

319 

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 

325 

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") 

331 

332 return RestResult(self.log_function, response=response) 

333 

334 def get(self, path, **kwargs): 

335 """Make an HTTP GET request. 

336 

337 :param path: Path to resource. 

338 """ 

339 return self.request(path, "GET", **kwargs) 

340 

341 def post(self, path, body="", **kwargs): 

342 """Make an HTTP POST request. 

343 

344 :param path: Path to resource. 

345 :param body: Post data content. 

346 """ 

347 return self.request(path, "POST", body, **kwargs) 

348 

349 def put(self, path, body="", **kwargs): 

350 """Make an HTTP PUT request. 

351 

352 :param path: Path to resource. 

353 :param body: Put data content. 

354 """ 

355 return self.request(path, "PUT", body, **kwargs) 

356 

357 def delete(self, path, **kwargs): 

358 """Make an HTTP DELETE request. 

359 

360 :param path: Path to resource that will be deleted. 

361 """ 

362 return self.request(path, "DELETE", **kwargs) 

363 

364 def head(self, path, **kwargs): 

365 """Make an HTTP HEAD request. 

366 

367 :param path: Path to resource. 

368 """ 

369 return self.request(path, "HEAD", **kwargs)