Coverage for manila/share/drivers/netapp/dataontap/protocols/nfs_cmode.py: 99%

102 statements  

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

1# Copyright (c) 2015 Clinton Knight. 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 cDOT NFS protocol helper class. 

16""" 

17 

18import uuid 

19 

20from oslo_log import log 

21from oslo_utils import netutils 

22 

23from manila.common import constants 

24from manila import exception 

25from manila.share.drivers.netapp.dataontap.protocols import base 

26from manila.share.drivers.netapp import utils as na_utils 

27 

28 

29LOG = log.getLogger(__name__) 

30 

31 

32class NetAppCmodeNFSHelper(base.NetAppBaseHelper): 

33 """NetApp cDOT NFS protocol helper class.""" 

34 

35 @staticmethod 

36 def _escaped_address(address): 

37 return netutils.escape_ipv6(address) 

38 

39 @na_utils.trace 

40 def create_share(self, share, share_name, 

41 clear_current_export_policy=True, 

42 ensure_share_already_exists=False, replica=False, 

43 is_flexgroup=False): 

44 """Ensures the share export policy is set correctly. 

45 

46 The export policy must have the same name as the share. If it matches, 

47 nothing is done. Otherwise, the possible scenarios: 

48 

49 1. policy as 'default': a new export policy is created. 

50 2. policy as any name: renames the assigned policy to match the name. 

51 

52 :param share: share entity. 

53 :param share_name: share name that must be the export policy name. 

54 :param clear_current_export_policy: set the policy to 'default' before 

55 the check. 

56 :param ensure_share_already_exists: ignored, CIFS only. 

57 :param replica: it is a replica volume (DP type). 

58 :param is_flexgroup: whether the share is a FlexGroup or not. 

59 """ 

60 

61 if clear_current_export_policy: 61 ↛ 63line 61 didn't jump to line 63 because the condition on line 61 was always true

62 self._client.clear_nfs_export_policy_for_volume(share_name) 

63 self._ensure_export_policy(share, share_name) 

64 

65 if is_flexgroup: 

66 volume_info = self._client.get_volume(share_name) 

67 export_path = volume_info['junction-path'] 

68 else: 

69 export_path = self._client.get_volume_junction_path(share_name) 

70 

71 # Return a callback that may be used for generating export paths 

72 # for this share. 

73 return (lambda export_address, export_path=export_path: 

74 ':'.join([self._escaped_address(export_address), 

75 export_path])) 

76 

77 @na_utils.trace 

78 @base.access_rules_synchronized 

79 def delete_share(self, share, share_name): 

80 """Deletes NFS share.""" 

81 LOG.debug('Deleting NFS export policy for share %s', share['id']) 

82 export_policy_name = self._get_export_policy_name(share) 

83 self._client.clear_nfs_export_policy_for_volume(share_name) 

84 self._client.soft_delete_nfs_export_policy(export_policy_name) 

85 

86 @na_utils.trace 

87 @base.access_rules_synchronized 

88 def update_access(self, share, share_name, rules): 

89 """Replaces the list of access rules known to the backend storage.""" 

90 

91 # Ensure rules are valid 

92 for rule in rules: 

93 self._validate_access_rule(rule) 

94 

95 # Sort rules by ascending network size 

96 new_rules = {rule['access_to']: rule['access_level'] for rule in rules} 

97 addresses = sorted(new_rules, reverse=True) 

98 

99 # Ensure current export policy has the name we expect 

100 self._ensure_export_policy(share, share_name) 

101 export_policy_name = self._get_export_policy_name(share) 

102 

103 # Make temp policy names so this non-atomic workflow remains resilient 

104 # across process interruptions. 

105 temp_new_export_policy_name = self._get_temp_export_policy_name() 

106 temp_old_export_policy_name = self._get_temp_export_policy_name() 

107 

108 # Create new export policy 

109 self._client.create_nfs_export_policy(temp_new_export_policy_name) 

110 

111 # Get authentication methods, based on Vserver configuration 

112 auth_methods = self._get_auth_methods() 

113 

114 metadata = share.get('metadata', None) 

115 all_squash = ( 

116 (metadata.get('all_squash', 'false').lower() == 'true') if 

117 metadata else False) 

118 

119 # Add new rules to new policy 

120 for address in addresses: 

121 readonly = self._is_readonly(new_rules[address]) 

122 if all_squash and not readonly: 

123 self._client.add_nfs_export_rule( 

124 temp_new_export_policy_name, address, False, ['none']) 

125 else: 

126 self._client.add_nfs_export_rule( 

127 temp_new_export_policy_name, address, 

128 self._is_readonly(new_rules[address]), auth_methods) 

129 

130 # Rename policy currently in force 

131 LOG.info('Renaming NFS export policy for share %(share)s to ' 

132 '%(policy)s.', 

133 {'share': share_name, 'policy': temp_old_export_policy_name}) 

134 self._client.rename_nfs_export_policy(export_policy_name, 

135 temp_old_export_policy_name) 

136 

137 # Switch share to the new policy 

138 LOG.info('Setting NFS export policy for share %(share)s to ' 

139 '%(policy)s.', 

140 {'share': share_name, 'policy': temp_new_export_policy_name}) 

141 self._client.set_nfs_export_policy_for_volume( 

142 share_name, temp_new_export_policy_name) 

143 

144 # Delete old policy 

145 self._client.soft_delete_nfs_export_policy(temp_old_export_policy_name) 

146 

147 # Rename new policy to its final name 

148 LOG.info('Renaming NFS export policy for share %(share)s to ' 

149 '%(policy)s.', 

150 {'share': share_name, 'policy': export_policy_name}) 

151 self._client.rename_nfs_export_policy(temp_new_export_policy_name, 

152 export_policy_name) 

153 

154 @na_utils.trace 

155 def _validate_access_rule(self, rule): 

156 """Checks whether access rule type and level are valid.""" 

157 

158 if rule['access_type'] != 'ip': 

159 msg = ("Clustered Data ONTAP supports only 'ip' type for share " 

160 "access rules with NFS protocol.") 

161 raise exception.InvalidShareAccess(reason=msg) 

162 

163 if rule['access_level'] not in constants.ACCESS_LEVELS: 

164 raise exception.InvalidShareAccessLevel(level=rule['access_level']) 

165 

166 @na_utils.trace 

167 def get_target(self, share): 

168 """Returns ID of target ONTAP device based on export location.""" 

169 return self._get_export_location(share)[0] 

170 

171 @na_utils.trace 

172 def get_share_name_for_share(self, share): 

173 """Returns the flexvol name that hosts a share.""" 

174 _, volume_junction_path = self._get_export_location(share) 

175 volume = self._client.get_volume_at_junction_path(volume_junction_path) 

176 return volume.get('name') if volume else None 

177 

178 @na_utils.trace 

179 def _get_export_location(self, share): 

180 """Returns IP address and export location of an NFS share.""" 

181 export_location = self._get_share_export_location(share) or ':' 

182 result = export_location.rsplit(':', 1) 

183 if len(result) != 2: 

184 return ['', ''] 

185 return result 

186 

187 @staticmethod 

188 def _get_temp_export_policy_name(): 

189 """Builds export policy name for an NFS share.""" 

190 return 'temp_' + str(uuid.uuid1()).replace('-', '_') 

191 

192 @staticmethod 

193 def _get_export_policy_name(share): 

194 """Builds export policy name for an NFS share.""" 

195 return 'policy_' + share['id'].replace('-', '_') 

196 

197 @na_utils.trace 

198 def _ensure_export_policy(self, share, share_name): 

199 """Ensures a flexvol/share has an export policy. 

200 

201 This method ensures a flexvol has an export policy with a name 

202 containing the share ID. For legacy reasons, this may not 

203 always be the case. 

204 """ 

205 expected_export_policy = self._get_export_policy_name(share) 

206 actual_export_policy = self._client.get_nfs_export_policy_for_volume( 

207 share_name) 

208 

209 if actual_export_policy == expected_export_policy: 

210 return 

211 elif actual_export_policy == 'default': 

212 self._client.create_nfs_export_policy(expected_export_policy) 

213 self._client.set_nfs_export_policy_for_volume( 

214 share_name, expected_export_policy) 

215 else: 

216 self._client.rename_nfs_export_policy(actual_export_policy, 

217 expected_export_policy) 

218 

219 @na_utils.trace 

220 def _get_auth_methods(self): 

221 """Returns authentication methods for export policy rules. 

222 

223 This method returns the authentication methods to be configure in an 

224 export policy rule, based on security services configuration set in 

225 the current Vserver. If Kerberos is enabled in vServer LIFs, the auth 

226 methods will be configure to support 'krb5', 'krb5i' and 'krb5p'. The 

227 default authentication method is 'sys' (AUTH_SYS). 

228 """ 

229 kerberos_enabled = self._client.is_kerberos_enabled() 

230 return ['krb5', 'krb5i', 'krb5p'] if kerberos_enabled else ['sys'] 

231 

232 @na_utils.trace 

233 def cleanup_demoted_replica(self, share, share_name): 

234 """Cleans up export NFS policy for a demoted replica.""" 

235 self.delete_share(share, share_name) 

236 return