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
« 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
16import os
17import sys
18import re
19import json
20import subprocess
22from sm.core import util
23from sm.core import xs_errors
24from sm.core import mpath_cli
26supported = ['iscsi', 'lvmoiscsi', 'lvmohba', 'gfs2']
28LOCK_TYPE_HOST = "host"
29LOCK_NS1 = "mpathcount1"
30LOCK_NS2 = "mpathcount2"
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'
40cached_DM_maj = None
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
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)
62def match_host_id(s):
63 regex = re.compile("^INSTALLATION_UUID")
64 return regex.search(s, 0)
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
80def match_dmpLUN(s):
81 regex = re.compile("[0-9]*:[0-9]*:[0-9]*:[0-9]*")
82 return regex.search(s, 0)
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
95def _tostring(l):
96 return str(l)
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)
110def get_root_dev_major():
111 buf = os.stat('/')
112 devno = buf.st_dev
113 return os.major(devno)
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)
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
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)
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)
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)
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