Coverage for libs/sm/mpathcount.py: 80%

159 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2026-07-10 07:59 +0000

1# Copyright (C) Citrix Systems Inc. 

2# 

3# This program is free software; you can redistribute it and/or modify 

4# it under the terms of the GNU Lesser General Public License as published 

5# by the Free Software Foundation; version 2.1 only. 

6# 

7# This program is distributed in the hope that it will be useful, 

8# but WITHOUT ANY WARRANTY; without even the implied warranty of 

9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

10# GNU Lesser General Public License for more details. 

11# 

12# You should have received a copy of the GNU Lesser General Public License 

13# along with this program; if not, write to the Free Software Foundation, Inc., 

14# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 

15 

16import os 

17import sys 

18import re 

19import json 

20import subprocess 

21 

22from sm.core import util 

23from sm.core import xs_errors 

24from sm.core import mpath_cli 

25 

26supported = ['iscsi', 'lvmoiscsi', 'lvmohba', 'gfs2'] 

27 

28LOCK_TYPE_HOST = "host" 

29LOCK_NS1 = "mpathcount1" 

30LOCK_NS2 = "mpathcount2" 

31 

32MAPPER_DIR = "/dev/mapper" 

33MPATHS_DIR = "/dev/shm" 

34MPATH_FILE_NAME = "/dev/shm/mpath_status" 

35match_bySCSIid = False 

36mpath_enabled = True 

37SCSIid = 'NOTSUPPLIED' 

38XAPI_HEALTH_CHECK = '/opt/xensource/libexec/xapi-health-check' 

39 

40cached_DM_maj = None 

41 

42def get_dm_major(): 

43 global cached_DM_maj 

44 if not cached_DM_maj: 

45 try: 

46 line = [x for x in open('/proc/devices').readlines() if x.endswith('device-mapper\n')] 

47 cached_DM_maj = int(line[0].split()[0]) 

48 except: 

49 pass 

50 return cached_DM_maj 

51 

52 

53def mpc_exit(session, code): 

54 if session is not None: 

55 try: 

56 session.xenapi.session.logout() 

57 except: 

58 pass 

59 sys.exit(code) 

60 

61 

62def match_host_id(s): 

63 regex = re.compile("^INSTALLATION_UUID") 

64 return regex.search(s, 0) 

65 

66 

67def get_localhost_uuid(): 

68 filename = '/etc/xensource-inventory' 

69 try: 

70 f = open(filename, 'r') 

71 except: 

72 raise xs_errors.XenError('EIO', \ 

73 opterr="Unable to open inventory file [%s]" % filename) 

74 domid = '' 

75 for line in filter(match_host_id, f.readlines()): 

76 domid = line.split("'")[1] 

77 return domid 

78 

79 

80def match_dmpLUN(s): 

81 regex = re.compile("[0-9]*:[0-9]*:[0-9]*:[0-9]*") 

82 return regex.search(s, 0) 

83 

84 

85def match_pathup(s): 

86 path_status = None 

87 match = re.match(r'.*\d+:\d+:\d+:\d+\s+\S+\s+\S+\s+\S+\s+(\S+)', s) 

88 if match: 88 ↛ 90line 88 didn't jump to line 90, because the condition on line 88 was never false

89 path_status = match.group(1) 

90 if path_status in ['faulty', 'shaky', 'failed']: 

91 return False 

92 return True 

93 

94 

95def _tostring(l): 

96 return str(l) 

97 

98 

99def get_path_count(SCSIid): 

100 count = 0 

101 total = 0 

102 lines = mpath_cli.get_topology(SCSIid) 

103 for line in filter(match_dmpLUN, lines): 

104 total += 1 

105 if match_pathup(line): 

106 count += 1 

107 return (count, total) 

108 

109 

110def get_root_dev_major(): 

111 buf = os.stat('/') 

112 devno = buf.st_dev 

113 return os.major(devno) 

114 

115 

116# @key: key to update 

117# @SCSIid: SCSI id of multipath map 

118# @entry: string representing previous value 

119# @remove: callback to remove key 

120# @add: callback to add key/value pair 

121# @mpath_status: map to record multipath status 

122def update_config(key, SCSIid, entry, remove, add, mpath_status=None): 

123 path = os.path.join(MAPPER_DIR, SCSIid) 

124 util.SMlog("MPATH: Updating entry for [%s], current: %s" % (SCSIid, entry)) 

125 if os.path.exists(path): 

126 count, total = get_path_count(SCSIid) 

127 max = 0 

128 if len(entry) != 0: 

129 try: 

130 p = entry.strip('[') 

131 p = p.strip(']') 

132 q = p.split(',') 

133 max = int(q[1]) 

134 except: 

135 pass 

136 if total > max: 136 ↛ 138line 136 didn't jump to line 138, because the condition on line 136 was never false

137 max = total 

138 newentry = [count, max] 

139 if str(newentry) != entry: 139 ↛ 145line 139 didn't jump to line 145, because the condition on line 139 was never false

140 remove('multipathed') 

141 remove(key) 

142 add('multipathed', 'true') 

143 add(key, str(newentry)) 

144 util.SMlog("MPATH: Set val: %s" % str(newentry)) 

145 if mpath_status != None: 145 ↛ 146line 145 didn't jump to line 146, because the condition on line 145 was never true

146 mpath_status.update({str(key): f"{count}/{max}"}) 

147 else: 

148 util.SMlog('MPATH: device %s gone' % (SCSIid)) 

149 remove('multipathed') 

150 remove(key) 

151 

152 

153def get_SCSIidlist(devconfig, sm_config): 

154 SCSIidlist = [] 

155 if 'SCSIid' in sm_config: 

156 SCSIidlist = sm_config['SCSIid'].split(',') 

157 elif 'SCSIid' in devconfig: 

158 SCSIidlist.append(devconfig['SCSIid']) 

159 elif 'provider' in devconfig and 'ScsiId' in devconfig: 

160 SCSIidlist.append(devconfig['ScsiId']) 

161 else: 

162 for key in sm_config: 162 ↛ 163line 162 didn't jump to line 163, because the loop on line 162 never started

163 if util._isSCSIid(key): 

164 SCSIidlist.append(re.sub("^scsi-", "", key)) 

165 return SCSIidlist 

166 

167 

168def check_root_disk(config, maps, remove, add): 

169 if get_root_dev_major() == get_dm_major(): 

170 # Ensure output headers are not in the list 

171 if 'name' in maps: 

172 maps.remove('name') 

173 # first map will always correspond to the root dev, dm-0 

174 assert(len(maps) > 0) 

175 i = maps[0] 

176 if (not match_bySCSIid) or i == SCSIid: 176 ↛ exitline 176 didn't return from function 'check_root_disk', because the condition on line 176 was never false

177 util.SMlog("Matched SCSIid %s, updating " \ 

178 " Host.other-config:mpath-boot " % i) 

179 key = "mpath-boot" 

180 if key not in config: 

181 update_config(key, i, "", remove, add) 

182 else: 

183 update_config(key, i, config[key], remove, add) 

184 

185 

186def check_devconfig(devconfig, sm_config, config, remove, add, mpath_status=None): 

187 SCSIidlist = get_SCSIidlist(devconfig, sm_config) 

188 if not len(SCSIidlist): 

189 return 

190 for i in SCSIidlist: 

191 if match_bySCSIid and i != SCSIid: 191 ↛ 192line 191 didn't jump to line 192, because the condition on line 191 was never true

192 continue 

193 util.SMlog("Matched SCSIid, updating %s" % i) 

194 key = "mpath-" + i 

195 if not mpath_enabled: 

196 remove(key) 

197 remove('multipathed') 

198 else: 

199 if key not in config: 

200 update_config(key, i, "", remove, add, mpath_status) 

201 else: 

202 update_config(key, i, config[key], remove, add, mpath_status) 

203 

204def check_xapi_is_enabled(): 

205 """Check XAPI health status""" 

206 def _run_command(command, timeout): 

207 try: 

208 process = subprocess.Popen( 

209 command, 

210 stdout=subprocess.PIPE, 

211 stderr=subprocess.PIPE, 

212 universal_newlines=True 

213 ) 

214 try: 

215 stdout, stderr = process.communicate(timeout=timeout) 

216 return process.returncode, stdout, stderr 

217 except subprocess.TimeoutExpired: 

218 process.kill() 

219 util.SMlog(f"Command execution timeout after {timeout}s: {' '.join(command)}") 

220 return -1, "", "Timeout" 

221 except Exception as e: 

222 util.SMlog(f"Error executing command: {e}") 

223 return -1, "", str(e) 

224 

225 returncode, _, stderr = _run_command([XAPI_HEALTH_CHECK], timeout=120) 

226 if returncode != 0: 

227 util.SMlog(f"XAPI health check failed: {stderr}") 

228 return returncode == 0