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
« 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.
17Contains classes required to issue REST API calls to Data ONTAP.
18"""
20import re
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
30from manila.share.drivers.netapp.dataontap.client import api
31from manila.share.drivers.netapp import utils
34LOG = log.getLogger(__name__)
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'
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)
70class RestNaServer(object):
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"
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
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
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
106 LOG.debug('Using REST with NetApp controller: %s', self._host)
108 def set_transport_type(self, transport_type):
109 """Set the transport type protocol for API.
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()
119 def get_transport_type(self):
120 """Get the transport type protocol."""
121 return self._protocol
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')
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
138 def set_ontap_version(self, ontap_version):
139 """Set the ONTAP version."""
140 self._ontap_version = ontap_version
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
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)
161 def get_port(self):
162 """Get the server communication port."""
163 return self._port
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')
172 def get_timeout(self):
173 """Gets the timeout in seconds if set."""
174 return self._timeout
176 def set_vserver(self, vserver):
177 """Set the vserver to use if tunneling gets enabled."""
178 self._vserver = vserver
180 def get_vserver(self):
181 """Get the vserver to use in tunneling."""
182 return self._vserver
184 def __str__(self):
185 """Gets a representation of the client."""
186 return "server: %s" % (self._host)
188 def _get_request_method(self, method, session):
189 """Returns the request method to be used in the REST call."""
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]
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
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'
211 def _build_session(self, headers):
212 """Builds a session in the client."""
213 self._session = requests.Session()
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)
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
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()
239 return headers
241 def _create_basic_auth_handler(self):
242 """Creates and returns a basic HTTP auth handler."""
243 return auth.HTTPBasicAuth(self._username, self._password)
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
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 {}
261 self._build_session(headers)
262 request_method = self._get_request_method(method, self._session)
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)
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)
290 code = response.status_code
291 res = jsonutils.loads(response.content) if response.content else {}
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)
301 return code, res
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)
312 if not response.get('error'):
313 return code, response
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)