No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

core_scripts.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. #
  2. # This file is part of Lodel 2 (https://github.com/OpenEdition)
  3. #
  4. # Copyright (C) 2015-2017 Cléo UMS-3287
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as published
  8. # by the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. import operator
  20. import shutil
  21. import tempfile
  22. import os
  23. import os.path
  24. import argparse
  25. from lodel.context import LodelContext
  26. LodelContext.expose_modules(globals(), {
  27. 'lodel.plugin.scripts': 'lodel_script',
  28. 'lodel.logger': 'logger'})
  29. # @package lodel.plugin.core_scripts
  30. #@brief Lodel2 internal scripts declaration
  31. #@ingroup lodel2_plugins
  32. #@ingroup lodel2_script
  33. ## @brief Implement lodel_admin.py list-plugins action
  34. #@ingroup lodel2_plugins
  35. #@ingroup lodel2_script
  36. #
  37. class ListPlugins(lodel_script.LodelScript):
  38. _action = 'plugins-list'
  39. _description = "List all installed plugins"
  40. ## @brief Set available arguments for the script lodel_admin with action plugin-list
  41. # @param parser : a parser (see argparse python module)
  42. @classmethod
  43. def argparser_config(cls, parser):
  44. parser.add_argument('-v', '--verbose',
  45. help="Display more informations on installed plugins",
  46. action='store_true')
  47. parser.add_argument('-c', '--csv',
  48. help="Format output in CSV format",
  49. action='store_true')
  50. ## @brief Display the list of plugins according to given arguments
  51. # @param args : grabbed from argv of command line
  52. @classmethod
  53. def run(cls, args):
  54. import lodel.plugin.plugins
  55. from lodel.plugin.plugins import Plugin
  56. if args.verbose:
  57. #_discover does not return duplicated names
  58. tmp_plist = Plugin._discover(lodel.plugin.plugins.PLUGINS_PATH)
  59. plist = []
  60. # ordering the list by plugin's name
  61. for pname in sorted(set([d['name'] for d in tmp_plist])):
  62. for pinfos in tmp_plist:
  63. if pinfos['name'] == pname:
  64. plist.append(pinfos)
  65. else:
  66. # Retrieve the dict with the list of plugins
  67. pdict = Plugin.discover()
  68. # casting to a list ordered by names
  69. plist = []
  70. for pname in sorted(pdict.keys()):
  71. plist.append(pdict[pname])
  72. if args.csv:
  73. if args.verbose:
  74. res = "name,version,path\n"
  75. fmt = "%s,%s,%s\n"
  76. else:
  77. res = "name,version\n"
  78. fmt = "%s,%s\n"
  79. else:
  80. res = "Installed plugins list :\n"
  81. if args.verbose:
  82. fmt = "\t- %s(%s) in %s\n"
  83. else:
  84. fmt = "\t- %s(%s)\n"
  85. for pinfos in plist:
  86. if args.verbose:
  87. res += fmt % (
  88. pinfos['name'], pinfos['version'], pinfos['path'])
  89. else:
  90. res += fmt % (pinfos['name'], pinfos['version'])
  91. print(res)
  92. ## @brief Handle install & uninstall of lodel plugins
  93. class PluginManager(lodel_script.LodelScript):
  94. _action = 'plugins'
  95. _description = "Install/Uninstall plugins"
  96. ## @brief Set parser's available arguments for lodel_admin.py with action plugins
  97. # @param parser : a parser (see argparse python module)
  98. @classmethod
  99. def argparser_config(cls, parser):
  100. parser.add_argument('-u', '--uninstall',
  101. help="Uninstall specified plugin",
  102. action='store_true')
  103. parser.add_argument('-c', '--clean',
  104. help="Uninstall duplicated plugins with smallest version number",
  105. action="store_true")
  106. parser.add_argument('-n', '--plugin-name', nargs='*',
  107. default=list(),
  108. help="Indicate a plugin name to uninstall",
  109. type=str)
  110. parser.add_argument('-a', '--archive', nargs='*',
  111. default=list(),
  112. help="(NOT IMPLEMENTED) Specify a tarball containing a plugin \
  113. to install",
  114. type=str)
  115. parser.add_argument('-d', '--directory', nargs='*',
  116. default=list(),
  117. help="Specify a plugin by its directory",
  118. type=str)
  119. parser.add_argument('-f', '--force', action="store_true",
  120. help="Force plugin directory deletion in case of check errors")
  121. ## @brief Install, uninstall or clean a plugin according to the option given
  122. # @param args : grabbed from argv of command line
  123. @classmethod
  124. def run(cls, args):
  125. if args.clean:
  126. if args.uninstall or len(args.directory) > 0 \
  127. or len(args.archive) > 0:
  128. raise RuntimeError("When using -c --clean option you can \
  129. only use option -n --name to clean plugins with specified names")
  130. return cls.clean(args)
  131. if args.uninstall:
  132. return cls.uninstall(args)
  133. return cls.install(args)
  134. ## @brief Install plugins
  135. # @param args : grabbed from argv of command line
  136. @classmethod
  137. def install(cls, args):
  138. import lodel.plugin.plugins
  139. from lodel.plugin.plugins import Plugin
  140. from lodel.plugin.exceptions import PluginError
  141. # We can't install a plugin with just its name, we have to know where
  142. # it is
  143. if len(args.plugin_name) > 0:
  144. raise RuntimeError("Unable to install a plugin from its name !\
  145. We do not know where to find it...")
  146. plist = Plugin.discover()
  147. errors = dict()
  148. # For now we do not handle archive for plugins
  149. if len(args.archive) > 0:
  150. raise NotImplementedError("Not supported yet")
  151. plugins_infos = {}
  152. for cur_dir in args.directory:
  153. # Check that the directories obtained correspond to plugins
  154. try:
  155. res = Plugin.dir_is_plugin(cur_dir, assert_in_package=False)
  156. if res is False:
  157. errors[cur_dir] = PluginError("Not a plugin")
  158. else:
  159. plugins_infos[res['name']] = res
  160. except Exception as e:
  161. errors[cur_dir] = e
  162. # Abording because of previous errors
  163. if len(errors) > 0:
  164. msg = "Abording installation because of following errors :\n"
  165. for path, expt in errors.items():
  166. msg += ("\t- For path '%s' : %s\n" % (path, expt))
  167. raise RuntimeError(msg)
  168. # No errors continuing to install
  169. for pname, pinfos in plugins_infos.items():
  170. if pname in plist:
  171. # Found an installed plugin with the same name
  172. # Checking both versions
  173. if plist[pname]['version'] == pinfos['version']:
  174. errors[pinfos['path']] = 'Abording installation of %s \
  175. found in %s because it seems to be allready installed in %s' % (
  176. pname, pinfos['path'], plist[pname]['path'])
  177. continue
  178. if plist[pname]['version'] > pinfos['version']:
  179. errors[pinfos['path']] = 'Abording installation of %s \
  180. found in %s because the same plugins with a greater version seems to be \
  181. installed in %s' % (pname, pinfos['path'], plist[pname]['path'])
  182. continue
  183. logger.info("Found a plugin with the same name but with an \
  184. inferior version. Continuing to install")
  185. # Checking that we can safely copy our plugin
  186. dst_path = os.path.join(lodel.plugin.plugins.PLUGINS_PATH,
  187. os.path.basename(os.path.dirname(pinfos['path'])))
  188. orig_path = dst_path
  189. if os.path.isdir(dst_path):
  190. dst_path = tempfile.mkdtemp(
  191. prefix=os.path.basename(dst_path) + '_',
  192. dir=lodel.plugin.plugins.PLUGINS_PATH)
  193. logger.warning("A plugin already exists in %s. Installing \
  194. in %s" % (orig_path, dst_path))
  195. shutil.rmtree(dst_path)
  196. # Install the plugin
  197. shutil.copytree(pinfos['path'], dst_path, symlinks=False)
  198. print("%s(%s) installed in %s" % (
  199. pname, pinfos['version'], dst_path))
  200. if len(errors) > 0:
  201. msg = "Following errors occurs during installation process :\n"
  202. for path, error_msg in errors.items():
  203. msg += "\t- For '%s' : %s" % (path, error_msg)
  204. print(msg)
  205. ## @brief Uninstall plugins
  206. # @param args : grabbed from argv of command line
  207. # @todo Does nothing for now : delete is commented
  208. @classmethod
  209. def uninstall(cls, args):
  210. import lodel.plugin.plugins
  211. from lodel.plugin.plugins import Plugin
  212. if len(args.archive) > 0:
  213. raise RuntimeError("Cannot uninstall plugin using -f --file \
  214. options. Use -d --directory instead")
  215. to_delete = dict() # will contain all pathes of plugins to delete
  216. errors = dict()
  217. # Uninstall by pathes
  218. if len(args.directory) > 0:
  219. # processing & checking -d --directory arguments
  220. for path in args.directory:
  221. apath = os.path.abspath(path)
  222. # We assume plugins are in lodel/plugins
  223. if not apath.startswith(lodel.plugins.PLUGINS_PATH):
  224. errors[path] = "Not a subdir of %s"
  225. errors[path] %= lodel.plugins.PLUGINS_PATH
  226. continue
  227. try:
  228. pinfos = Plugin.dir_is_plugin(apath)
  229. except Exception as e:
  230. if not args.force:
  231. errors[path] = e
  232. continue
  233. to_delete[path] = pinfos
  234. # Uninstall by plugin's names
  235. # We retrieve the path of the plugin from its name
  236. if len(args.plugin_name) > 0:
  237. # Processing -n --plugin-name arguments
  238. plist = Plugin._discover(lodel.plugins.PLUGINS_PATH)
  239. for pinfos in plist:
  240. if pinfos['name'] in args.plugin_name:
  241. to_delete[pinfos['path']] = pinfos
  242. # Manage errors and exit if there is no force option
  243. if len(errors) > 0:
  244. msg = "Following errors detected before begining deletions :\n"
  245. for path, errmsg in errors.items():
  246. msg += "\t- For %s : %s" % (path, errmsg)
  247. print(msg)
  248. if not args.force:
  249. exit(1)
  250. print("Begining deletion :")
  251. for path, pinfos in to_delete.items():
  252. # shutil.rmtree(path)
  253. print("rm -R %s" % path)
  254. print("\t%s(%s) in %s deleted" % (
  255. pinfos['name'], pinfos['version'], pinfos['path']))
  256. ## @brief Clean plugins by removing plugins with same names \
  257. # The last version is kept
  258. # @param args : grabbed from argv of command line
  259. @classmethod
  260. def clean(cls, args):
  261. import lodel.plugin.plugins
  262. from lodel.plugin.plugins import Plugin
  263. if len(args.archive) > 0:
  264. raise RuntimeError("Cannot specify plugins to uninstall using \
  265. -f --file option. You have to use -d --directory or -n --name")
  266. if len(args.plugin_name) > 0:
  267. names = args.plugin_name
  268. else:
  269. names = list(Plugin.discover().keys())
  270. #_discover do not remove duplicated names
  271. full_list = Plugin._discover(lodel.plugins.PLUGINS_PATH)
  272. # Casting into a dict with list of plugins infos
  273. pdict = dict()
  274. for pinfos in full_list:
  275. if pinfos['name'] in names:
  276. if pinfos['name'] in pdict:
  277. pdict[pinfos['name']].append(pinfos)
  278. else:
  279. pdict[pinfos['name']] = [pinfos]
  280. to_clean = list()
  281. clean_count = 0
  282. for pname, pinfos_l in pdict.items():
  283. if len(pinfos_l) > 1:
  284. # There are some plugins to clean
  285. tmp_l = sorted(pinfos_l, key=lambda item: item['version'])
  286. to_clean += tmp_l[:-1]
  287. msg = "Found %s(%s). Cleaning " % (
  288. pname, tmp_l[-1]['version'])
  289. for pinfos in to_clean:
  290. clean_count += 1
  291. str_info = '%s(%s)' % (pname, pinfos['version'])
  292. msg += "%s, " % (str_info)
  293. shutil.rmtree(pinfos['path'])
  294. print(msg)
  295. if clean_count > 0:
  296. print("%d plugins were uninstalled" % clean_count)
  297. else:
  298. print("Already clean")
  299. ## @brief Implements lodel_admin.py **hooks-list** action
  300. #@ingroup lodel2_script
  301. #@ingroup lodel2_hooks
  302. class ListHooks(lodel_script.LodelScript):
  303. _action = 'hooks-list'
  304. _description = 'Generate a list of registered hooks once instance started'
  305. @classmethod
  306. def argparser_config(cls, parser):
  307. pass
  308. ## @brief Display the list of hooks registered
  309. @classmethod
  310. def run(cls, args):
  311. import loader
  312. loader.start()
  313. from lodel.plugin.hooks import LodelHook
  314. hlist = LodelHook.hook_list()
  315. print("Registered hooks : ")
  316. for name in sorted(hlist.keys()):
  317. print("\t- %s is registered by :" % name)
  318. for hfun, priority in hlist[name]:
  319. msg = "\t\t- {modname}.{funname} with priority : {priority}"
  320. print(msg.format(
  321. modname=hfun.__module__,
  322. funname=hfun.__name__,
  323. priority=priority))
  324. print("\n")