Coverage for manila/share/drivers/netapp/dataontap/client/api.py: 66%

560 statements  

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

1# Copyright (c) 2014 Navneet Singh. All rights reserved. 

2# Copyright (c) 2014 Clinton Knight. All rights reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15""" 

16NetApp API for Data ONTAP and OnCommand DFM. 

17 

18Contains classes required to issue API calls to Data ONTAP and OnCommand DFM. 

19""" 

20 

21import copy 

22import re 

23 

24from lxml import etree 

25from oslo_log import log 

26from oslo_serialization import jsonutils 

27from oslo_utils import netutils 

28import requests 

29from requests.adapters import HTTPAdapter 

30from requests import auth 

31from requests.packages.urllib3.util.retry import Retry 

32 

33from manila import exception 

34from manila.i18n import _ 

35from manila.share.drivers.netapp.dataontap.client import rest_endpoints 

36from manila.share.drivers.netapp import utils 

37 

38LOG = log.getLogger(__name__) 

39 

40EONTAPI_EINVAL = '22' 

41EVOLOPNOTSUPP = '160' 

42EAPIERROR = '13001' 

43EAPINOTFOUND = '13005' 

44ESNAPSHOTNOTALLOWED = '13023' 

45EVOLUMEDOESNOTEXIST = '13040' 

46EVOLUMEOFFLINE = '13042' 

47EINTERNALERROR = '13114' 

48EINVALIDINPUTERROR = '13115' 

49EDUPLICATEENTRY = '13130' 

50EVOLUMENOTONLINE = '13157' 

51EVOLNOTCLONE = '13170' 

52EVOLOPNOTUNDERWAY = '13171' 

53EVOLMOVE_CANNOT_MOVE_TO_CFO = '13633' 

54EAGGRDOESNOTEXIST = '14420' 

55EVOL_NOT_MOUNTED = '14716' 

56EVSERVERALREADYSTARTED = '14923' 

57ESIS_CLONE_NOT_LICENSED = '14956' 

58EOBJECTNOTFOUND = '15661' 

59EVSERVERNOTFOUND = '15698' 

60EVOLDEL_NOT_ALLOW_BY_CLONE = '15894' 

61E_VIFMGR_PORT_ALREADY_ASSIGNED_TO_BROADCAST_DOMAIN = '18605' 

62EPARENTNOTONLINE = '17003' 

63ERELATION_EXISTS = '17122' 

64ENOTRANSFER_IN_PROGRESS = '17130' 

65ETRANSFER_IN_PROGRESS = '17137' 

66EANOTHER_OP_ACTIVE = '17131' 

67ERELATION_NOT_QUIESCED = '17127' 

68ESOURCE_IS_DIFFERENT = '17105' 

69EVOL_CLONE_BEING_SPLIT = '17151' 

70EPOLICYNOTFOUND = '18251' 

71EEVENTNOTFOUND = '18253' 

72ESCOPENOTFOUND = '18259' 

73ESVMDR_CANNOT_PERFORM_OP_FOR_STATUS = '18815' 

74OPERATION_ALREADY_ENABLED = '40043' 

75ENFS_V4_0_ENABLED_MIGRATION_FAILURE = '13172940' 

76EVSERVER_MIGRATION_TO_NON_AFF_CLUSTER = '13172984' 

77 

78STYLE_LOGIN_PASSWORD = 'basic_auth' 

79TRANSPORT_TYPE_HTTP = 'http' 

80TRANSPORT_TYPE_HTTPS = 'https' 

81STYLE_CERTIFICATE = 'certificate_auth' 

82 

83 

84class BaseClient(object): 

85 """Encapsulates server connection logic.""" 

86 

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

88 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, 

89 username=None, password=None, port=None, 

90 trace=False, api_trace_pattern=None, private_key_file=None, 

91 certificate_file=None, ca_certificate_file=None, 

92 certificate_host_validation=False): 

93 super(BaseClient, self).__init__() 

94 self._host = host 

95 if private_key_file and certificate_file: 

96 transport_type = TRANSPORT_TYPE_HTTPS 

97 style = STYLE_CERTIFICATE 

98 self.set_transport_type(transport_type) 

99 self.set_style(style) 

100 if port: 

101 self.set_port(port) 

102 self._username = username 

103 self._password = password 

104 self._trace = trace 

105 self._api_trace_pattern = api_trace_pattern 

106 self._refresh_conn = True 

107 if ssl_cert_path is not None: 

108 self._ssl_verify = ssl_cert_path 

109 else: 

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

111 # given by certifi package. 

112 self._ssl_verify = True 

113 self._private_key_file = private_key_file 

114 self._certificate_file = certificate_file 

115 self._ca_certificate_file = ca_certificate_file 

116 self._certificate_host_validation = certificate_host_validation 

117 LOG.debug('Using NetApp controller: %s', self._host) 

118 

119 def get_style(self): 

120 """Get the authorization style for communicating with the server.""" 

121 return self._auth_style 

122 

123 def set_style(self, style): 

124 """Set the authorization style for communicating with the server. 

125 

126 Supports basic_auth for now. Certificate_auth mode to be done. 

127 """ 

128 if style.lower() not in (STYLE_LOGIN_PASSWORD, STYLE_CERTIFICATE): 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true

129 raise ValueError('Unsupported authentication style') 

130 self._auth_style = style.lower() 

131 

132 def get_transport_type(self): 

133 """Get the transport type protocol.""" 

134 return self._protocol 

135 

136 def set_transport_type(self, transport_type): 

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

138 

139 Supports http and https transport types. 

140 """ 

141 if transport_type.lower() not in ( 141 ↛ 143line 141 didn't jump to line 143 because the condition on line 141 was never true

142 TRANSPORT_TYPE_HTTP, TRANSPORT_TYPE_HTTPS): 

143 raise ValueError('Unsupported transport type') 

144 self._protocol = transport_type.lower() 

145 self._refresh_conn = True 

146 

147 def get_server_type(self): 

148 """Get the server type.""" 

149 return self._server_type 

150 

151 def set_server_type(self, server_type): 

152 """Set the target server type. 

153 

154 Supports filer and dfm server types. 

155 """ 

156 raise NotImplementedError() 

157 

158 def set_api_version(self, major, minor): 

159 """Set the API version.""" 

160 try: 

161 self._api_major_version = int(major) 

162 self._api_minor_version = int(minor) 

163 self._api_version = (str(major) + "." + 

164 str(minor)) 

165 except ValueError: 

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

167 self._refresh_conn = True 

168 

169 def set_system_version(self, system_version): 

170 """Set the ONTAP system version.""" 

171 self._system_version = system_version 

172 self._refresh_conn = True 

173 

174 def get_api_version(self): 

175 """Gets the API version tuple.""" 

176 if hasattr(self, '_api_version'): 

177 return (self._api_major_version, self._api_minor_version) 

178 return None 

179 

180 def get_system_version(self): 

181 """Gets the ONTAP system version.""" 

182 if hasattr(self, '_system_version'): 

183 return self._system_version 

184 return None 

185 

186 def set_port(self, port): 

187 """Set the server communication port.""" 

188 try: 

189 int(port) 

190 except ValueError: 

191 raise ValueError('Port must be integer') 

192 self._port = str(port) 

193 self._refresh_conn = True 

194 

195 def get_port(self): 

196 """Get the server communication port.""" 

197 return self._port 

198 

199 def set_timeout(self, seconds): 

200 """Sets the timeout in seconds.""" 

201 try: 

202 self._timeout = int(seconds) 

203 except ValueError: 

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

205 

206 def get_timeout(self): 

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

208 if hasattr(self, '_timeout'): 

209 return self._timeout 

210 return None 

211 

212 def get_vserver(self): 

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

214 return self._vserver 

215 

216 def set_vserver(self, vserver): 

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

218 self._vserver = vserver 

219 

220 def set_username(self, username): 

221 """Set the user name for authentication.""" 

222 self._username = username 

223 self._refresh_conn = True 

224 

225 def set_password(self, password): 

226 """Set the password for authentication.""" 

227 self._password = password 

228 self._refresh_conn = True 

229 

230 def invoke_successfully(self, na_element, api_args=None, 

231 enable_tunneling=False, use_zapi=True): 

232 """Invokes API and checks execution status as success. 

233 

234 Need to set enable_tunneling to True explicitly to achieve it. 

235 This helps to use same connection instance to enable or disable 

236 tunneling. The vserver or vfiler should be set before this call 

237 otherwise tunneling remains disabled. 

238 """ 

239 pass 

240 

241 def _build_session(self): 

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

243 self._session = requests.Session() 

244 

245 max_retries = Retry(total=5, connect=5, read=2, backoff_factor=1) 

246 adapter = HTTPAdapter(max_retries=max_retries) 

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

248 

249 if self._auth_style == STYLE_CERTIFICATE: 

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

251 self._create_certificate_auth_handler()) 

252 else: 

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

254 self._session.verify = self._ssl_verify 

255 headers = self._build_headers() 

256 

257 self._session.headers = headers 

258 

259 def _build_headers(self): 

260 """Adds the necessary headers to the session.""" 

261 raise NotImplementedError() 

262 

263 def _create_basic_auth_handler(self): 

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

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

266 

267 def _create_certificate_auth_handler(self): 

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

269 self._session.verify = self._certificate_host_validation 

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

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

272 self._private_key_file) 

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

274 # when we have host validation set to true 

275 if self._certificate_host_validation and self._ca_certificate_file: 

276 self._session.verify = self._ca_certificate_file 

277 return self._session.cert, self._session.verify 

278 

279 def __str__(self): 

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

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

282 

283 

284class ZapiClient(BaseClient): 

285 

286 SERVER_TYPE_FILER = 'filer' 

287 SERVER_TYPE_DFM = 'dfm' 

288 URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer' 

289 URL_DFM = 'apis/XMLrequest' 

290 NETAPP_NS = 'http://www.netapp.com/filer/admin' 

291 

292 def __init__(self, host, server_type=SERVER_TYPE_FILER, 

293 transport_type=TRANSPORT_TYPE_HTTP, 

294 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None, 

295 password=None, port=None, trace=False, 

296 api_trace_pattern=utils.API_TRACE_PATTERN, 

297 private_key_file=None, 

298 certificate_file=None, ca_certificate_file=None, 

299 certificate_host_validation=None): 

300 super(ZapiClient, self).__init__( 

301 host, transport_type=transport_type, style=style, 

302 ssl_cert_path=ssl_cert_path, username=username, password=password, 

303 port=port, trace=trace, api_trace_pattern=api_trace_pattern, 

304 private_key_file=private_key_file, 

305 certificate_file=certificate_file, 

306 ca_certificate_file=ca_certificate_file, 

307 certificate_host_validation=certificate_host_validation) 

308 self.set_server_type(server_type) 

309 if port is None: 

310 # Not yet set in parent, use defaults 

311 self._set_port() 

312 

313 def _set_port(self): 

314 """Defines which port will be used to communicate with ONTAP.""" 

315 if self._protocol == TRANSPORT_TYPE_HTTP: 315 ↛ 321line 315 didn't jump to line 321 because the condition on line 315 was always true

316 if self._server_type == ZapiClient.SERVER_TYPE_FILER: 316 ↛ 319line 316 didn't jump to line 319 because the condition on line 316 was always true

317 self.set_port(80) 

318 else: 

319 self.set_port(8088) 

320 else: 

321 if self._server_type == ZapiClient.SERVER_TYPE_FILER: 

322 self.set_port(443) 

323 else: 

324 self.set_port(8488) 

325 

326 def set_server_type(self, server_type): 

327 """Set the target server type. 

328 

329 Supports filer and dfm server types. 

330 """ 

331 if server_type.lower() not in (ZapiClient.SERVER_TYPE_FILER, 331 ↛ 333line 331 didn't jump to line 333 because the condition on line 331 was never true

332 ZapiClient.SERVER_TYPE_DFM): 

333 raise ValueError('Unsupported server type') 

334 self._server_type = server_type.lower() 

335 if self._server_type == ZapiClient.SERVER_TYPE_FILER: 335 ↛ 338line 335 didn't jump to line 338 because the condition on line 335 was always true

336 self._url = ZapiClient.URL_FILER 

337 else: 

338 self._url = ZapiClient.URL_DFM 

339 self._ns = ZapiClient.NETAPP_NS 

340 self._refresh_conn = True 

341 

342 def get_vfiler(self): 

343 """Get the vfiler to use in tunneling.""" 

344 return self._vfiler 

345 

346 def set_vfiler(self, vfiler): 

347 """Set the vfiler to use if tunneling gets enabled.""" 

348 self._vfiler = vfiler 

349 

350 def invoke_elem(self, na_element, enable_tunneling=False): 

351 """Invoke the API on the server.""" 

352 if na_element and not isinstance(na_element, NaElement): 

353 ValueError('NaElement must be supplied to invoke API') 

354 

355 request_element = self._create_request(na_element, enable_tunneling) 

356 request_d = request_element.to_string() 

357 

358 api_name = na_element.get_name() 

359 api_name_matches_regex = (re.match(self._api_trace_pattern, api_name) 

360 is not None) 

361 

362 if self._trace and api_name_matches_regex: 

363 LOG.debug("Request: %s", request_element.to_string(pretty=True)) 

364 

365 if (not hasattr(self, '_session') or not self._session 365 ↛ 368line 365 didn't jump to line 368 because the condition on line 365 was always true

366 or self._refresh_conn): 

367 self._build_session() 

368 try: 

369 if hasattr(self, '_timeout'): 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true

370 if self._timeout is None: 

371 self._timeout = 10 

372 response = self._session.post( 

373 self._get_url(), data=request_d, timeout=self._timeout) 

374 else: 

375 response = self._session.post( 

376 self._get_url(), data=request_d) 

377 except requests.HTTPError as e: 

378 raise NaApiError(e.errno, e.strerror) 

379 except requests.URLRequired as e: 

380 raise exception.StorageCommunicationException(str(e)) 

381 except Exception as e: 

382 raise NaApiError(message=e) 

383 

384 response_xml = response.text 

385 response_element = self._get_result( 

386 bytes(bytearray(response_xml, encoding='utf-8'))) 

387 

388 if self._trace and api_name_matches_regex: 

389 LOG.debug("Response: %s", response_element.to_string(pretty=True)) 

390 

391 return response_element 

392 

393 def invoke_successfully(self, na_element, api_args=None, 

394 enable_tunneling=False, use_zapi=True): 

395 """Invokes API and checks execution status as success. 

396 

397 Need to set enable_tunneling to True explicitly to achieve it. 

398 This helps to use same connection instance to enable or disable 

399 tunneling. The vserver or vfiler should be set before this call 

400 otherwise tunneling remains disabled. 

401 """ 

402 if api_args: 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true

403 na_element.translate_struct(api_args) 

404 

405 result = self.invoke_elem( 

406 na_element, enable_tunneling=enable_tunneling) 

407 

408 if result.has_attr('status') and result.get_attr('status') == 'passed': 

409 return result 

410 code = (result.get_attr('errno') 

411 or result.get_child_content('errorno') 

412 or 'ESTATUSFAILED') 

413 if code == ESIS_CLONE_NOT_LICENSED: 

414 msg = 'Clone operation failed: FlexClone not licensed.' 

415 else: 

416 msg = (result.get_attr('reason') 

417 or result.get_child_content('reason') 

418 or 'Execution status is failed due to unknown reason') 

419 raise NaApiError(code, msg) 

420 

421 def _create_request(self, na_element, enable_tunneling=False): 

422 """Creates request in the desired format.""" 

423 netapp_elem = NaElement('netapp') 

424 netapp_elem.add_attr('xmlns', self._ns) 

425 if hasattr(self, '_api_version'): 

426 netapp_elem.add_attr('version', self._api_version) 

427 if enable_tunneling: 427 ↛ 428line 427 didn't jump to line 428 because the condition on line 427 was never true

428 self._enable_tunnel_request(netapp_elem) 

429 netapp_elem.add_child_elem(na_element) 

430 return netapp_elem 

431 

432 def _enable_tunnel_request(self, netapp_elem): 

433 """Enables vserver or vfiler tunneling.""" 

434 if hasattr(self, '_vfiler') and self._vfiler: 

435 if (hasattr(self, '_api_major_version') and 

436 hasattr(self, '_api_minor_version') and 

437 self._api_major_version >= 1 and 

438 self._api_minor_version >= 7): 

439 netapp_elem.add_attr('vfiler', self._vfiler) 

440 else: 

441 raise ValueError('ontapi version has to be atleast 1.7' 

442 ' to send request to vfiler') 

443 if hasattr(self, '_vserver') and self._vserver: 

444 if (hasattr(self, '_api_major_version') and 

445 hasattr(self, '_api_minor_version') and 

446 self._api_major_version >= 1 and 

447 self._api_minor_version >= 15): 

448 netapp_elem.add_attr('vfiler', self._vserver) 

449 else: 

450 raise ValueError('ontapi version has to be atleast 1.15' 

451 ' to send request to vserver') 

452 

453 @staticmethod 

454 def _parse_response(response): 

455 """Get the NaElement for the response.""" 

456 if not response: 

457 raise NaApiError('No response received') 

458 xml = etree.XML(response) 

459 return NaElement(xml) 

460 

461 def _get_result(self, response): 

462 """Gets the call result.""" 

463 processed_response = self._parse_response(response) 

464 return processed_response.get_child_by_name('results') 

465 

466 def _get_url(self): 

467 """Get the base url to send the request.""" 

468 host = netutils.escape_ipv6(self._host) 

469 return '%s://%s:%s/%s' % (self._protocol, host, self._port, self._url) 

470 

471 def _build_headers(self): 

472 """Build and return headers.""" 

473 return {'Content-Type': 'text/xml'} 

474 

475 

476class RestClient(BaseClient): 

477 

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

479 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None, 

480 password=None, port=None, trace=False, 

481 api_trace_pattern=utils.API_TRACE_PATTERN, 

482 private_key_file=None, certificate_file=None, 

483 ca_certificate_file=None, certificate_host_validation=False): 

484 super(RestClient, self).__init__( 

485 host, transport_type=transport_type, style=style, 

486 ssl_cert_path=ssl_cert_path, username=username, password=password, 

487 port=port, trace=trace, api_trace_pattern=api_trace_pattern, 

488 private_key_file=private_key_file, 

489 certificate_file=certificate_file, 

490 ca_certificate_file=ca_certificate_file, 

491 certificate_host_validation=certificate_host_validation) 

492 if port is None: 

493 # Not yet set in parent, use defaults 

494 self._set_port() 

495 

496 def _set_port(self): 

497 if self._protocol == TRANSPORT_TYPE_HTTP: 497 ↛ 500line 497 didn't jump to line 500 because the condition on line 497 was always true

498 self.set_port(80) 

499 else: 

500 self.set_port(443) 

501 

502 def _get_request_info(self, api_name, session): 

503 """Returns the request method and url to be used in the REST call.""" 

504 

505 request_methods = { 

506 'post': session.post, 

507 'get': session.get, 

508 'put': session.put, 

509 'delete': session.delete, 

510 'patch': session.patch, 

511 } 

512 rest_call = rest_endpoints.endpoints.get(api_name) 

513 return request_methods[rest_call['method']], rest_call['url'] 

514 

515 def _add_query_params_to_url(self, url, query): 

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

517 filters = "" 

518 for k, v in query.items(): 

519 filters += "%(key)s=%(value)s&" % {"key": k, "value": v} 

520 url += "?" + filters 

521 return url 

522 

523 def invoke_elem(self, na_element, api_args=None): 

524 """Invoke the API on the server.""" 

525 if na_element and not isinstance(na_element, NaElement): 

526 raise ValueError('NaElement must be supplied to invoke API') 

527 

528 api_name = na_element.get_name() 

529 api_name_matches_regex = (re.match(self._api_trace_pattern, api_name) 

530 is not None) 

531 data = api_args.get("body") if api_args else {} 

532 

533 if (not hasattr(self, '_session') or not self._session 533 ↛ 536line 533 didn't jump to line 536 because the condition on line 533 was always true

534 or self._refresh_conn): 

535 self._build_session() 

536 request_method, action_url = self._get_request_info( 

537 api_name, self._session) 

538 

539 url_params = api_args.get("url_params") if api_args else None 

540 if url_params: 540 ↛ 541line 540 didn't jump to line 541 because the condition on line 540 was never true

541 action_url = action_url % url_params 

542 

543 query = api_args.get("query") if api_args else None 

544 if query: 

545 action_url = self._add_query_params_to_url( 

546 action_url, api_args['query']) 

547 

548 url = self._get_base_url() + action_url 

549 data = jsonutils.dumps(data) if data else data 

550 

551 if self._trace and api_name_matches_regex: 

552 message = ("Request: %(method)s %(url)s. Request body " 

553 "%(body)s") % { 

554 "method": request_method, 

555 "url": action_url, 

556 "body": api_args.get("body") if api_args else {} 

557 } 

558 LOG.debug(message) 

559 

560 try: 

561 if hasattr(self, '_timeout'): 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true

562 response = request_method( 

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

564 else: 

565 response = request_method(url, data=data) 

566 except requests.HTTPError as e: 

567 raise NaApiError(e.errno, e.strerror) 

568 except requests.URLRequired as e: 

569 raise exception.StorageCommunicationException(str(e)) 

570 except Exception as e: 

571 raise NaApiError(message=e) 

572 

573 response = ( 

574 jsonutils.loads(response.content) if response.content else None) 

575 if self._trace and api_name_matches_regex: 

576 LOG.debug("Response: %s", response) 

577 

578 return response 

579 

580 def invoke_successfully(self, na_element, api_args=None, 

581 enable_tunneling=False, use_zapi=False): 

582 """Invokes API and checks execution status as success. 

583 

584 Need to set enable_tunneling to True explicitly to achieve it. 

585 This helps to use same connection instance to enable or disable 

586 tunneling. The vserver or vfiler should be set before this call 

587 otherwise tunneling remains disabled. 

588 """ 

589 result = self.invoke_elem(na_element, api_args=api_args) 

590 if not result.get('error'): 

591 return result 

592 result_error = result.get('error') 

593 code = (result_error.get('code') 

594 or 'ESTATUSFAILED') 

595 if code == ESIS_CLONE_NOT_LICENSED: 

596 msg = 'Clone operation failed: FlexClone not licensed.' 

597 else: 

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

599 or 'Execution status is failed due to unknown reason') 

600 raise NaApiError(code, msg) 

601 

602 def _get_base_url(self): 

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

604 host = netutils.escape_ipv6(self._host) 

605 return '%s://%s:%s/api/' % (self._protocol, host, self._port) 

606 

607 def _build_headers(self): 

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

609 headers = { 

610 "Accept": "application/json", 

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

612 } 

613 return headers 

614 

615 

616class NaServer(object): 

617 """Encapsulates server connection logic.""" 

618 

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

620 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None, 

621 password=None, port=None, trace=False, 

622 api_trace_pattern=utils.API_TRACE_PATTERN, 

623 private_key_file=None, certificate_file=None, 

624 ca_certificate_file=None, certificate_host_validation=False): 

625 self.zapi_client = ZapiClient( 

626 host, transport_type=transport_type, style=style, 

627 ssl_cert_path=ssl_cert_path, username=username, password=password, 

628 port=port, trace=trace, api_trace_pattern=api_trace_pattern, 

629 private_key_file=private_key_file, 

630 certificate_file=certificate_file, 

631 ca_certificate_file=ca_certificate_file, 

632 certificate_host_validation=certificate_host_validation) 

633 self.rest_client = RestClient( 

634 host, transport_type=transport_type, style=style, 

635 ssl_cert_path=ssl_cert_path, username=username, password=password, 

636 port=port, trace=trace, api_trace_pattern=api_trace_pattern, 

637 private_key_file=private_key_file, 

638 certificate_file=certificate_file, 

639 ca_certificate_file=ca_certificate_file, 

640 certificate_host_validation=certificate_host_validation) 

641 self._host = host 

642 

643 LOG.debug('Using NetApp controller: %s', self._host) 

644 

645 def get_transport_type(self, use_zapi_client=True): 

646 """Get the transport type protocol.""" 

647 return self.get_client(use_zapi=use_zapi_client).get_transport_type() 

648 

649 def set_transport_type(self, transport_type): 

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

651 

652 Supports http and https transport types. 

653 """ 

654 self.zapi_client.set_transport_type(transport_type) 

655 self.rest_client.set_transport_type(transport_type) 

656 

657 def get_style(self, use_zapi_client=True): 

658 """Get the authorization style for communicating with the server.""" 

659 return self.get_client(use_zapi=use_zapi_client).get_style() 

660 

661 def set_style(self, style): 

662 """Set the authorization style for communicating with the server. 

663 

664 Supports basic_auth for now. Certificate_auth mode to be done. 

665 """ 

666 self.zapi_client.set_style(style) 

667 self.rest_client.set_style(style) 

668 

669 def get_server_type(self, use_zapi_client=True): 

670 """Get the target server type.""" 

671 return self.get_client(use_zapi=use_zapi_client).get_server_type() 

672 

673 def set_server_type(self, server_type): 

674 """Set the target server type. 

675 

676 Supports filer and dfm server types. 

677 """ 

678 self.zapi_client.set_server_type(server_type) 

679 self.rest_client.set_server_type(server_type) 

680 

681 def set_api_version(self, major, minor): 

682 """Set the API version.""" 

683 self.zapi_client.set_api_version(major, minor) 

684 self.rest_client.set_api_version(1, 0) 

685 

686 def set_system_version(self, system_version): 

687 """Set the ONTAP system version.""" 

688 self.zapi_client.set_system_version(system_version) 

689 self.rest_client.set_system_version(system_version) 

690 

691 def get_api_version(self, use_zapi_client=True): 

692 """Gets the API version tuple.""" 

693 return self.get_client(use_zapi=use_zapi_client).get_api_version() 

694 

695 def get_system_version(self, use_zapi_client=True): 

696 """Gets the ONTAP system version.""" 

697 return self.get_client(use_zapi=use_zapi_client).get_system_version() 

698 

699 def set_port(self, port): 

700 """Set the server communication port.""" 

701 self.zapi_client.set_port(port) 

702 self.rest_client.set_port(port) 

703 

704 def get_port(self, use_zapi_client=True): 

705 """Get the server communication port.""" 

706 return self.get_client(use_zapi=use_zapi_client).get_port() 

707 

708 def set_timeout(self, seconds): 

709 """Sets the timeout in seconds.""" 

710 self.zapi_client.set_timeout(seconds) 

711 self.rest_client.set_timeout(seconds) 

712 

713 def get_timeout(self, use_zapi_client=True): 

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

715 return self.get_client(use_zapi=use_zapi_client).get_timeout() 

716 

717 def get_vfiler(self): 

718 """Get the vfiler to use in tunneling.""" 

719 return self.zapi_client.get_vfiler() 

720 

721 def set_vfiler(self, vfiler): 

722 """Set the vfiler to use if tunneling gets enabled.""" 

723 self.zapi_client.set_vfiler(vfiler) 

724 

725 def get_vserver(self, use_zapi_client=True): 

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

727 return self.get_client(use_zapi=use_zapi_client).get_vserver() 

728 

729 def set_vserver(self, vserver): 

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

731 self.zapi_client.set_vserver(vserver) 

732 self.rest_client.set_vserver(vserver) 

733 

734 def set_username(self, username): 

735 """Set the user name for authentication.""" 

736 self.zapi_client.set_username(username) 

737 self.rest_client.set_username(username) 

738 

739 def set_password(self, password): 

740 """Set the password for authentication.""" 

741 self.zapi_client.set_password(password) 

742 self.rest_client.set_password(password) 

743 

744 def get_client(self, use_zapi=True): 

745 """Chooses the client to be used in the request.""" 

746 if use_zapi: 746 ↛ 748line 746 didn't jump to line 748 because the condition on line 746 was always true

747 return self.zapi_client 

748 return self.rest_client 

749 

750 def invoke_successfully(self, na_element, api_args=None, 

751 enable_tunneling=False, use_zapi=True): 

752 """Invokes API and checks execution status as success. 

753 

754 Need to set enable_tunneling to True explicitly to achieve it. 

755 This helps to use same connection instance to enable or disable 

756 tunneling. The vserver or vfiler should be set before this call 

757 otherwise tunneling remains disabled. 

758 """ 

759 return self.get_client(use_zapi=use_zapi).invoke_successfully( 

760 na_element, api_args=api_args, enable_tunneling=enable_tunneling) 

761 

762 def __str__(self): 

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

764 

765 

766class NaElement(object): 

767 """Class wraps basic building block for NetApp API request.""" 

768 

769 def __init__(self, name): 

770 """Name of the element or etree.Element.""" 

771 if isinstance(name, etree._Element): 

772 self._element = name 

773 else: 

774 self._element = etree.Element(name) 

775 

776 def get_name(self): 

777 """Returns the tag name of the element.""" 

778 return self._element.tag 

779 

780 def set_content(self, text): 

781 """Set the text string for the element.""" 

782 self._element.text = text 

783 

784 def get_content(self): 

785 """Get the text for the element.""" 

786 return self._element.text 

787 

788 def add_attr(self, name, value): 

789 """Add the attribute to the element.""" 

790 self._element.set(name, value) 

791 

792 def add_attrs(self, **attrs): 

793 """Add multiple attributes to the element.""" 

794 for attr in attrs.keys(): 

795 self._element.set(attr, attrs.get(attr)) 

796 

797 def add_child_elem(self, na_element): 

798 """Add the child element to the element.""" 

799 if isinstance(na_element, NaElement): 

800 self._element.append(na_element._element) 

801 return 

802 raise ValueError(_("Can only add elements of type NaElement.")) 

803 

804 def get_child_by_name(self, name): 

805 """Get the child element by the tag name.""" 

806 for child in self._element.iterchildren(): 

807 if child.tag == name or etree.QName(child.tag).localname == name: 

808 return NaElement(child) 

809 return None 

810 

811 def get_child_content(self, name): 

812 """Get the content of the child.""" 

813 for child in self._element.iterchildren(): 

814 if child.tag == name or etree.QName(child.tag).localname == name: 

815 return child.text 

816 return None 

817 

818 def get_children(self): 

819 """Get the children for the element.""" 

820 return [NaElement(el) for el in self._element.iterchildren()] 

821 

822 def has_attr(self, name): 

823 """Checks whether element has attribute.""" 

824 attributes = self._element.attrib or {} 

825 return name in attributes.keys() 

826 

827 def get_attr(self, name): 

828 """Get the attribute with the given name.""" 

829 attributes = self._element.attrib or {} 

830 return attributes.get(name) 

831 

832 def get_attr_names(self): 

833 """Returns the list of attribute names.""" 

834 attributes = self._element.attrib or {} 

835 return attributes.keys() 

836 

837 def add_new_child(self, name, content, convert=False): 

838 """Add child with tag name and context. 

839 

840 Convert replaces entity refs to chars. 

841 """ 

842 child = NaElement(name) 

843 if convert: 843 ↛ 844line 843 didn't jump to line 844 because the condition on line 843 was never true

844 content = NaElement._convert_entity_refs(content) 

845 child.set_content(content) 

846 self.add_child_elem(child) 

847 

848 @staticmethod 

849 def _convert_entity_refs(text): 

850 """Converts entity refs to chars to handle etree auto conversions.""" 

851 text = text.replace("&lt;", "<") 

852 text = text.replace("&gt;", ">") 

853 return text 

854 

855 @staticmethod 

856 def create_node_with_children(node, **children): 

857 """Creates and returns named node with children.""" 

858 parent = NaElement(node) 

859 for child in children.keys(): 

860 parent.add_new_child(child, children.get(child, None)) 

861 return parent 

862 

863 def add_node_with_children(self, node, **children): 

864 """Creates named node with children.""" 

865 parent = NaElement.create_node_with_children(node, **children) 

866 self.add_child_elem(parent) 

867 

868 def to_string(self, pretty=False, method='xml', encoding='UTF-8'): 

869 """Prints the element to string.""" 

870 return etree.tostring(self._element, method=method, encoding=encoding, 

871 pretty_print=pretty) 

872 

873 def __getitem__(self, key): 

874 """Dict getter method for NaElement. 

875 

876 Returns NaElement list if present, 

877 text value in case no NaElement node 

878 children or attribute value if present. 

879 """ 

880 

881 child = self.get_child_by_name(key) 

882 if child: 

883 if child.get_children(): 

884 return child 

885 else: 

886 return child.get_content() 

887 elif self.has_attr(key): 

888 return self.get_attr(key) 

889 raise KeyError(_('No element by given name %s.') % (key)) 

890 

891 def __setitem__(self, key, value): 

892 """Dict setter method for NaElement. 

893 

894 Accepts dict, list, tuple, str, int, float and long as valid value. 

895 """ 

896 if key: 

897 if value: 

898 if isinstance(value, NaElement): 

899 child = NaElement(key) 

900 child.add_child_elem(value) 

901 self.add_child_elem(child) 

902 elif isinstance( 

903 value, 

904 (str, ) + (int, ) + (float, )): 

905 self.add_new_child(key, str(value)) 

906 elif isinstance(value, (list, tuple, dict)): 

907 child = NaElement(key) 

908 child.translate_struct(value) 

909 self.add_child_elem(child) 

910 else: 

911 raise TypeError(_('Not a valid value for NaElement.')) 

912 else: 

913 self.add_child_elem(NaElement(key)) 

914 else: 

915 raise KeyError(_('NaElement name cannot be null.')) 

916 

917 def translate_struct(self, data_struct): 

918 """Convert list, tuple, dict to NaElement and appends. 

919 

920 Example usage: 

921 1. 

922 <root> 

923 <elem1>vl1</elem1> 

924 <elem2>vl2</elem2> 

925 <elem3>vl3</elem3> 

926 </root> 

927 The above can be achieved by doing 

928 root = NaElement('root') 

929 root.translate_struct({'elem1': 'vl1', 'elem2': 'vl2', 

930 'elem3': 'vl3'}) 

931 2. 

932 <root> 

933 <elem1>vl1</elem1> 

934 <elem2>vl2</elem2> 

935 <elem1>vl3</elem1> 

936 </root> 

937 The above can be achieved by doing 

938 root = NaElement('root') 

939 root.translate_struct([{'elem1': 'vl1', 'elem2': 'vl2'}, 

940 {'elem1': 'vl3'}]) 

941 """ 

942 if isinstance(data_struct, (list, tuple)): 

943 for el in data_struct: 

944 if isinstance(el, (list, tuple, dict)): 

945 self.translate_struct(el) 

946 else: 

947 self.add_child_elem(NaElement(el)) 

948 elif isinstance(data_struct, dict): 

949 for k in data_struct.keys(): 

950 child = NaElement(k) 

951 if isinstance(data_struct[k], (dict, list, tuple)): 951 ↛ 952line 951 didn't jump to line 952 because the condition on line 951 was never true

952 child.translate_struct(data_struct[k]) 

953 else: 

954 if data_struct[k]: 954 ↛ 956line 954 didn't jump to line 956 because the condition on line 954 was always true

955 child.set_content(str(data_struct[k])) 

956 self.add_child_elem(child) 

957 else: 

958 raise ValueError(_('Type cannot be converted into NaElement.')) 

959 

960 

961class NaApiError(Exception): 

962 """Base exception class for NetApp API errors.""" 

963 

964 def __init__(self, code='unknown', message='unknown'): 

965 self.code = code 

966 self.message = message 

967 

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

969 return 'NetApp API failed. Reason - %s:%s' % (self.code, self.message) 

970 

971 

972def invoke_api(na_server, api_name, api_family='cm', query=None, 

973 des_result=None, additional_elems=None, 

974 is_iter=False, records=0, tag=None, 

975 timeout=0, tunnel=None): 

976 """Invokes any given API call to a NetApp server. 

977 

978 :param na_server: na_server instance 

979 :param api_name: API name string 

980 :param api_family: cm or 7m 

981 :param query: API query as dict 

982 :param des_result: desired result as dict 

983 :param additional_elems: dict other than query and des_result 

984 :param is_iter: is iterator API 

985 :param records: limit for records, 0 for infinite 

986 :param timeout: timeout seconds 

987 :param tunnel: tunnel entity, vserver or vfiler name 

988 """ 

989 record_step = 50 

990 if not (na_server or isinstance(na_server, NaServer)): 

991 msg = _("Requires an NaServer instance.") 

992 raise exception.InvalidInput(reason=msg) 

993 server = copy.copy(na_server) 

994 if api_family == 'cm': 

995 server.set_vserver(tunnel) 

996 else: 

997 server.set_vfiler(tunnel) 

998 if timeout > 0: 

999 server.set_timeout(timeout) 

1000 iter_records = 0 

1001 cond = True 

1002 while cond: 

1003 na_element = create_api_request( 

1004 api_name, query, des_result, additional_elems, 

1005 is_iter, record_step, tag) 

1006 result = server.invoke_successfully(na_element, True) 

1007 if is_iter: 

1008 if records > 0: 

1009 iter_records = iter_records + record_step 

1010 if iter_records >= records: 

1011 cond = False 

1012 tag_el = result.get_child_by_name('next-tag') 

1013 tag = tag_el.get_content() if tag_el else None 

1014 if not tag: 

1015 cond = False 

1016 else: 

1017 cond = False 

1018 yield result 

1019 

1020 

1021def create_api_request(api_name, query=None, des_result=None, 

1022 additional_elems=None, is_iter=False, 

1023 record_step=50, tag=None): 

1024 """Creates a NetApp API request. 

1025 

1026 :param api_name: API name string 

1027 :param query: API query as dict 

1028 :param des_result: desired result as dict 

1029 :param additional_elems: dict other than query and des_result 

1030 :param is_iter: is iterator API 

1031 :param record_step: records at a time for iter API 

1032 :param tag: next tag for iter API 

1033 """ 

1034 api_el = NaElement(api_name) 

1035 if query: 

1036 query_el = NaElement('query') 

1037 query_el.translate_struct(query) 

1038 api_el.add_child_elem(query_el) 

1039 if des_result: 

1040 res_el = NaElement('desired-attributes') 

1041 res_el.translate_struct(des_result) 

1042 api_el.add_child_elem(res_el) 

1043 if additional_elems: 

1044 api_el.translate_struct(additional_elems) 

1045 if is_iter: 

1046 api_el.add_new_child('max-records', str(record_step)) 

1047 if tag: 

1048 api_el.add_new_child('tag', tag, True) 

1049 return api_el