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.

slim.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. #!/usr/bin/env python3
  2. #-*- coding: utf-8 -*-
  3. import os, os.path
  4. import sys
  5. import shutil
  6. import argparse
  7. import logging
  8. import re
  9. import json
  10. import configparser
  11. import signal
  12. import subprocess
  13. logging.basicConfig(level=logging.INFO)
  14. INSTANCES_ABSPATH="/tmp/lodel2_instances"
  15. CONFFILE='conf.d/lodel2.ini'
  16. try:
  17. STORE_FILE = os.path.join("[@]SLIM_VAR_DIR[@]", 'slim_instances.json')
  18. PID_FILE = os.path.join("[@]SLIM_VAR_DIR[@]", 'slim_instances_pid.json')
  19. CREATION_SCRIPT = os.path.join("[@]LODEL2_PROGSDIR[@]", 'create_instance')
  20. INSTALL_TPL = "[@]SLIM_INSTALLMODEL_DIR[@]"
  21. EMFILE = os.path.join("[@]SLIM_DATADIR[@]", 'emfile.pickle')
  22. except SyntaxError:
  23. STORE_FILE='./instances.json'
  24. PID_FILE = './slim_instances_pid.json'
  25. CREATION_SCRIPT='../scripts/create_instance.sh'
  26. INSTALL_TPL = './slim_ressources/slim_install_model'
  27. EMFILE = './slim_ressources/emfile.pickle'
  28. CREATION_SCRIPT=os.path.join(os.path.dirname(__file__), CREATION_SCRIPT)
  29. STORE_FILE=os.path.join(os.path.dirname(__file__), STORE_FILE)
  30. INSTALL_TPL=os.path.join(os.path.dirname(__file__), INSTALL_TPL)
  31. EMFILE=os.path.join(os.path.dirname(__file__), EMFILE)
  32. #STORE_FILE syntax :
  33. #
  34. #First level keys are instances names, their values are dict with following
  35. #informations :
  36. # - path
  37. #
  38. ##@brief Run 'make %target%' for each instances given in names
  39. #@param target str : make target
  40. #@param names list : list of instance name
  41. def run_make(target, names):
  42. validate_names(names)
  43. store_datas = get_store_datas()
  44. cwd = os.getcwd()
  45. for name in [n for n in store_datas if n in names]:
  46. datas = store_datas[name]
  47. logging.info("Running 'make %s' for '%s' in %s" % (
  48. target, name, datas['path']))
  49. os.chdir(datas['path'])
  50. os.system('make %s' % target)
  51. os.chdir(cwd)
  52. ##@brief Set configuration given args
  53. #@param args as returned by argparse
  54. def set_conf(name, args):
  55. validate_names([name])
  56. conffile = get_conffile(name)
  57. config = configparser.ConfigParser(interpolation=None)
  58. config.read(conffile)
  59. if args.interface is not None:
  60. iarg = args.interface
  61. if iarg not in ('web', 'python'):
  62. raise TypeError("Interface can only be on of : 'web', 'python'")
  63. if iarg.lower() == 'web':
  64. iarg = 'webui'
  65. else:
  66. iarg = ''
  67. config['lodel2']['interface'] = iarg
  68. interface = config['lodel2']['interface']
  69. if interface == 'webui':
  70. if 'lodel2.webui' not in config:
  71. config['lodel2.webui'] = dict()
  72. config['lodel2.webui']['standalone'] = 'True'
  73. if args.listen_port is not None:
  74. config['lodel2.webui']['listen_port'] = str(args.listen_port)
  75. if args.listen_address is not None:
  76. config['lodel2.webui']['listen_address'] = str(args.listen_address)
  77. else: #interface is python
  78. if args.listen_port is not None or args.listen_address is not None:
  79. logging.error("Listen port and listen address will not being set. \
  80. Selected interface is not the web iterface")
  81. if 'lodel.webui' in config:
  82. del(config['lodel2.webui'])
  83. #Now config should be OK to be written again in conffile
  84. with open(conffile, 'w+') as cfp:
  85. config.write(cfp)
  86. ##@brief If the name is not valid raise
  87. def name_is_valid(name):
  88. allowed_chars = [chr(i) for i in range(ord('a'), ord('z')+1)]
  89. allowed_chars += [chr(i) for i in range(ord('A'), ord('Z')+1)]
  90. allowed_chars += [chr(i) for i in range(ord('0'), ord('9')+1)]
  91. allowed_chars += ['_']
  92. for c in name:
  93. if c not in allowed_chars:
  94. raise RuntimeError("Allowed characters for instance name are \
  95. lower&upper alphanum and '_'. Name '%s' is invalid" % name)
  96. ##@brief Create a new instance
  97. #@param name str : the instance name
  98. def new_instance(name):
  99. name_is_valid(name)
  100. store_datas = get_store_datas()
  101. if name in store_datas:
  102. logging.error("An instance named '%s' already exists" % name)
  103. exit(1)
  104. if not os.path.isdir(INSTANCES_ABSPATH):
  105. logging.info("Instances directory '%s' don't exists, creating it")
  106. os.mkdir(INSTANCES_ABSPATH)
  107. instance_path = os.path.join(INSTANCES_ABSPATH, name)
  108. creation_cmd = '{script} "{name}" "{path}" "{install_tpl}" \
  109. "{emfile}"'.format(
  110. script = CREATION_SCRIPT,
  111. name = name,
  112. path = instance_path,
  113. install_tpl = INSTALL_TPL,
  114. emfile = EMFILE)
  115. res = os.system(creation_cmd)
  116. if res != 0:
  117. logging.error("Creation script fails")
  118. exit(res)
  119. #storing new instance
  120. store_datas[name] = {'path': instance_path}
  121. save_datas(store_datas)
  122. ##@brief Delete an instance
  123. #@param name str : the instance name
  124. def delete_instance(name):
  125. if get_pid(name) is not None:
  126. logging.error("The instance '%s' is started. Stop it before deleting \
  127. it" % name)
  128. store_datas = get_store_datas()
  129. logging.warning("Deleting instance %s" % name)
  130. logging.info("Deleting instance folder %s" % store_datas[name]['path'])
  131. shutil.rmtree(store_datas[name]['path'])
  132. logging.info("Deleting instance from json store file")
  133. del(store_datas[name])
  134. save_datas(store_datas)
  135. ##@brief returns stored datas
  136. def get_store_datas():
  137. if not os.path.isfile(STORE_FILE) or os.stat(STORE_FILE).st_size == 0:
  138. return dict()
  139. else:
  140. with open(STORE_FILE, 'r') as sfp:
  141. datas = json.load(sfp)
  142. return datas
  143. ##@brief Checks names validity and exit if fails
  144. def validate_names(names):
  145. store_datas = get_store_datas()
  146. invalid = [ n for n in names if n not in store_datas]
  147. if len(invalid) > 0:
  148. print("Following names are not existing instance :", file=sys.stderr)
  149. for name in invalid:
  150. print("\t%s" % name, file=sys.stderr)
  151. exit(1)
  152. ##@brief Returns the PID dict
  153. #@return a dict with instance name as key an PID as value
  154. def get_pids():
  155. if not os.path.isfile(PID_FILE):
  156. return dict()
  157. with open(PID_FILE, 'r') as pfd:
  158. return json.load(pfd)
  159. ##@brief Save a dict of pid
  160. #@param pid_dict dict : key is instance name values are pid
  161. def save_pids(pid_dict):
  162. with open(PID_FILE, 'w+') as pfd:
  163. json.dump(pid_dict, pfd)
  164. ##@brief Given an instance name returns its PID
  165. #@return False or an int
  166. def get_pid(name):
  167. pid_datas = get_pids()
  168. if name not in pid_datas:
  169. return False
  170. else:
  171. return pid_datas[name]
  172. ##@brief Start an instance
  173. #@param names list : instance name list
  174. def start_instances(names):
  175. pids = get_pids()
  176. store_datas = get_store_datas()
  177. for name in names:
  178. if name in pids:
  179. logging.warning("The instance %s is allready running" % name)
  180. continue
  181. os.chdir(store_datas[name]['path'])
  182. args = [sys.executable, 'loader.py']
  183. curexec = subprocess.Popen(args)
  184. pids[name] = curexec.pid
  185. logging.info("Instance '%s' started. PID %d" % (name, curexec.pid))
  186. save_pids(pids)
  187. def stop_instances(names):
  188. pids = get_pids()
  189. store_datas = get_store_datas()
  190. for name in names:
  191. if name not in pids:
  192. logging.warning("The instance %s is not running" % name)
  193. continue
  194. pid = pids[name]
  195. os.kill(pid, signal.SIGINT)
  196. ##@brief Check if instance are specified
  197. def get_specified(args):
  198. if args.all:
  199. names = list(get_store_datas().keys())
  200. elif args.name is not None:
  201. names = args.name
  202. else:
  203. names = None
  204. return names
  205. ##@brief Saves store datas
  206. def save_datas(datas):
  207. with open(STORE_FILE, 'w+') as sfp:
  208. json.dump(datas, sfp)
  209. ##@return conffile path
  210. def get_conffile(name):
  211. validate_names([name])
  212. store_datas = get_store_datas()
  213. return os.path.join(store_datas[name]['path'], CONFFILE)
  214. ##@brief Print the list of instances and exit
  215. #@param verbosity int
  216. #@param batch bool : if true make simple output
  217. def list_instances(verbosity, batch):
  218. verbosity = 0 if verbosity is None else verbosity
  219. if not os.path.isfile(STORE_FILE):
  220. print("No store file, no instances are existing. Exiting...",
  221. file=sys.stderr)
  222. exit(0)
  223. store_datas = get_store_datas()
  224. if not batch:
  225. print('Instances list :')
  226. for name in store_datas:
  227. details_instance(name, verbosity, batch)
  228. exit(0)
  229. ##@brief Print instance informations and return (None)
  230. #@param name str : instance name
  231. #@param verbosity int
  232. #@param batch bool : if true make simple output
  233. def details_instance(name, verbosity, batch):
  234. validate_names([name])
  235. store_datas = get_store_datas()
  236. pids = get_pids()
  237. if not batch:
  238. msg = "\t- '%s'" % name
  239. if name in pids:
  240. msg += ' [Run PID %d] ' % pids[name]
  241. if verbosity > 0:
  242. msg += ' path = "%s"' % store_datas[name]['path']
  243. if verbosity > 1:
  244. ruler = (''.join(['=' for _ in range(20)])) + "\n"
  245. msg += "\n\t\t====conf.d/lodel2.ini====\n"
  246. with open(get_conffile(name)) as cfp:
  247. for line in cfp:
  248. msg += "\t\t"+line
  249. msg += "\t\t=========================\n"
  250. print(msg)
  251. else:
  252. msg = name
  253. if verbosity > 0:
  254. msg += "\t"+'"%s"' % store_datas[name]['path']
  255. if verbosity > 1:
  256. conffile = get_conffile(name)
  257. msg += "\n\t#####"+conffile+"#####\n"
  258. with open(conffile, 'r') as cfp:
  259. for line in cfp:
  260. msg += "\t"+line
  261. msg += "\n\t###########"
  262. print(msg)
  263. ##@brief Returns instanciated parser
  264. def get_parser():
  265. parser = argparse.ArgumentParser(
  266. description='SLIM (Simple Lodel Instance Manager.)')
  267. selector = parser.add_argument_group('Instances selectors')
  268. actions = parser.add_argument_group('Instances actions')
  269. confs = parser.add_argument_group('Options (use with -c or -s)')
  270. parser.add_argument('-l', '--list',
  271. help='list existing instances and exit', action='store_const',
  272. const=True, default=False)
  273. parser.add_argument('-v', '--verbose', action='count')
  274. parser.add_argument('-b', '--batch', action='store_const',
  275. default=False, const=True,
  276. help="Format output (when possible) making it usable by POSIX scripts \
  277. (only implemented for -l for the moment)")
  278. selector.add_argument('-a', '--all', action='store_const',
  279. default=False, const=True,
  280. help='Select all instances')
  281. selector.add_argument('-n', '--name', metavar='NAME', type=str, nargs='*',
  282. help="Specify an instance name")
  283. actions.add_argument('-c', '--create', action='store_const',
  284. default=False, const=True,
  285. help="Create a new instance with given name (see -n --name)")
  286. actions.add_argument('-d', '--delete', action='store_const',
  287. default=False, const=True,
  288. help="Delete an instance with given name (see -n --name)")
  289. actions.add_argument('-p', '--purge', action='store_const',
  290. default=False, const=True,
  291. help="Delete ALL instances")
  292. actions.add_argument('-s', '--set-option', action='store_const',
  293. default=False, const=True,
  294. help="Use this flag to set options on instance")
  295. actions.add_argument('-e', '--edit-config', action='store_const',
  296. default=False, const=True,
  297. help='Edit configuration of specified instance')
  298. actions.add_argument('-i', '--interactive', action='store_const',
  299. default=False, const=True,
  300. help='Run a loader.py from ONE instance in foreground')
  301. actions.add_argument('--stop', action='store_const',
  302. default=False, const=True, help="Stop instances")
  303. actions.add_argument('--start', action='store_const',
  304. default=False, const=True, help="Start instances")
  305. actions.add_argument('-m', '--make', metavar='TARGET', type=str,
  306. nargs="?", default='not',
  307. help='Run make for selected instances')
  308. confs.add_argument('--interface', type=str,
  309. help="Select wich interface to run. Possible values are \
  310. 'python' and 'web'")
  311. confs.add_argument('--listen-port', type=int,
  312. help="Select the port on wich the web interface will listen to")
  313. confs.add_argument('--listen-address', type=str,
  314. help="Select the address on wich the web interface will bind to")
  315. return parser
  316. if __name__ == '__main__':
  317. parser = get_parser()
  318. args = parser.parse_args()
  319. if args.list:
  320. # Instances list
  321. if args.name is not None:
  322. validate_names(args.name)
  323. for name in args.name:
  324. details_instance(name, args.verbose, args.batch)
  325. else:
  326. list_instances(args.verbose, args.batch)
  327. elif args.create:
  328. #Instance creation
  329. if args.name is None:
  330. parser.print_help()
  331. print("\nAn instance name expected when creating an instance !",
  332. file=sys.stderr)
  333. exit(1)
  334. for name in args.name:
  335. new_instance(name)
  336. elif args.purge:
  337. print("Are you sure ? Yes/no ",)
  338. rep = sys.stdin.read()
  339. if rep == 'Yes':
  340. store = get_store_datas()
  341. for name in store:
  342. delete_instance(name)
  343. elif rep.lower() != 'no':
  344. print("Expect exactly 'Yes' to confirm...")
  345. exit()
  346. elif args.delete:
  347. #Instance deletion
  348. if args.all:
  349. parser.print_help()
  350. print("\n use -p --purge instead of --delete --all",
  351. file=sys.stderr)
  352. exit(1)
  353. if args.name is None:
  354. parser.print_help()
  355. print("\nAn instance name expected when creating an instance !",
  356. file=sys.stderr)
  357. exit(1)
  358. validate_names(args.name)
  359. for name in args.name:
  360. delete_instance(name)
  361. elif args.make != 'not':
  362. #Running make in instances
  363. if args.make is None:
  364. target = 'all'
  365. else:
  366. target = args.make
  367. names = get_specified(args)
  368. if names is None:
  369. parser.print_help()
  370. print("\nWhen using -m --make options you have to select \
  371. instances, either by name using -n or all using -a")
  372. exit(1)
  373. run_make(target, names)
  374. elif args.edit_config:
  375. #Edit configuration
  376. names = get_specified(args)
  377. if len(names) > 1:
  378. print("\n-e --edit-config option works only when 1 instance is \
  379. specified")
  380. validate_names(names)
  381. name = names[0]
  382. store_datas = get_store_datas()
  383. conffile = get_conffile(name)
  384. os.system('editor "%s"' % conffile)
  385. exit(0)
  386. elif args.interactive:
  387. #Run loader.py in foreground
  388. if args.name is None or len(args.name) != 1:
  389. print("\n-i option only allowed with ONE instance name")
  390. parser.print_help()
  391. exit(1)
  392. validate_names(args.name)
  393. name = args.name[0]
  394. store_datas = get_store_datas()
  395. os.chdir(store_datas[name]['path'])
  396. os.execl('/usr/bin/env', '/usr/bin/env', 'python3', 'loader.py')
  397. elif args.set_option:
  398. names = None
  399. if args.all:
  400. names = list(get_store_datas().values())
  401. elif args.name is not None:
  402. names = args.name
  403. if names is None:
  404. print("\n-s option only allowed with instance specified (by name \
  405. or with -a)")
  406. parser.print_help()
  407. exit(1)
  408. for name in names:
  409. set_conf(name, args)
  410. elif args.start:
  411. names = get_specified(args)
  412. start_instances(names)
  413. elif args.stop:
  414. names = get_specified(args)
  415. stop_instances(names)