Coverage for manila/share/drivers/netapp/dataontap/client/rest_api.py: 94%

184 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2023 NetApp, Inc. 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""" 

15NetApp API for REST Data ONTAP. 

16 

17Contains classes required to issue REST API calls to Data ONTAP. 

18""" 

19 

20import re 

21 

22from oslo_log import log 

23from oslo_serialization import jsonutils 

24from oslo_utils import netutils 

25import requests 

26from requests.adapters import HTTPAdapter 

27from requests import auth 

28from urllib3.util import retry 

29 

30from manila.share.drivers.netapp.dataontap.client import api 

31from manila.share.drivers.netapp import utils 

32 

33 

34LOG = log.getLogger(__name__) 

35 

36EREST_DUPLICATE_ENTRY = '1' 

37EREST_ENTRY_NOT_FOUND = '4' 

38EREST_NOT_AUTHORIZED = '6' 

39EREST_SNAPMIRROR_INITIALIZING = '917536' 

40EREST_VSERVER_NOT_FOUND = '13434920' 

41EREST_ANOTHER_VOLUME_OPERATION = '13107406' 

42EREST_LICENSE_NOT_INSTALLED = '1115127' 

43EREST_SNAPSHOT_NOT_SPECIFIED = '1638515' 

44EREST_FPOLICY_MODIF_POLICY_DISABLED = '9765029' 

45EREST_POLICY_ALREADY_DISABLED = '9764907' 

46EREST_ERELATION_EXISTS = '6619637' 

47EREST_BREAK_SNAPMIRROR_FAILED = '13303808' 

48EREST_UPDATE_SNAPMIRROR_FAILED = '13303844' 

49EREST_SNAPMIRROR_NOT_INITIALIZED = '13303812' 

50EREST_DUPLICATE_ROUTE = '1966345' 

51EREST_FAIL_ADD_PORT_BROADCAST = '1967149' 

52EREST_KERBEROS_IS_ENABLED_DISABLED = '3276861' 

53EREST_INTERFACE_BOUND = '1376858' 

54EREST_PORT_IN_USE = '1966189' 

55EREST_NFS_V4_0_ENABLED_MIGRATION_FAILURE = '13172940' 

56EREST_VSERVER_MIGRATION_TO_NON_AFF_CLUSTER = '13172984' 

57EREST_UNMOUNT_FAILED_LOCK = '917536' 

58EREST_CANNOT_MODITY_OFFLINE_VOLUME = '917533' 

59EREST_CANNOT_MODITY_SPECIFIED_FIELD = '917628' 

60EREST_VOLDEL_NOT_ALLOW_BY_CLONE = '524615' 

61EREST_SNAPSHOT_NOT_FOUND = '542797' 

62 

63 

64class NaRetryableError(api.NaApiError): 

65 def __str__(self, *args, **kwargs): 

66 return 'NetApp API failed. Try again. Reason - %s:%s' % ( 

67 self.code, self.message) 

68 

69 

70class RestNaServer(object): 

71 

72 TRANSPORT_TYPE_HTTP = 'http' 

73 TRANSPORT_TYPE_HTTPS = 'https' 

74 HTTP_PORT = '80' 

75 HTTPS_PORT = '443' 

76 TUNNELING_HEADER_KEY = "X-Dot-SVM-Name" 

77 

78 def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP, 

79 ssl_cert_path=None, username=None, password=None, port=None, 

80 trace=False, api_trace_pattern=utils.API_TRACE_PATTERN, 

81 private_key_file=None, certificate_file=None, 

82 ca_certificate_file=None, certificate_host_validation=None): 

83 self._host = host 

84 if private_key_file and certificate_file: 

85 transport_type = RestNaServer.TRANSPORT_TYPE_HTTPS 

86 self.set_transport_type(transport_type) 

87 self.set_port(port=port) 

88 self._username = username 

89 self._password = password 

90 self._trace = trace 

91 self._api_trace_pattern = api_trace_pattern 

92 self._timeout = None 

93 

94 if ssl_cert_path is not None: 

95 self._ssl_verify = ssl_cert_path 

96 else: 

97 # Note(felipe_rodrigues): it will verify with the mozila CA roots, 

98 # given by certifi package. 

99 self._ssl_verify = True 

100 

101 self._private_key_file = private_key_file 

102 self._certificate_file = certificate_file 

103 self._ca_certificate_file = ca_certificate_file 

104 self._certificate_host_validation = certificate_host_validation 

105 

106 LOG.debug('Using REST with NetApp controller: %s', self._host) 

107 

108 def set_transport_type(self, transport_type): 

109 """Set the transport type protocol for API. 

110 

111 Supports http and https transport types. 

112 """ 

113 if transport_type is None or transport_type.lower() not in ( 

114 RestNaServer.TRANSPORT_TYPE_HTTP, 

115 RestNaServer.TRANSPORT_TYPE_HTTPS): 

116 raise ValueError('Unsupported transport type') 

117 self._protocol = transport_type.lower() 

118 

119 def get_transport_type(self): 

120 """Get the transport type protocol.""" 

121 return self._protocol 

122 

123 def set_api_version(self, major, minor): 

124 """Set the API version.""" 

125 try: 

126 self._api_major_version = int(major) 

127 self._api_minor_version = int(minor) 

128 self._api_version = str(major) + "." + str(minor) 

129 except ValueError: 

130 raise ValueError('Major and minor versions must be integers') 

131 

132 def get_api_version(self): 

133 """Gets the API version tuple.""" 

134 if hasattr(self, '_api_version'): 

135 return (self._api_major_version, self._api_minor_version) 

136 return None 

137 

138 def set_ontap_version(self, ontap_version): 

139 """Set the ONTAP version.""" 

140 self._ontap_version = ontap_version 

141 

142 def get_ontap_version(self): 

143 """Gets the ONTAP version.""" 

144 if hasattr(self, '_ontap_version'): 

145 return self._ontap_version 

146 return None 

147 

148 def set_port(self, port=None): 

149 """Set the ONTAP port, if not informed, set with default one.""" 

150 if port is None and self._protocol == RestNaServer.TRANSPORT_TYPE_HTTP: 

151 self._port = RestNaServer.HTTP_PORT 

152 elif port is None: 

153 self._port = RestNaServer.HTTPS_PORT 

154 else: 

155 try: 

156 int(port) 

157 except ValueError: 

158 raise ValueError('Port must be integer') 

159 self._port = str(port) 

160 

161 def get_port(self): 

162 """Get the server communication port.""" 

163 return self._port 

164 

165 def set_timeout(self, seconds): 

166 """Sets the timeout in seconds.""" 

167 try: 

168 self._timeout = int(seconds) 

169 except ValueError: 

170 raise ValueError('timeout in seconds must be integer') 

171 

172 def get_timeout(self): 

173 """Gets the timeout in seconds if set.""" 

174 return self._timeout 

175 

176 def set_vserver(self, vserver): 

177 """Set the vserver to use if tunneling gets enabled.""" 

178 self._vserver = vserver 

179 

180 def get_vserver(self): 

181 """Get the vserver to use in tunneling.""" 

182 return self._vserver 

183 

184 def __str__(self): 

185 """Gets a representation of the client.""" 

186 return "server: %s" % (self._host) 

187 

188 def _get_request_method(self, method, session): 

189 """Returns the request method to be used in the REST call.""" 

190 

191 request_methods = { 

192 'post': session.post, 

193 'get': session.get, 

194 'put': session.put, 

195 'delete': session.delete, 

196 'patch': session.patch, 

197 } 

198 return request_methods[method] 

199 

200 def _add_query_params_to_url(self, url, query): 

201 """Populates the URL with specified filters.""" 

202 filters = '&'.join([f"{k}={v}" for k, v in query.items()]) 

203 url += "?" + filters 

204 return url 

205 

206 def _get_base_url(self): 

207 """Get the base URL for REST requests.""" 

208 host = netutils.escape_ipv6(self._host) 

209 return f'{self._protocol}://{host}:{self._port}/api' 

210 

211 def _build_session(self, headers): 

212 """Builds a session in the client.""" 

213 self._session = requests.Session() 

214 

215 # NOTE(felipe_rodrigues): request resilient of temporary network 

216 # failures (like name resolution failure), retrying until 5 times. 

217 max_retries = retry.Retry(total=5, connect=5, read=2, backoff_factor=1) 

218 adapter = HTTPAdapter(max_retries=max_retries) 

219 self._session.mount('%s://' % self._protocol, adapter) 

220 

221 if self._private_key_file and self._certificate_file: 

222 self._session.cert, self._session.verify = ( 

223 self._create_certificate_auth_handler()) 

224 else: 

225 self._session.auth = self._create_basic_auth_handler() 

226 self._session.verify = self._ssl_verify 

227 self._session.headers = headers 

228 

229 def _build_headers(self, enable_tunneling): 

230 """Build and return headers for a REST request.""" 

231 headers = { 

232 "Accept": "application/json", 

233 "Content-Type": "application/json" 

234 } 

235 # enable tunneling only if vserver is set by upper layer 

236 if enable_tunneling and self.get_vserver: 

237 headers[RestNaServer.TUNNELING_HEADER_KEY] = self.get_vserver() 

238 

239 return headers 

240 

241 def _create_basic_auth_handler(self): 

242 """Creates and returns a basic HTTP auth handler.""" 

243 return auth.HTTPBasicAuth(self._username, self._password) 

244 

245 def _create_certificate_auth_handler(self): 

246 """Creates and returns a certificate auth handler.""" 

247 self._session.verify = self._certificate_host_validation 

248 if self._certificate_file and self._private_key_file: 248 ↛ 253line 248 didn't jump to line 253 because the condition on line 248 was always true

249 self._session.cert = (self._certificate_file, 

250 self._private_key_file) 

251 # Assigning _session.verify to ca cert file to validate the certs 

252 # when we have host validation set to true 

253 if self._certificate_host_validation and self._ca_certificate_file: 

254 self._session.verify = self._ca_certificate_file 

255 return self._session.cert, self._session.verify 

256 

257 def send_http_request(self, method, url, body, headers): 

258 """Invoke the API on the server.""" 

259 data = jsonutils.dumps(body) if body else {} 

260 

261 self._build_session(headers) 

262 request_method = self._get_request_method(method, self._session) 

263 

264 api_name_matches_regex = (re.match(self._api_trace_pattern, url) 

265 is not None) 

266 if self._trace and api_name_matches_regex: 266 ↛ 267line 266 didn't jump to line 267 because the condition on line 266 was never true

267 svm = headers.get(RestNaServer.TUNNELING_HEADER_KEY) 

268 message = ("Request: %(method)s Header=%(header)s %(url)s " 

269 "Body=%(body)s") 

270 msg_args = { 

271 "method": method.upper(), 

272 "url": url, 

273 "body": body, 

274 "header": ({RestNaServer.TUNNELING_HEADER_KEY: svm} 

275 if svm else {}), 

276 } 

277 LOG.debug(message, msg_args) 

278 

279 try: 

280 if self._timeout is not None: 

281 response = request_method( 

282 url, data=data, timeout=self._timeout) 

283 else: 

284 response = request_method(url, data=data) 

285 except requests.HTTPError as e: 

286 raise api.NaApiError(e.errno, e.strerror) 

287 except Exception as e: 

288 raise api.NaApiError(message=e) 

289 

290 code = response.status_code 

291 res = jsonutils.loads(response.content) if response.content else {} 

292 

293 if self._trace and api_name_matches_regex: 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true

294 message = "Response: %(code)s Body=%(body)s" 

295 msg_args = { 

296 "code": code, 

297 "body": res 

298 } 

299 LOG.debug(message, msg_args) 

300 

301 return code, res 

302 

303 def invoke_successfully(self, action_url, method, body=None, query=None, 

304 enable_tunneling=False): 

305 """Invokes REST API and checks execution status as success.""" 

306 headers = self._build_headers(enable_tunneling) 

307 if query: 

308 action_url = self._add_query_params_to_url(action_url, query) 

309 url = self._get_base_url() + action_url 

310 code, response = self.send_http_request(method, url, body, headers) 

311 

312 if not response.get('error'): 

313 return code, response 

314 

315 result_error = response.get('error') 

316 code = result_error.get('code') or 'ESTATUSFAILED' 

317 msg = (result_error.get('message') 

318 or 'Execution failed due to unknown reason') 

319 raise api.NaApiError(code, msg)