Coverage for mfplugin/utils.py: 75%

204 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-13 15:57 +0000

1import re 

2import os 

3import json 

4import importlib 

5from mfutil import BashWrapperException, BashWrapper, get_ipv4_for_hostname, \ 

6 mkdir_p_or_die 

7 

8__pdoc__ = { 

9 "PluginEnvContextManager": False 

10} 

11MFMODULE = os.environ.get('MFMODULE', 'GENERIC') 

12MFMODULE_RUNTIME_HOME = os.environ.get("MFMODULE_RUNTIME_HOME", "/tmp") 

13MFMODULE_LOWERCASE = os.environ.get('MFMODULE_LOWERCASE', 'generic') 

14PLUGIN_NAME_REGEXP = "^[A-Za-z0-9_-]+$" 

15 

16 

17class PluginEnvContextManager(object): 

18 

19 __env_dict = None 

20 __saved_environ = None 

21 

22 def __init__(self, env_dict): 

23 self.__env_dict = env_dict 

24 

25 def __enter__(self): 

26 self.__saved_environ = dict(os.environ) 

27 for key, value in self.__env_dict.items(): 

28 os.environ[key] = value 

29 

30 def __exit__(self, type, value, traceback): 

31 os.environ.clear() 

32 os.environ.update(self.__saved_environ) 

33 

34 

35def validate_plugin_name(plugin_name): 

36 """Validate a plugin name. 

37 

38 Args: 

39 plugin_name (string): the plugin name to validate. 

40 

41 Raises: 

42 BadPluginName exception if the plugin_name is incorrect. 

43 """ 

44 if plugin_name.startswith("plugin_"): 

45 raise BadPluginName("A plugin name can't start with 'plugin_'") 

46 if plugin_name.startswith("__"): 

47 raise BadPluginName("A plugin name can't start with '__'") 

48 if plugin_name == "base": 

49 raise BadPluginName("A plugin name can't be 'base'") 

50 if plugin_name == "config": 

51 raise BadPluginName("A plugin name can't be 'config'") 

52 if not re.match(PLUGIN_NAME_REGEXP, plugin_name): 

53 raise BadPluginName("A plugin name must follow %s" % 

54 PLUGIN_NAME_REGEXP) 

55 

56 

57def plugin_name_to_layerapi2_label(plugin_name): 

58 """Get a layerapi2 label from a plugin name. 

59 

60 Args: 

61 plugin_name (string): the plugin name from which we create the label. 

62 

63 Returns: 

64 (string): the layerapi2 label. 

65 """ 

66 return "plugin_%s@%s" % (plugin_name, MFMODULE_LOWERCASE) 

67 

68 

69def layerapi2_label_to_plugin_name(label): 

70 """Get the plugin name from the layerapi2 label. 

71 

72 Args: 

73 label (string): the label from which we extract the plugin name. 

74 

75 Returns: 

76 (string): the plugin name. 

77 """ 

78 if not label.startswith("plugin_"): 

79 raise BadPlugin("bad layerapi2_label: %s => is it really a plugin? " 

80 "(it must start with 'plugin_')" % label) 

81 if not label.endswith("@%s" % MFMODULE_LOWERCASE): 

82 raise BadPlugin("bad layerapi2_label: %s => is it really a plugin? " 

83 "(it must end with '@%s')" % 

84 (label, MFMODULE_LOWERCASE)) 

85 return label[7:].split('@')[0] 

86 

87 

88def layerapi2_label_file_to_plugin_name(llf_path): 

89 """Get the plugin name from the layerapi2 label file. 

90 

91 Args: 

92 llf_path (string): the layerapi2 label file path from which 

93 we extract the label. 

94 

95 Returns: 

96 (string): the plugin name. 

97 """ 

98 try: 

99 with open(llf_path, 'r') as f: 

100 c = f.read().strip() 

101 except Exception: 

102 raise BadPlugin("can't read %s file" % llf_path) 

103 return layerapi2_label_to_plugin_name(c) 

104 

105 

106def layerapi2_label_to_plugin_home(plugins_base_dir, label): 

107 """Find the plugin home corresponding to the given layerapi2 label. 

108 

109 We search in plugins_base_dir for a directory (not recursively) with 

110 the corresponding label value. 

111 

112 If we found nothing, None is returned. 

113 

114 Args: 

115 plugins_base_dir (string): plugins base dir to search. 

116 label (string): the label to search. 

117 

118 Returns: 

119 (string): plugin home (absolute directory path) or None. 

120 

121 """ 

122 for d in os.listdir(plugins_base_dir): 

123 if d == "base": 

124 continue 

125 fd = os.path.abspath(os.path.join(plugins_base_dir, d)) 

126 if not os.path.isdir(fd): 

127 continue 

128 llf = os.path.join(fd, ".layerapi2_label") 

129 if not os.path.isfile(llf): 

130 continue 

131 try: 

132 with open(llf, 'r') as f: 

133 c = f.read().strip() 

134 except Exception: 

135 continue 

136 if c == label: 

137 return fd 

138 # not found 

139 return None 

140 

141 

142def inside_a_plugin_env(): 

143 """Return True if we are inside a plugin_env. 

144 

145 Returns: 

146 (boolean): True if we are inside a plugin_env, False else 

147 """ 

148 return ("%s_CURRENT_PLUGIN_NAME" % MFMODULE) in os.environ 

149 

150 

151def validate_configparser(v, cpobj, schema, public=False): 

152 document = {} 

153 for section in cpobj.sections(): 

154 document[section] = {} 

155 for key in cpobj.options(section): 

156 if not public or not key.startswith('_'): 

157 document[section][key] = cpobj.get(section, key) 

158 return v.validate(document, schema) 

159 

160 

161def cerberus_errors_to_human_string(v_errors): 

162 errors = "" 

163 for section in v_errors.keys(): 

164 for error in v_errors[section]: 

165 if errors == "": 

166 errors = "\n" 

167 if type(error) is str: 

168 errors = errors + "- [section: %s] %s\n" % (section, error) 

169 continue 

170 for key in error.keys(): 

171 for error2 in error[key]: 

172 errors = errors + \ 

173 "- [section: %s][key: %s] %s\n" % (section, key, 

174 error2) 

175 return errors 

176 

177 

178class MFPluginException(BashWrapperException): 

179 """Base mfplugin Exception class.""" 

180 

181 def __init__(self, msg=None, validation_errors=None, 

182 original_exception=None, **kwargs): 

183 if msg is not None: 

184 self.message = msg 

185 else: 

186 self.message = "mfplugin exception!" 

187 if original_exception is not None: 

188 BashWrapperException.__init__( 

189 self, 

190 self.message + (": %s" % original_exception), 

191 **kwargs) 

192 else: 

193 BashWrapperException.__init__(self, self.message, **kwargs) 

194 self.original_exception = original_exception 

195 

196 

197class BadPlugin(MFPluginException): 

198 """Exception raised when a plugin is badly constructed.""" 

199 

200 def __init__(self, msg=None, validation_errors=None, **kwargs): 

201 if msg is not None: 

202 self.message = msg 

203 else: 

204 self.message = "bad plugin!" 

205 MFPluginException.__init__(self, self.message, **kwargs) 

206 self.validation_errors = validation_errors 

207 

208 def __repr__(self): 

209 if self.validation_errors is None: 

210 return MFPluginException.__repr__(self) 

211 return "%s exception with message: %s and validation errors: %s" % \ 

212 (self.__class__.__name__, self.message, self.validation_errors) 

213 

214 def __str__(self): 

215 return self.__repr__() 

216 

217 

218class BadPluginConfiguration(BadPlugin): 

219 """Exception raised when a plugin has a bad configuration.""" 

220 

221 pass 

222 

223 

224class BadPluginName(BadPlugin): 

225 """Exception raised when a plugin has an invalid name.""" 

226 

227 pass 

228 

229 

230class NotInstalledPlugin(MFPluginException): 

231 """Exception raised when a plugin is not installed.""" 

232 

233 pass 

234 

235 

236class AlreadyInstalledPlugin(MFPluginException): 

237 """Exception raised when a plugin is already installed.""" 

238 

239 pass 

240 

241 

242class CantInstallPlugin(MFPluginException): 

243 """Exception raised when we can't install a plugin.""" 

244 

245 pass 

246 

247 

248class CantUninstallPlugin(MFPluginException): 

249 """Exception raised when we can't uninstall a plugin.""" 

250 

251 pass 

252 

253 

254class CantBuildPlugin(MFPluginException): 

255 """Exception raised when we can't build a plugin.""" 

256 

257 pass 

258 

259 

260class BadPluginFile(MFPluginException): 

261 """Exception raised when a plugin file is bad.""" 

262 

263 pass 

264 

265 

266def get_default_plugins_base_dir(): 

267 """Return the default plugins base directory path. 

268 

269 This value correspond to the content of MFMODULE_PLUGINS_BASE_DIR env var 

270 or ${RUNTIME_HOME}/var/plugins (if not set). 

271 

272 Returns: 

273 (string): the default plugins base directory path. 

274 

275 """ 

276 if "MFMODULE_PLUGINS_BASE_DIR" in os.environ: 

277 return os.environ.get("MFMODULE_PLUGINS_BASE_DIR") 

278 return os.path.join(MFMODULE_RUNTIME_HOME, "var", "plugins") 

279 

280 

281def _touch_conf_monitor_control_file(): 

282 BashWrapper("touch %s/var/conf_monitor" % MFMODULE_RUNTIME_HOME) 

283 

284 

285def resolve(val): 

286 if val.lower() == "null" or val.startswith("/"): 

287 # If it's "null" or linux socket 

288 return val 

289 return get_ipv4_for_hostname(val) 

290 

291 

292def to_bool(strng): 

293 if isinstance(strng, bool): 

294 return strng 

295 try: 

296 return strng.lower() in ('1', 'true', 'yes', 'y', 't') 

297 except Exception: 

298 return False 

299 

300 

301def to_int(strng): 

302 try: 

303 return int(strng) 

304 except Exception: 

305 return 0 

306 

307 

308def null_to_empty(value): 

309 if value == "null": 

310 return "" 

311 return value 

312 

313 

314def get_plugin_lock_path(): 

315 lock_dir = os.path.join(MFMODULE_RUNTIME_HOME, 'tmp') 

316 lock_path = os.path.join(lock_dir, "plugin_management_lock") 

317 if not os.path.isdir(lock_dir): 

318 mkdir_p_or_die(lock_dir) 

319 return lock_path 

320 

321 

322def get_current_envs(plugin_name, plugin_home): 

323 plugin_label = plugin_name_to_layerapi2_label(plugin_name) 

324 return { 

325 "%s_CURRENT_PLUGIN_NAME" % MFMODULE: plugin_name, 

326 "%s_CURRENT_PLUGIN_DIR" % MFMODULE: plugin_home, 

327 "%s_CURRENT_PLUGIN_LABEL" % MFMODULE: plugin_label 

328 } 

329 

330 

331def get_class_from_fqn(class_fqn): 

332 class_name = class_fqn.split('.')[-1] 

333 module_path = ".".join(class_fqn.split('.')[0:-1]) 

334 if module_path == "": 

335 raise Exception("incorrect class_fqn: %s" % class_fqn) 

336 mod = importlib.import_module(module_path) 

337 return getattr(mod, class_name) 

338 

339 

340def __get_class(class_arg, env, default): 

341 if class_arg is not None: 

342 return class_arg 

343 if env in os.environ: 

344 class_fqn = os.environ[env] 

345 return get_class_from_fqn(class_fqn) 

346 return default 

347 

348 

349def get_configuration_class(configuration_class_arg, default): 

350 return __get_class(configuration_class_arg, "MFPLUGIN_CONFIGURATION_CLASS", 

351 default) 

352 

353 

354def get_app_class(app_class_arg, default): 

355 return __get_class(app_class_arg, "MFPLUGIN_APP_CLASS", 

356 default) 

357 

358 

359def get_extra_daemon_class(extra_daemon_class_arg, default): 

360 return __get_class(extra_daemon_class_arg, "MFPLUGIN_EXTRA_DAEMON_CLASS", 

361 default) 

362 

363 

364def is_jsonable(x): 

365 try: 

366 json.dumps(x) 

367 return True 

368 except Exception: 

369 return False 

370 

371 

372def get_nice_dump(val): 

373 def default(o): 

374 return f"<<non-serializable: {type(o).__qualname__}" 

375 return json.dumps(val, indent=4, default=default) 

376 

377 

378def get_configuration_path(plugin_home): 

379 return os.path.join(plugin_home, "config.ini") 

380 

381 

382def get_configuration_paths(plugin_name, plugin_home): 

383 return [ 

384 get_configuration_path(plugin_home), 

385 "%s/config/plugins/%s.ini" % (MFMODULE_RUNTIME_HOME, plugin_name), 

386 "/etc/metwork.config.d/%s/plugins/%s.ini" % 

387 (MFMODULE_LOWERCASE, plugin_name), 

388 ] 

389 

390 

391NON_REQUIRED_BOOLEAN = { 

392 "required": False, 

393 "type": "boolean", 

394 "coerce": to_bool 

395} 

396NON_REQUIRED_BOOLEAN_DEFAULT_FALSE = { 

397 **NON_REQUIRED_BOOLEAN, 

398 "default": False 

399} 

400NON_REQUIRED_BOOLEAN_DEFAULT_TRUE = { 

401 **NON_REQUIRED_BOOLEAN, 

402 "default": True 

403} 

404NON_REQUIRED_INTEGER = { 

405 "required": False, 

406 "type": "integer", 

407 "coerce": to_int 

408} 

409NON_REQUIRED_INTEGER_DEFAULT_0 = { 

410 **NON_REQUIRED_INTEGER, 

411 "default": 0 

412} 

413NON_REQUIRED_STRING = { 

414 "required": False, 

415 "type": "string", 

416 "coerce": null_to_empty, 

417 "default": "" 

418} 

419NON_REQUIRED_STRING_DEFAULT_EMPTY = { 

420 **NON_REQUIRED_STRING, 

421 "default": "" 

422} 

423NON_REQUIRED_STRING_DEFAULT_1 = { 

424 **NON_REQUIRED_STRING, 

425 "default": "1" 

426}