Advertisement
Guest User

easy-install.py

a guest
Jan 1st, 2023
72
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.12 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import argparse
  4. import logging
  5. import os
  6. import platform
  7. import subprocess
  8. import sys
  9. import time
  10. import urllib.request
  11. from shutil import move, unpack_archive, which
  12. from typing import Dict
  13.  
  14. logging.basicConfig(
  15. filename="easy-install.log",
  16. filemode="w",
  17. format="%(asctime)s - %(levelname)s - %(message)s",
  18. level=logging.INFO,
  19. )
  20.  
  21.  
  22. def cprint(*args, level: int = 1):
  23. """
  24. logs colorful messages
  25. level = 1 : RED
  26. level = 2 : GREEN
  27. level = 3 : YELLOW
  28.  
  29. default level = 1
  30. """
  31. CRED = "\033[31m"
  32. CGRN = "\33[92m"
  33. CYLW = "\33[93m"
  34. reset = "\033[0m"
  35. message = " ".join(map(str, args))
  36. if level == 1:
  37. print(CRED, message, reset)
  38. if level == 2:
  39. print(CGRN, message, reset)
  40. if level == 3:
  41. print(CYLW, message, reset)
  42.  
  43.  
  44. def clone_frappe_docker_repo() -> None:
  45. try:
  46. urllib.request.urlretrieve(
  47. "https://github.com/frappe/frappe_docker/archive/refs/heads/main.zip",
  48. "frappe_docker.zip",
  49. )
  50. logging.info("Downloaded frappe_docker zip file from GitHub")
  51. unpack_archive(
  52. "frappe_docker.zip", "."
  53. ) # Unzipping the frappe_docker.zip creates a folder "frappe_docker-main"
  54. move("frappe_docker-main", "frappe_docker")
  55. logging.info("Unzipped and Renamed frappe_docker")
  56. os.remove("frappe_docker.zip")
  57. logging.info("Removed the downloaded zip file")
  58. except Exception as e:
  59. logging.error("Download and unzip failed", exc_info=True)
  60. cprint("\nCloning frappe_docker Failed\n\n", "[ERROR]: ", e, level=1)
  61.  
  62.  
  63. def get_from_env(dir, file) -> Dict:
  64. env_vars = {}
  65. with open(os.path.join(dir, file)) as f:
  66. for line in f:
  67. if line.startswith("#") or not line.strip():
  68. continue
  69. key, value = line.strip().split("=", 1)
  70. env_vars[key] = value
  71. return env_vars
  72.  
  73.  
  74. def write_to_env(wd: str, site: str, db_pass: str, admin_pass: str, email: str) -> None:
  75. site_name = site or ""
  76. example_env = get_from_env(wd, "example.env")
  77. with open(os.path.join(wd, ".env"), "w") as f:
  78. f.writelines(
  79. [
  80. f"FRAPPE_VERSION={example_env['FRAPPE_VERSION']}\n", # Defaults to latest version of Frappe
  81. f"ERPNEXT_VERSION={example_env['ERPNEXT_VERSION']}\n", # defaults to latest version of ERPNext
  82. f"DB_PASSWORD={db_pass}\n",
  83. "DB_HOST=db\n",
  84. "DB_PORT=3306\n",
  85. "REDIS_CACHE=redis-cache:6379\n",
  86. "REDIS_QUEUE=redis-queue:6379\n",
  87. "REDIS_SOCKETIO=redis-socketio:6379\n",
  88. f"LETSENCRYPT_EMAIL={email}\n",
  89. f"FRAPPE_SITE_NAME_HEADER={site_name}\n",
  90. f"SITE_ADMIN_PASS={admin_pass}",
  91. ]
  92. )
  93.  
  94.  
  95. def generate_pass(length: int = 12) -> str:
  96. """Generate random hash using best available randomness source."""
  97. import math
  98. import secrets
  99.  
  100. if not length:
  101. length = 56
  102.  
  103. return secrets.token_hex(math.ceil(length / 2))[:length]
  104.  
  105.  
  106. def check_repo_exists() -> bool:
  107. return os.path.exists(os.path.join(os.getcwd(), "frappe_docker"))
  108.  
  109.  
  110. def setup_prod(project: str, sitename: str, email: str) -> None:
  111. if check_repo_exists():
  112. compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml")
  113. docker_repo_path = os.path.join(os.getcwd(), "frappe_docker")
  114. cprint(
  115. "\nPlease refer to .example.env file in the frappe_docker folder to know which keys to set\n\n",
  116. level=3,
  117. )
  118. admin_pass = ""
  119. db_pass = ""
  120. with open(compose_file_name, "w") as f:
  121. # Writing to compose file
  122. if not os.path.exists(os.path.join(docker_repo_path, ".env")):
  123. admin_pass = generate_pass()
  124. db_pass = generate_pass(9)
  125. write_to_env(docker_repo_path, sitename, db_pass, admin_pass, email)
  126. cprint(
  127. "\nA .env file is generated with basic configs. Please edit it to fit to your needs \n",
  128. level=3,
  129. )
  130. with open(os.path.join(os.path.expanduser("~"), "passwords.txt"), "w") as en:
  131. en.writelines(f"ADMINISTRATOR_PASSWORD={admin_pass}\n")
  132. en.writelines(f"MARIADB_ROOT_PASSWORD={db_pass}\n")
  133. else:
  134. env = get_from_env(docker_repo_path, ".env")
  135. admin_pass = env["SITE_ADMIN_PASS"]
  136. db_pass = env["DB_PASSWORD"]
  137. try:
  138. # TODO: Include flags for non-https and non-erpnext installation
  139. subprocess.run(
  140. [
  141. which("docker"),
  142. "compose",
  143. "--project-name",
  144. project,
  145. "-f",
  146. "compose.yaml",
  147. "-f",
  148. "overrides/compose.mariadb.yaml",
  149. "-f",
  150. "overrides/compose.redis.yaml",
  151. # "-f", "overrides/compose.noproxy.yaml", TODO: Add support for local proxying without HTTPs
  152. "-f",
  153. "overrides/compose.erpnext.yaml",
  154. "-f",
  155. "overrides/compose.https.yaml",
  156. "--env-file",
  157. ".env",
  158. "config",
  159. ],
  160. cwd=docker_repo_path,
  161. stdout=f,
  162. check=True,
  163. )
  164.  
  165. except Exception:
  166. logging.error("Docker Compose generation failed", exc_info=True)
  167. cprint("\nGenerating Compose File failed\n")
  168. sys.exit(1)
  169. try:
  170. # Starting with generated compose file
  171. subprocess.run(
  172. [
  173. which("docker"),
  174. "compose",
  175. "-p",
  176. project,
  177. "-f",
  178. compose_file_name,
  179. "up",
  180. "-d",
  181. ],
  182. check=True,
  183. )
  184. logging.info(f"Docker Compose file generated at ~/{project}-compose.yml")
  185.  
  186. except Exception as e:
  187. logging.error("Prod docker-compose failed", exc_info=True)
  188. cprint(" Docker Compose failed, please check the container logs\n", e)
  189. sys.exit(1)
  190.  
  191. cprint(f"\nCreating site: {sitename} \n", level=3)
  192.  
  193. try:
  194. subprocess.run(
  195. [
  196. which("docker"),
  197. "compose",
  198. "-p",
  199. project,
  200. "exec",
  201. "backend",
  202. "bench",
  203. "new-site",
  204. sitename,
  205. "--db-root-password",
  206. db_pass,
  207. "--admin-password",
  208. admin_pass,
  209. "--install-app",
  210. "erpnext",
  211. "--set-default",
  212. ],
  213. check=True,
  214. )
  215. logging.info("New site creation completed")
  216. except Exception as e:
  217. logging.error("Bench site creation failed", exc_info=True)
  218. cprint("Bench Site creation failed\n", e)
  219. sys.exit(1)
  220. else:
  221. install_docker()
  222. clone_frappe_docker_repo()
  223. setup_prod(project, sitename, email) # Recursive
  224.  
  225.  
  226. def setup_dev_instance(project: str):
  227. if check_repo_exists():
  228. try:
  229. subprocess.run(
  230. [
  231. "docker",
  232. "compose",
  233. "-f",
  234. "devcontainer-example/docker-compose.yml",
  235. "--project-name",
  236. project,
  237. "up",
  238. "-d",
  239. ],
  240. cwd=os.path.join(os.getcwd(), "frappe_docker"),
  241. check=True,
  242. )
  243. cprint(
  244. "Please go through the Development Documentation: https://github.com/frappe/frappe_docker/tree/main/development to fully complete the setup.",
  245. level=2,
  246. )
  247. logging.info("Development Setup completed")
  248. except Exception as e:
  249. logging.error("Dev Environment setup failed", exc_info=True)
  250. cprint("Setting Up Development Environment Failed\n", e)
  251. else:
  252. install_docker()
  253. clone_frappe_docker_repo()
  254. setup_dev_instance(project) # Recursion on goes brrrr
  255.  
  256.  
  257. def install_docker():
  258. if which("docker") is not None:
  259. return
  260. cprint("Docker is not installed, Installing Docker...", level=3)
  261. logging.info("Docker not found, installing Docker")
  262. if platform.system() == "Darwin" or platform.system() == "Windows":
  263. print(
  264. f"""
  265. This script doesn't install Docker on {"Mac" if platform.system()=="Darwin" else "Windows"}.
  266.  
  267. Please go through the Docker Installation docs for your system and run this script again"""
  268. )
  269. logging.debug("Docker setup failed due to platform is not Linux")
  270. sys.exit(1)
  271. try:
  272. ps = subprocess.run(
  273. ["curl", "-fsSL", "https://get.docker.com"],
  274. capture_output=True,
  275. check=True,
  276. )
  277. subprocess.run(["/bin/bash"], input=ps.stdout, capture_output=True)
  278. subprocess.run(
  279. ["sudo", "usermod", "-aG", "docker", str(os.getenv("USER"))], check=True
  280. )
  281. cprint("Waiting Docker to start", level=3)
  282. time.sleep(10)
  283. subprocess.run(["sudo", "systemctl", "restart", "docker.service"], check=True)
  284. except Exception as e:
  285. logging.error("Installing Docker failed", exc_info=True)
  286. cprint("Failed to Install Docker\n", e)
  287. cprint("\n Try Installing Docker Manually and re-run this script again\n")
  288. sys.exit(1)
  289.  
  290.  
  291. if __name__ == "__main__":
  292. parser = argparse.ArgumentParser(description="Install Frappe with Docker")
  293. parser.add_argument(
  294. "-p", "--prod", help="Setup Production System", action="store_true"
  295. )
  296. parser.add_argument(
  297. "-d", "--dev", help="Setup Development System", action="store_true"
  298. )
  299. parser.add_argument(
  300. "-s",
  301. "--sitename",
  302. help="The Site Name for your production site",
  303. default="site1.local",
  304. )
  305. parser.add_argument("-n", "--project", help="Project Name", default="frappe")
  306. parser.add_argument(
  307. "--email", help="Add email for the SSL.", required="--prod" in sys.argv
  308. )
  309. args = parser.parse_args()
  310. if args.dev:
  311. cprint("\nSetting Up Development Instance\n", level=2)
  312. logging.info("Running Development Setup")
  313. setup_dev_instance(args.project)
  314. elif args.prod:
  315. cprint("\nSetting Up Production Instance\n", level=2)
  316. logging.info("Running Production Setup")
  317. if "example.com" in args.email:
  318. cprint("Emails with example.com not acceptable", level=1)
  319. sys.exit(1)
  320. setup_prod(args.project, args.sitename, args.email)
  321. else:
  322. parser.print_help()
  323.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement