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