prerequisites.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2010-2013 Giovanni Mascellani <mascellani@poisson.phc.unipi.it>
  4. # Copyright © 2010-2017 Stefano Maggiolo <s.maggiolo@gmail.com>
  5. # Copyright © 2010-2012 Matteo Boscariol <boscarim@hotmail.com>
  6. # Copyright © 2013 Luca Wehrstedt <luca.wehrstedt@gmail.com>
  7. # Copyright © 2014 Artem Iglikov <artem.iglikov@gmail.com>
  8. # Copyright © 2015 William Di Luigi <williamdiluigi@gmail.com>
  9. # Copyright © 2016 Myungwoo Chun <mc.tamaki@gmail.com>
  10. #
  11. # This program is free software: you can redistribute it and/or modify
  12. # it under the terms of the GNU Affero General Public License as
  13. # published by the Free Software Foundation, either version 3 of the
  14. # License, or (at your option) any later version.
  15. #
  16. # This program is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU Affero General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU Affero General Public License
  22. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. """Build and installation routines needed to run CMS (user creation,
  24. configuration, and so on).
  25. """
  26. import argparse
  27. import grp
  28. import os
  29. import pwd
  30. import shutil
  31. import subprocess
  32. import sys
  33. from glob import glob
  34. # Root directories for the /usr and /var trees.
  35. USR_ROOT = os.path.join("/", "usr", "local")
  36. VAR_ROOT = os.path.join("/", "var", "local")
  37. # Do not prompt the user for interactive yes-or-no confirmations:
  38. # always assume yes! This is useful for programmatic use.
  39. ALWAYS_YES = False
  40. # Allow to do operations that should normally be performed as an
  41. # unprivileged user (e.g., building) as root.
  42. AS_ROOT = False
  43. # Do not even try to install configuration files (i.e., copying the
  44. # samples) when installing.
  45. NO_CONF = False
  46. # The user and group that CMS will be run as: will be created and will
  47. # receive the correct permissions to access isolate, the configuration
  48. # file and the system directories.
  49. CMSUSER = "cmsuser"
  50. def copyfile(src, dest, owner, perm, group=None):
  51. """Copy the file src to dest, and assign owner and permissions.
  52. src (string): the complete path of the source file.
  53. dest (string): the complete path of the destination file (i.e.,
  54. not the destination directory).
  55. owner (as given by pwd.getpwnam): the owner we want for dest.
  56. perm (integer): the permission for dest (example: 0o660).
  57. group (as given by grp.getgrnam): the group we want for dest; if
  58. not specified, use owner's
  59. group.
  60. """
  61. shutil.copy(src, dest)
  62. owner_id = owner.pw_uid
  63. if group is not None:
  64. group_id = group.gr_gid
  65. else:
  66. group_id = owner.pw_gid
  67. os.chown(dest, owner_id, group_id)
  68. os.chmod(dest, perm)
  69. def try_delete(path):
  70. """Try to delete a given path, failing gracefully.
  71. """
  72. if os.path.isdir(path):
  73. try:
  74. os.rmdir(path)
  75. except OSError:
  76. print("[Warning] Skipping because directory is not empty: ", path)
  77. else:
  78. try:
  79. os.remove(path)
  80. except OSError:
  81. print("[Warning] File not found: ", path)
  82. def makedir(dir_path, owner=None, perm=None):
  83. """Create a directory with given owner and permission.
  84. dir_path (string): the new directory to create.
  85. owner (as given by pwd.getpwnam): the owner we want for dest.
  86. perm (integer): the permission for dest (example: 0o660).
  87. """
  88. if not os.path.exists(dir_path):
  89. os.makedirs(dir_path)
  90. if perm is not None:
  91. os.chmod(dir_path, perm)
  92. if owner is not None:
  93. os.chown(dir_path, owner.pw_uid, owner.pw_gid)
  94. def copytree(src_path, dest_path, owner, perm_files, perm_dirs):
  95. """Copy the *content* of src_path in dest_path, assigning the
  96. given owner and permissions.
  97. src_path (string): the root of the subtree to copy.
  98. dest_path (string): the destination path.
  99. owner (as given by pwd.getpwnam): the owner we want for dest.
  100. perm_files (integer): the permission for copied not-directories.
  101. perm_dirs (integer): the permission for copied directories.
  102. """
  103. for path in glob(os.path.join(src_path, "*")):
  104. sub_dest = os.path.join(dest_path, os.path.basename(path))
  105. if os.path.isdir(path):
  106. makedir(sub_dest, owner, perm_dirs)
  107. copytree(path, sub_dest, owner, perm_files, perm_dirs)
  108. elif os.path.isfile(path):
  109. copyfile(path, sub_dest, owner, perm_files)
  110. else:
  111. print("Error: unexpected filetype for file %s. Not copied" % path)
  112. def ask(message):
  113. """Ask the user and return True if and only if one of the following holds:
  114. - the users responds "Y" or "y"
  115. - the "-y" flag was set as a CLI argument
  116. """
  117. return ALWAYS_YES or input(message) in ["Y", "y"]
  118. def assert_root():
  119. """Check if the current user is root, and exit with an error message if
  120. needed.
  121. """
  122. if os.geteuid() != 0:
  123. print("[Error] You must be root to do this, try using 'sudo'")
  124. exit(1)
  125. def assert_not_root():
  126. """Check if the current user is *not* root, and exit with an error message
  127. if needed. If the --as-root flag is set, this function does nothing.
  128. """
  129. if AS_ROOT:
  130. return
  131. if os.geteuid() == 0:
  132. print("[Error] You must *not* be root to do this, try avoiding 'sudo'")
  133. exit(1)
  134. def get_real_user():
  135. """Get the real username (the one who called sudo/su).
  136. In the case of a user *actually being root* we return an error. If the
  137. --as-root flag is set, this function returns "root".
  138. """
  139. if AS_ROOT:
  140. return "root"
  141. name = os.getenv("SUDO_USER")
  142. if name is None:
  143. name = os.popen("logname").read().strip()
  144. if name == "root":
  145. print("[Error] You are logged in as root")
  146. print(
  147. "[Error] Log in as a normal user instead, and use 'sudo' or 'su'")
  148. exit(1)
  149. return name
  150. def build_isolate():
  151. """This function compiles the isolate sandbox.
  152. """
  153. assert_not_root()
  154. print("===== Compiling isolate")
  155. # We make only the executable isolate, otherwise the tool a2x
  156. # is needed and we have to add more compilation dependencies.
  157. subprocess.check_call(["make", "-C", "isolate", "isolate"])
  158. def install_isolate():
  159. """This function installs the isolate sandbox.
  160. """
  161. assert_root()
  162. root = pwd.getpwnam("root")
  163. try:
  164. cmsuser_grp = grp.getgrgid(pwd.getpwnam(CMSUSER).pw_gid)
  165. except:
  166. print("[Error] The user %s doesn't exist yet" % CMSUSER)
  167. print("[Error] You need to run the install command at least once")
  168. exit(1)
  169. # Check if build_isolate() has been called
  170. if not os.path.exists(os.path.join("isolate", "isolate")):
  171. print("[Error] You must run the build_isolate command first")
  172. exit(1)
  173. print("===== Copying isolate to /usr/local/bin/")
  174. makedir(os.path.join(USR_ROOT, "bin"), root, 0o755)
  175. copyfile(os.path.join(".", "isolate", "isolate"),
  176. os.path.join(USR_ROOT, "bin", "isolate"),
  177. root, 0o4750, group=cmsuser_grp)
  178. print("===== Copying isolate config to /usr/local/etc/")
  179. makedir(os.path.join(USR_ROOT, "etc"), root, 0o755)
  180. copyfile(os.path.join(".", "isolate", "default.cf"),
  181. os.path.join(USR_ROOT, "etc", "isolate"),
  182. root, 0o640, group=cmsuser_grp)
  183. def build():
  184. """This function builds all the prerequisites by calling:
  185. - build_isolate
  186. """
  187. build_isolate()
  188. def install_conf():
  189. """Install configuration files"""
  190. assert_root()
  191. print("===== Copying configuration to /usr/local/etc/")
  192. root = pwd.getpwnam("root")
  193. cmsuser = pwd.getpwnam(CMSUSER)
  194. makedir(os.path.join(USR_ROOT, "etc"), root, 0o755)
  195. for conf_file_name in ["cms.conf", "cms.ranking.conf"]:
  196. conf_file = os.path.join(USR_ROOT, "etc", conf_file_name)
  197. # Skip if destination is a symlink
  198. if os.path.islink(conf_file):
  199. continue
  200. # If the config exists, check if the user wants to overwrite it
  201. if os.path.exists(conf_file):
  202. if not ask("The %s file is already installed, "
  203. "type Y to overwrite it: " % (conf_file_name)):
  204. continue
  205. if os.path.exists(os.path.join(".", "config", conf_file_name)):
  206. copyfile(os.path.join(".", "config", conf_file_name),
  207. conf_file, cmsuser, 0o660)
  208. else:
  209. conf_file_name = "%s.sample" % conf_file_name
  210. copyfile(os.path.join(".", "config", conf_file_name),
  211. conf_file, cmsuser, 0o660)
  212. def install():
  213. """This function prepares all that's needed to run CMS:
  214. - creation of cmsuser user
  215. - compilation and installation of isolate
  216. - installation of configuration files
  217. and so on.
  218. """
  219. assert_root()
  220. # Get real user to run non-sudo commands
  221. real_user = get_real_user()
  222. try:
  223. cmsuser_pw = pwd.getpwnam(CMSUSER)
  224. except KeyError:
  225. print("===== Creating user %s" % CMSUSER)
  226. subprocess.check_call(["useradd", CMSUSER, "--system",
  227. "--comment", "CMS default user",
  228. "--shell", "/bin/false", "-U"])
  229. cmsuser_pw = pwd.getpwnam(CMSUSER)
  230. cmsuser_gr = grp.getgrgid(cmsuser_pw.pw_gid)
  231. root_pw = pwd.getpwnam("root")
  232. if real_user == "root":
  233. # Run build() command as root
  234. build()
  235. else:
  236. # Run build() command as not root
  237. subprocess.check_call(["sudo", "-E", "-u", real_user,
  238. sys.executable, sys.argv[0], "build"])
  239. install_isolate()
  240. # We set permissions for each manually installed files, so we want
  241. # max liberty to change them.
  242. old_umask = os.umask(0o000)
  243. if not NO_CONF:
  244. install_conf()
  245. print("===== Creating directories")
  246. dirs = [os.path.join(VAR_ROOT, "log"),
  247. os.path.join(VAR_ROOT, "cache"),
  248. os.path.join(VAR_ROOT, "lib"),
  249. os.path.join(VAR_ROOT, "run"),
  250. os.path.join(USR_ROOT, "include"),
  251. os.path.join(USR_ROOT, "share")]
  252. for _dir in dirs:
  253. # Skip if destination is a symlink
  254. if os.path.islink(os.path.join(_dir, "cms")):
  255. continue
  256. makedir(_dir, root_pw, 0o755)
  257. _dir = os.path.join(_dir, "cms")
  258. makedir(_dir, cmsuser_pw, 0o770)
  259. extra_dirs = [os.path.join(VAR_ROOT, "cache", "cms", "fs-cache-shared")]
  260. for _dir in extra_dirs:
  261. makedir(_dir, cmsuser_pw, 0o770)
  262. print("===== Copying Polygon testlib")
  263. path = os.path.join("cmscontrib", "loaders", "polygon", "testlib.h")
  264. dest_path = os.path.join(USR_ROOT, "include", "cms", "testlib.h")
  265. copyfile(path, dest_path, root_pw, 0o644)
  266. os.umask(old_umask)
  267. if real_user != "root":
  268. gr_name = cmsuser_gr.gr_name
  269. print("===== Adding yourself to the %s group" % gr_name)
  270. if ask("Type Y if you want me to automatically add "
  271. "\"%s\" to the %s group: " % (real_user, gr_name)):
  272. subprocess.check_call(["usermod", "-a", "-G", gr_name, real_user])
  273. print("""
  274. ###########################################################################
  275. ### ###
  276. ### Remember that you must now logout in order to make the change ###
  277. ### effective ("the change" is: being in the %s group). ###
  278. ### ###
  279. ###########################################################################
  280. """ % gr_name)
  281. else:
  282. print("""
  283. ###########################################################################
  284. ### ###
  285. ### Remember that you must be in the %s group to use CMS: ###
  286. ### ###
  287. ### $ sudo usermod -a -G %s <your user> ###
  288. ### ###
  289. ### You must also logout to make the change effective. ###
  290. ### ###
  291. ###########################################################################
  292. """ % (gr_name, gr_name))
  293. def uninstall():
  294. """This function deletes all that was installed by the install()
  295. function:
  296. - deletion of the cmsuser user
  297. - deletion of isolate
  298. - deletion of configuration files
  299. and so on.
  300. """
  301. assert_root()
  302. print("===== Deleting isolate from /usr/local/bin/")
  303. try_delete(os.path.join(USR_ROOT, "bin", "isolate"))
  304. print("===== Deleting configuration to /usr/local/etc/")
  305. if ask("Type Y if you really want to remove configuration files: "):
  306. for conf_file_name in ["cms.conf", "cms.ranking.conf"]:
  307. try_delete(os.path.join(USR_ROOT, "etc", conf_file_name))
  308. print("===== Deleting empty directories")
  309. extra_dirs = [os.path.join(VAR_ROOT, "cache", "cms", "fs-cache-shared")]
  310. for _dir in extra_dirs:
  311. if os.listdir(_dir) == []:
  312. try_delete(_dir)
  313. dirs = [os.path.join(VAR_ROOT, "log"),
  314. os.path.join(VAR_ROOT, "cache"),
  315. os.path.join(VAR_ROOT, "lib"),
  316. os.path.join(VAR_ROOT, "run"),
  317. os.path.join(USR_ROOT, "include"),
  318. os.path.join(USR_ROOT, "share")]
  319. for _dir in dirs:
  320. if os.listdir(_dir) == []:
  321. try_delete(_dir)
  322. print("===== Deleting Polygon testlib")
  323. try_delete(os.path.join(USR_ROOT, "include", "cms", "testlib.h"))
  324. print("===== Deleting user and group %s" % CMSUSER)
  325. try:
  326. # Just to check whether it exists.
  327. pwd.getpwnam(CMSUSER)
  328. except KeyError:
  329. pass
  330. else:
  331. if ask("Do you want to delete user %s? [y/N] " % CMSUSER):
  332. subprocess.check_call(["userdel", CMSUSER])
  333. try:
  334. # Just to check whether it exists. If CMSUSER had a different primary
  335. # group, we'll do nothing here.
  336. grp.getgrnam(CMSUSER)
  337. except KeyError:
  338. pass
  339. else:
  340. if ask("Do you want to delete group %s? [y/N] " % CMSUSER):
  341. subprocess.check_call(["groupdel", CMSUSER])
  342. elif ask("Do you want to remove all users from group %s? [y/N] "
  343. % CMSUSER):
  344. for user in grp.getgrnam(CMSUSER).gr_mem:
  345. subprocess.check_call(["gpasswd", "-d", user, CMSUSER])
  346. print("===== Done")
  347. if __name__ == '__main__':
  348. parser = argparse.ArgumentParser(
  349. description='Script used to manage prerequisites for CMS')
  350. parser.add_argument(
  351. "-y", "--yes", action="store_true",
  352. help="Don't ask questions interactively")
  353. parser.add_argument(
  354. "--no-conf", action="store_true",
  355. help="Don't install configuration files")
  356. parser.add_argument(
  357. "--as-root", action="store_true",
  358. help="(DON'T USE) Allow running non-root commands as root")
  359. parser.add_argument(
  360. "--cmsuser", action="store", type=str, default=CMSUSER,
  361. help="(DON'T USE) The user CMS will be run as"
  362. )
  363. subparsers = parser.add_subparsers(metavar="command",
  364. help="Subcommand to run")
  365. subparsers.add_parser("build_isolate",
  366. help="Build \"isolate\" sandbox") \
  367. .set_defaults(func=build_isolate)
  368. subparsers.add_parser("build",
  369. help="Build everything") \
  370. .set_defaults(func=build)
  371. subparsers.add_parser("install_isolate",
  372. help="Install \"isolate\" sandbox (requires root)") \
  373. .set_defaults(func=install_isolate)
  374. subparsers.add_parser("install",
  375. help="Install everything (requires root)") \
  376. .set_defaults(func=install)
  377. subparsers.add_parser("uninstall",
  378. help="Uninstall everything (requires root)") \
  379. .set_defaults(func=uninstall)
  380. args = parser.parse_args()
  381. ALWAYS_YES = args.yes
  382. NO_CONF = args.no_conf
  383. AS_ROOT = args.as_root
  384. CMSUSER = args.cmsuser
  385. if not hasattr(args, "func"):
  386. parser.error("Please specify a command to run. "
  387. "Use \"--help\" for more information.")
  388. args.func()