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 14KB

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