Coverage for drivers/mpathcount.py : 61%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/python3
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18from sm_typing import Dict
20import util
21import os
22import sys
23import re
24import xs_errors
25import mpath_cli
26import json
27import subprocess
29supported = ['iscsi', 'lvmoiscsi', 'lvmohba', 'lvmofcoe', 'gfs2']
31LOCK_TYPE_HOST = "host"
32LOCK_NS1 = "mpathcount1"
33LOCK_NS2 = "mpathcount2"
35MAPPER_DIR = "/dev/mapper"
36MPATHS_DIR = "/dev/shm"
37MPATH_FILE_NAME = "/dev/shm/mpath_status"
38match_bySCSIid = False
39mpath_enabled = True
40SCSIid = 'NOTSUPPLIED'
41XAPI_HEALTH_CHECK = '/opt/xensource/libexec/xapi-health-check'
43cached_DM_maj = None
45def get_dm_major():
46 global cached_DM_maj
47 if not cached_DM_maj:
48 try:
49 line = [x for x in open('/proc/devices').readlines() if x.endswith('device-mapper\n')]
50 cached_DM_maj = int(line[0].split()[0])
51 except:
52 pass
53 return cached_DM_maj
56def match_host_id(s):
57 regex = re.compile("^INSTALLATION_UUID")
58 return regex.search(s, 0)
61def get_localhost_uuid():
62 filename = '/etc/xensource-inventory'
63 try:
64 f = open(filename, 'r')
65 except:
66 raise xs_errors.XenError('EIO', \
67 opterr="Unable to open inventory file [%s]" % filename)
68 domid = ''
69 for line in filter(match_host_id, f.readlines()):
70 domid = line.split("'")[1]
71 return domid
74def match_dmpLUN(s):
75 regex = re.compile("[0-9]*:[0-9]*:[0-9]*:[0-9]*")
76 return regex.search(s, 0)
79def match_pathup(s):
80 match = re.match(r'.*\d+:\d+:\d+:\d+\s+\S+\s+\S+\s+\S+\s+(\S+)', s)
81 if match: 81 ↛ 85line 81 didn't jump to line 85, because the condition on line 81 was never false
82 path_status = match.group(1)
83 if path_status in ['faulty', 'shaky', 'failed']:
84 return False
85 return True
88def _tostring(l):
89 return str(l)
92def get_path_count(SCSIid):
93 count = 0
94 total = 0
95 lines = mpath_cli.get_topology(SCSIid)
96 for line in filter(match_dmpLUN, lines):
97 total += 1
98 if match_pathup(line):
99 count += 1
100 return (count, total)
103def get_root_dev_major():
104 buf = os.stat('/')
105 devno = buf.st_dev
106 return os.major(devno)
109# @key: key to update
110# @SCSIid: SCSI id of multipath map
111# @entry: string representing previous value
112# @remove: callback to remove key
113# @add: callback to add key/value pair
114# @mpath_status: map to record multipath status
115def update_config(key, SCSIid, entry, remove, add, mpath_status=None):
116 path = os.path.join(MAPPER_DIR, SCSIid)
117 util.SMlog("MPATH: Updating entry for [%s], current: %s" % (SCSIid, entry))
118 if os.path.exists(path):
119 count, total = get_path_count(SCSIid)
120 max = 0
121 if len(entry) != 0:
122 try:
123 p = entry.strip('[')
124 p = p.strip(']')
125 q = p.split(',')
126 max = int(q[1])
127 except:
128 pass
129 if total > max: 129 ↛ 131line 129 didn't jump to line 131, because the condition on line 129 was never false
130 max = total
131 newentry = [count, max]
132 if str(newentry) != entry: 132 ↛ 138line 132 didn't jump to line 138, because the condition on line 132 was never false
133 remove('multipathed')
134 remove(key)
135 add('multipathed', 'true')
136 add(key, str(newentry))
137 util.SMlog("MPATH: Set val: %s" % str(newentry))
138 if mpath_status != None: 138 ↛ 139line 138 didn't jump to line 139, because the condition on line 138 was never true
139 mpath_status.update({str(key): f"{count}/{max}"})
140 else:
141 util.SMlog('MPATH: device %s gone' % (SCSIid))
142 remove('multipathed')
143 remove(key)
146def get_SCSIidlist(devconfig, sm_config):
147 SCSIidlist = []
148 if 'SCSIid' in sm_config:
149 SCSIidlist = sm_config['SCSIid'].split(',')
150 elif 'SCSIid' in devconfig:
151 SCSIidlist.append(devconfig['SCSIid'])
152 elif 'provider' in devconfig:
153 SCSIidlist.append(devconfig['ScsiId'])
154 else:
155 for key in sm_config: 155 ↛ 156line 155 didn't jump to line 156, because the loop on line 155 never started
156 if util._isSCSIid(key):
157 SCSIidlist.append(re.sub("^scsi-", "", key))
158 return SCSIidlist
161def check_root_disk(config, maps, remove, add):
162 if get_root_dev_major() == get_dm_major():
163 # Ensure output headers are not in the list
164 if 'name' in maps:
165 maps.remove('name')
166 # first map will always correspond to the root dev, dm-0
167 assert(len(maps) > 0)
168 i = maps[0]
169 if (not match_bySCSIid) or i == SCSIid: 169 ↛ exitline 169 didn't return from function 'check_root_disk', because the condition on line 169 was never false
170 util.SMlog("Matched SCSIid %s, updating " \
171 " Host.other-config:mpath-boot " % i)
172 key = "mpath-boot"
173 if key not in config:
174 update_config(key, i, "", remove, add)
175 else:
176 update_config(key, i, config[key], remove, add)
179def check_devconfig(devconfig, sm_config, config, remove, add, mpath_status=None):
180 SCSIidlist = get_SCSIidlist(devconfig, sm_config)
181 if not len(SCSIidlist):
182 return
183 for i in SCSIidlist:
184 if match_bySCSIid and i != SCSIid: 184 ↛ 185line 184 didn't jump to line 185, because the condition on line 184 was never true
185 continue
186 util.SMlog("Matched SCSIid, updating %s" % i)
187 key = "mpath-" + i
188 if not mpath_enabled:
189 remove(key)
190 remove('multipathed')
191 else:
192 if key not in config:
193 update_config(key, i, "", remove, add, mpath_status)
194 else:
195 update_config(key, i, config[key], remove, add, mpath_status)
197def check_xapi_is_enabled():
198 """Check XAPI health status"""
199 def _run_command(command, timeout):
200 try:
201 process = subprocess.Popen(
202 command,
203 stdout=subprocess.PIPE,
204 stderr=subprocess.PIPE,
205 universal_newlines=True
206 )
207 try:
208 stdout, stderr = process.communicate(timeout=timeout)
209 return process.returncode, stdout, stderr
210 except subprocess.TimeoutExpired:
211 process.kill()
212 util.SMlog(f"Command execution timeout after {timeout}s: {' '.join(command)}")
213 return -1, "", "Timeout"
214 except Exception as e:
215 util.SMlog(f"Error executing command: {e}")
216 return -1, "", str(e)
218 returncode, _, stderr = _run_command([XAPI_HEALTH_CHECK], timeout=120)
219 if returncode != 0:
220 util.SMlog(f"XAPI health check failed: {stderr}")
221 return returncode == 0
223def main(session):
224 global mpath_enabled
225 localhost = session.xenapi.host.get_by_uuid(get_localhost_uuid())
226 check_xapi_is_enabled()
227 # Check whether multipathing is enabled (either for root dev or SRs)
228 try:
229 if get_root_dev_major() != get_dm_major():
230 hconf = session.xenapi.host.get_other_config(localhost)
231 assert(hconf['multipathing'] == 'true')
232 mpath_enabled = True
233 except:
234 mpath_enabled = False
236 # Check root disk if multipathed
237 try:
238 def _remove(key):
239 session.xenapi.host.remove_from_other_config(localhost, key)
241 def _add(key, val):
242 session.xenapi.host.add_to_other_config(localhost, key, val)
243 config = session.xenapi.host.get_other_config(localhost)
244 maps = mpath_cli.list_maps()
245 check_root_disk(config, maps, _remove, _add)
247 except:
248 util.SMlog("MPATH: Failure updating Host.other-config:mpath-boot db")
249 sys.exit(-1)
251 try:
252 pbds = session.xenapi.PBD.get_all_records_where("field \"host\" = \"%s\"" % localhost)
253 except:
254 sys.exit(-1)
256 try:
257 mpath_status: Dict[str, str] = {}
258 for pbd in pbds:
259 def remove(key):
260 session.xenapi.PBD.remove_from_other_config(pbd, key)
262 def add(key, val):
263 session.xenapi.PBD.add_to_other_config(pbd, key, val)
264 record = pbds[pbd]
265 config = record['other_config']
266 SR = record['SR']
267 srtype = session.xenapi.SR.get_type(SR)
268 if srtype in supported:
269 devconfig = record["device_config"]
270 sm_config = session.xenapi.SR.get_sm_config(SR)
271 check_devconfig(devconfig, sm_config, config, remove, add, mpath_status)
272 mpath_status = mpath_status if mpath_enabled else {}
273 util.atomicFileWrite(MPATH_FILE_NAME, MPATHS_DIR, json.dumps(mpath_status))
274 os.chmod(MPATH_FILE_NAME, 0o0644)
275 except:
276 util.SMlog("MPATH: Failure updating db. %s" % str(sys.exc_info()))
277 sys.exit(-1)
279 util.SMlog("MPATH: Update done")
281if __name__ == '__main__': 281 ↛ 282line 281 didn't jump to line 282, because the condition on line 281 was never true
282 try:
283 with util.ApiSession("SM-mpathcount") as api_session:
284 main(api_session)
285 except xs_errors.XenError as e:
286 util.SMlog(f"MPATH: Failure `{e}`")
287 sys.exit(-1)
288 sys.exit(0)