Coverage for mfutil/misc.py: 59%

139 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-18 16:04 +0000

1"""Generic utility classes and functions.""" 

2 

3import uuid 

4import errno 

5import os 

6import logging 

7import tempfile 

8import socket 

9import psutil 

10import pickle 

11import hashlib 

12import datetime 

13import time 

14import fnmatch 

15 

16from inotify_simple import flags 

17from mfutil.exc import MFUtilException 

18from mfutil.eval import SandboxedEval 

19 

20__pdoc__ = { 

21 "add_inotify_watch": False 

22} 

23 

24 

25def __get_logger(): 

26 return logging.getLogger("mfutil") 

27 

28 

29def eval(expr, variables=None): 

30 """Evaluate (safely) a python expression (as a string). 

31 

32 The eval is done with simpleeval library. 

33 

34 Following functions are available (in expressions): 

35 

36 - re_match: see match() function of re module 

37 - re_imatch: insensitive match() function of re module 

38 - fnmatch.fnmatch: fnmatch() function of fnmatch module 

39 

40 Args: 

41 expr (string): the python expression to eval. 

42 variables (dict): if set, inject some variables/values 

43 in the expression. 

44 

45 """ 

46 

47 s = SandboxedEval(names=variables) 

48 return s.eval(expr) 

49 

50 

51def get_unique_hexa_identifier(): 

52 """Return an unique hexa identifier on 32 bytes. 

53 

54 The idenfier is made only with 0123456789abcdef 

55 characters. 

56 

57 Returns: 

58 (string) unique hexa identifier. 

59 

60 """ 

61 return str(uuid.uuid4()).replace('-', '') 

62 

63 

64def get_utc_unix_timestamp(): 

65 """Return the current unix UTC timestamp on all platforms. 

66 

67 It works even if the machine is configured in local time. 

68 

69 Returns: 

70 (int) a int corresponding to the current unix utc timestamp. 

71 

72 """ 

73 dts = datetime.datetime.utcnow() 

74 return int(time.mktime(dts.timetuple())) 

75 

76 

77def mkdir_p(path, nodebug=False, nowarning=False): 

78 """Make a directory recursively (clone of mkdir -p). 

79 

80 Thanks to http://stackoverflow.com/questions/600268/ 

81 mkdir-p-functionality-in-python . 

82 

83 Any exceptions are catched and a warning message 

84 is logged in case of problems. 

85 

86 If the directory already exists, True is returned 

87 with no debug or warning. 

88 

89 Args: 

90 path (string): complete path to create. 

91 nodebug (boolean): if True, no debug messages are logged. 

92 nowarning (boolean): if True, no message are logged in 

93 case of problems. 

94 

95 Returns: 

96 boolean: True if the directory exists at the end. 

97 

98 """ 

99 try: 

100 os.makedirs(path) 

101 except OSError as exc: 

102 if exc.errno == errno.EEXIST and os.path.isdir(path): 

103 return True 

104 else: 

105 if not nowarning: 

106 __get_logger().warning("can't create %s directory", path) 

107 return False 

108 #We do not log debug message because it is logged in circus configuration file  

109 #if not nodebug: 

110 # __get_logger().debug("%s directory created", path) 

111 return True 

112 

113 

114def mkdir_p_or_die(path, nodebug=False, exit_code=2): 

115 """Make a directory recursively (clone of mkdir -p). 

116 

117 If the directory already exists, True is returned 

118 with no debug or warning. 

119 

120 Any exceptions are catched. 

121 

122 In case of problems, the program dies here with corresponding 

123 exit_code. 

124 

125 Args: 

126 path (string): complete path to create. 

127 nodebug (boolean): if True, no debug messages are logged. 

128 exit_code (int): os._exit() exit code. 

129 

130 """ 

131 res = mkdir_p(path, nodebug=nodebug, nowarning=True) 

132 if not res: 

133 __get_logger().error("can't create %s directory", path) 

134 os._exit(exit_code) 

135 

136 

137def _get_temp_dir(tmp_dir=None): 

138 """Return system temp dir or used choosen temp dir. 

139 

140 If the user provides a tmp_dir argument, the 

141 directory is created (if necessary). 

142 

143 If the user don't provide a tmp_dir argument, 

144 the function returns a system temp dir. 

145 

146 If the directory is not good or can be created, 

147 an exception is raised. 

148 

149 Args: 

150 tmp_dir (string): user provided tmp directory (None 

151 to use the system temp dir). 

152 

153 Returns: 

154 (string) temp directory 

155 

156 Raises: 

157 MFUtilException if the temp directory is not good or can't 

158 be created. 

159 

160 """ 

161 if tmp_dir is None: 

162 tmp_dir = tempfile.gettempdir() 

163 res = mkdir_p(tmp_dir) 

164 if not res: 

165 raise MFUtilException("can't create temp_dir: %s", tmp_dir) 

166 return tmp_dir 

167 

168 

169def get_tmp_filepath(tmp_dir=None, prefix=""): 

170 """Return a tmp (complete) filepath. 

171 

172 The filename is made with get_unique_hexa_identifier() identifier 

173 so 32 hexa characters. 

174 

175 The dirname can be provided by the user (or be a system one). 

176 He will be created if necessary. An exception can be raised if any 

177 problems at this side. 

178 

179 Note: the file is not created or open at all. The function just 

180 returns a filename. 

181 

182 Args: 

183 tmp_dir (string): user provided tmp directory (None 

184 to use the system temp dir). 

185 prefix (string): you can add here a prefix for filenames 

186 (will be preprended before the 32 hexa characters). 

187 

188 Returns: 

189 (string) tmp (complete) filepath. 

190 

191 Raises: 

192 MFUtilException if the temp directory is not good or can't 

193 be created. 

194 

195 """ 

196 temp_dir = _get_temp_dir(tmp_dir) 

197 return os.path.join(temp_dir, prefix + get_unique_hexa_identifier()) 

198 

199 

200def create_tmp_dirpath(tmp_dir=None, prefix=""): 

201 """Create and return a temporary directory inside a father tempory directory. 

202 

203 The dirname is made with get_unique_hexa_identifier() identifier 

204 so 32 hexa characters. 

205 

206 The father dirname can be provided by the user (or be a system one). 

207 He will be created if necessary. An exception can be raised if any 

208 problems at this side. 

209 

210 Note: the temporary directory is created. 

211 

212 Args: 

213 tmp_dir (string): user provided tmp directory (None 

214 to use the system temp dir). 

215 prefix (string): you can add here a prefix for dirnames 

216 (will be preprended before the 32 hexa characters). 

217 

218 Returns: 

219 (string) complete path of a newly created temporary directory. 

220 

221 Raises: 

222 MFUtilException if the temp directory can't be created. 

223 

224 """ 

225 temp_dir = _get_temp_dir(tmp_dir) 

226 new_temp_dir = os.path.join(temp_dir, 

227 prefix + get_unique_hexa_identifier()) 

228 res = mkdir_p(new_temp_dir, nowarning=True) 

229 if not res: 

230 raise MFUtilException("can't create temp_dir: %s", new_temp_dir) 

231 return new_temp_dir 

232 

233 

234def get_ipv4_for_hostname(hostname, static_mappings={}): 

235 """Translate a host name to IPv4 address format. 

236 

237 The IPv4 address is returned as a string, such as '100.50.200.5'. 

238 If the host name is an IPv4 address itself it is returned unchanged. 

239 

240 You can provide a dictionnary with static mappings. 

241 Following mappings are added by default: 

242 '127.0.0.1' => '127.0.0.1' 

243 'localhost' => '127.0.0.1' 

244 'localhost.localdomain' => '127.0.0.1' 

245 

246 Args: 

247 hostname (string): hostname. 

248 static_mappings (dict): dictionnary of static mappings 

249 ((hostname) string: (ip) string). 

250 

251 Returns: 

252 (string) IPv4 address for the given hostname (None if any problem) 

253 

254 """ 

255 hostname = hostname.lower() 

256 static_mappings.update({'127.0.0.1': '127.0.0.1', 'localhost': '127.0.0.1', 

257 'localhost.localdomain': '127.0.0.1'}) 

258 if hostname in static_mappings: 

259 return static_mappings[hostname] 

260 try: 

261 return socket.gethostbyname(hostname) 

262 except Exception: 

263 return None 

264 

265 

266def get_recursive_mtime(directory, ignores=[]): 

267 """Get the latest mtime recursivly on a directory. 

268 

269 Args: 

270 directory (string): complete path of a directory to scan. 

271 ignores (list of strings): list of shell-style wildcards 

272 to define which filenames/dirnames to ignores (see fnmatch). 

273 

274 Returns: 

275 (int) timestamp of the latest mtime on the directory. 

276 

277 """ 

278 result = 0 

279 for name in os.listdir(directory): 

280 ignored = False 

281 for ssw in ignores: 

282 if fnmatch.fnmatch(name, ssw): 

283 ignored = True 

284 break 

285 if ignored: 

286 continue 

287 fullpath = os.path.join(directory, name) 

288 if os.path.isdir(fullpath): 

289 mtime = get_recursive_mtime(fullpath, ignores=ignores) 

290 else: 

291 mtime = 0 

292 try: 

293 mtime = int(os.path.getmtime(fullpath)) 

294 except Exception: 

295 pass 

296 if mtime > result: 

297 result = mtime 

298 return result 

299 

300 

301def add_inotify_watch(inotify, directory, ignores=[]): 

302 """Register recursively directories to watch. 

303 

304 Args: 

305 inotify (inotify object): object that owns the file descriptors 

306 directory (string): complete path of a directory to scan. 

307 ignores (list of strings): list of shell-style wildcards 

308 to define which filenames/dirnames to ignores (see fnmatch). 

309 

310 """ 

311 watch_flags = flags.MODIFY | flags.CREATE |\ 

312 flags.DELETE | flags.DELETE_SELF 

313 try: 

314 __get_logger().info("watch %s" % directory) 

315 inotify.add_watch(directory, watch_flags) 

316 except Exception as e: 

317 __get_logger().warning("cannot watch %s: %s" % (directory, e)) 

318 

319 if not os.access(directory, os.R_OK): 

320 __get_logger().warning("cannot enter into %s" % directory) 

321 return 

322 

323 for name in os.listdir(directory): 

324 ignored = False 

325 for ssw in ignores: 

326 if fnmatch.fnmatch(name, ssw): 

327 ignored = True 

328 break 

329 if ignored: 

330 continue 

331 fullpath = os.path.join(directory, name) 

332 if os.path.isdir(fullpath): 

333 add_inotify_watch(inotify, fullpath, ignores=ignores) 

334 

335 

336def _kill_process_and_children(process): 

337 children = None 

338 all_children = None 

339 # First we keep the full view of the process tree to kill 

340 try: 

341 all_children = process.children(recursive=True) 

342 except psutil.NoSuchProcess: 

343 pass 

344 # Then, we keep just immediate children to kill in the "good" order 

345 try: 

346 children = process.children(recursive=False) 

347 except psutil.NoSuchProcess: 

348 pass 

349 try: 

350 process.kill() 

351 except psutil.NoSuchProcess: 

352 pass 

353 if children is not None: 

354 for child in children: 

355 _kill_process_and_children(child) 

356 # To be sure, we didn't miss something, we kill the initial full list 

357 if all_children is not None: 

358 for child in all_children: 

359 _kill_process_and_children(child) 

360 

361 

362def kill_process_and_children(pid): 

363 """Kill recursively a complete tree of processes. 

364 

365 Given a pid, this method recursively kills the complete tree (children and 

366 children of each child...) of this process. 

367 

368 The SIGKILL signal is used. 

369 

370 Args: 

371 pid (int): process PID to kill. 

372 

373 """ 

374 try: 

375 process = psutil.Process(pid) 

376 except psutil.NoSuchProcess: 

377 return 

378 _kill_process_and_children(process) 

379 

380 

381def hash_generator(*args): 

382 """Generate a hash from a variable number of arguments as a safe string. 

383 

384 Note that pickle is used so arguments have to be serializable. 

385 

386 Args: 

387 *args: arguments to hash 

388 

389 """ 

390 temp = pickle.dumps(args, pickle.HIGHEST_PROTOCOL) 

391 return hashlib.md5(temp).hexdigest()