1 import os

2 import platform

3 import shutil

4 import subprocess

5 import sys

6 from pathlib import Path

7 from typing import Annotated

8

9 import structlog

10 import typer

11

12 from meshadmin.cli.utils import get_context_config

13

14 service_app = typer.Typer()

15 logger = structlog.get_logger(__name__)

16

17

18@service_app.command(name="install")

19 def service_install():

20 context = get_context_config()

21 network_dir = context["network_dir"]

22 context_name = context["name"]

23 os_name = platform.system()

24 meshadmin_path = shutil.which("meshadmin")

25

26 if not meshadmin_path:

27 logger.error("meshadmin executable not found in PATH")

28 exit(1)

29

30 (network_dir / "env").write_text(f"MESH_CONTEXT={context_name}\n")

31

32 if os_name == "Darwin":

33 plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>

34<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

35<plist version="1.0">

36<dict>

37 <key>Label</key>

38 <string>com.meshadmin.{context_name}</string>

39 <key>ProgramArguments</key>

40 <array>

41 <string>{meshadmin_path}</string>

42 <string>nebula</string>

43 <string>start</string>

44 </array>

45 <key>EnvironmentVariables</key>

46 <dict>

47 <key>MESH_CONTEXT</key>

48 <string>{context_name}</string>

49 </dict>

50 <key>RunAtLoad</key>

51 <true/>

52 <key>KeepAlive</key>

53 <true/>

54 <key>StandardErrorPath</key>

55 <string>{network_dir}/error.log</string>

56 <key>StandardOutPath</key>

57 <string>{network_dir}/output.log</string>

58</dict>

59</plist>

60"""

61 launch_agents_dir = Path(os.path.expanduser("~/Library/LaunchAgents"))

62 if not launch_agents_dir.exists():

63 launch_agents_dir.mkdir(exist_ok=True, parents=True)

64 plist_path = launch_agents_dir / f"com.meshadmin.{context_name}.plist"

65 plist_path.write_text(plist_content)

66 subprocess.run(["launchctl", "load", str(plist_path)])

67 logger.info(

68 "meshadmin service installed and started",

69 plist_path=str(plist_path),

70 context_name=context_name,

71 )

72 print(f"meshadmin service installed at {plist_path}")

73 print(f"Context: {context_name}")

74 print("Service has been loaded and will start automatically on login")

75

76 else:

77 systemd_unit = f"""[Unit]

78 Description=Meshadmin {context_name}

79 Wants=basic.target network-online.target nss-lookup.target time-sync.target

80 After=basic.target network.target network-online.target

81 Before=sshd.service

82

83[Service]

84#Type=notify

85#NotifyAccess=main

86 SyslogIdentifier={context_name}

87 EnvironmentFile={network_dir}/env

88 ExecReload=/bin/kill -HUP $MAINPID

89 ExecStart={meshadmin_path} nebula start

90 Restart=always

91

92[Install]

93 WantedBy=multi-user.target

94"""

95 systemd_service_path = Path(

96 f"/usr/lib/systemd/system/meshadmin-{context_name}.service"

97 )

98 systemd_service_path.write_text(systemd_unit)

99 subprocess.run(["systemctl", "daemon-reload"])

100 subprocess.run(["systemctl", "enable", f"meshadmin-{context_name}"])

101 print(f"meshadmin service installed at {systemd_service_path}")

102 print(f"Context: {context_name}")

103 print("Service has been enabled and will start automatically on boot")

104

105

106@service_app.command(name="uninstall")

107 def service_uninstall():

108 context = get_context_config()

109 context_name = context["name"]

110 network_dir = context["network_dir"]

111 os_name = platform.system()

112

113 if os_name == "Darwin":

114 plist_path = Path(

115 os.path.expanduser(

116 f"~/Library/LaunchAgents/com.meshadmin.{context_name}.plist"

117 )

118 )

119 if plist_path.exists():

120 subprocess.run(["launchctl", "unload", str(plist_path)])

121 plist_path.unlink()

122 env_path = network_dir / "env"

123 if env_path.exists():

124 env_path.unlink()

125 logger.info("meshadmin service uninstalled", plist_path=str(plist_path))

126 print(f"meshadmin service uninstalled from {plist_path}")

127 else:

128 logger.warning("meshadmin service not found", plist_path=str(plist_path))

129 print("meshadmin service not found, nothing to uninstall")

130 else:

131 systemd_service_path = Path(

132 f"/usr/lib/systemd/system/meshadmin-{context_name}.service"

133 )

134 if systemd_service_path.exists():

135 subprocess.run(["systemctl", "stop", f"meshadmin-{context_name}"])

136 subprocess.run(["systemctl", "disable", f"meshadmin-{context_name}"])

137 subprocess.run(["systemctl", "daemon-reload"])

138 systemd_service_path.unlink()

139 env_path = network_dir / "env"

140 if env_path.exists():

141 env_path.unlink()

142 logger.info("meshadmin service uninstalled")

143 print("meshadmin service uninstalled")

144 else:

145 logger.warning("meshadmin service not found")

146 print("meshadmin service not found, nothing to uninstall")

147

148

149@service_app.command(name="start")

150 def service_start():

151 context = get_context_config()

152 context_name = context["name"]

153 os_name = platform.system()

154

155 if os_name == "Darwin":

156 plist_path = Path(

157 os.path.expanduser(

158 f"~/Library/LaunchAgents/com.meshadmin.{context_name}.plist"

159 )

160 )

161 if plist_path.exists():

162 subprocess.run(["launchctl", "load", str(plist_path)])

163 logger.info("meshadmin service started", context=context_name)

164 print(f"meshadmin service started for context {context_name}")

165 else:

166 logger.error("meshadmin service not installed", plist_path=str(plist_path))

167 print(

168 f"meshadmin service not installed for context {context_name}. Run 'meshadmin service install' first."

169 )

170 else:

171 subprocess.run(["systemctl", "start", f"meshadmin-{context_name}"])

172 print(f"meshadmin service started for context {context_name}")

173

174

175@service_app.command(name="stop")

176 def service_stop():

177 context = get_context_config()

178 context_name = context["name"]

179 os_name = platform.system()

180

181 if os_name == "Darwin":

182 plist_path = Path(

183 os.path.expanduser(

184 f"~/Library/LaunchAgents/com.meshadmin.{context_name}.plist"

185 )

186 )

187 if plist_path.exists():

188 subprocess.run(["launchctl", "unload", str(plist_path)])

189 logger.info("meshadmin service stopped", context=context_name)

190 print(f"meshadmin service stopped for context {context_name}")

191 else:

192 logger.error("meshadmin service not installed", plist_path=str(plist_path))

193 print(

194 f"meshadmin service not installed for context {context_name}. Nothing to stop."

195 )

196 else:

197 subprocess.run(["systemctl", "stop", f"meshadmin-{context_name}"])

198 print(f"meshadmin service stopped for context {context_name}")

199

200

201@service_app.command(name="logs")

202 def service_logs(

203 follow: Annotated[

204 bool,

205 typer.Option("--follow", "-f", help="Follow the logs in real time"),

206 ] = False,

207 lines: Annotated[

208 int,

209 typer.Option("--lines", "-n", help="Number of lines to show"),

210 ] = 50,

211):

212 context = get_context_config()

213 context_name = context["name"]

214 network_dir = context["network_dir"]

215 os_name = platform.system()

216

217 if os_name == "Darwin":

218 error_log = network_dir / "error.log"

219 output_log = network_dir / "output.log"

220

221 if not error_log.exists() and not output_log.exists():

222 print(

223 f"No log files found for context {context_name}. Has the service been started?"

224 )

225 raise typer.Exit(1)

226

227 if follow:

228 try:

229 process = subprocess.Popen(

230 ["tail", "-f", str(error_log), str(output_log)],

231 stdout=sys.stdout,

232 stderr=sys.stderr,

233 )

234 process.wait()

235 except KeyboardInterrupt:

236 process.terminate()

237 else:

238 for log_file in [output_log, error_log]:

239 if log_file.exists():

240 print(f"\n=== {log_file.name} ===")

241 result = subprocess.run(

242 ["tail", f"-n{lines}", str(log_file)],

243 capture_output=True,

244 text=True,

245 )

246 print(result.stdout)

247 else:

248 try:

249 cmd = ["journalctl", "-u", f"meshadmin-{context_name}"]

250 if follow:

251 cmd.append("-f")

252 if lines:

253 cmd.append(f"-n{lines}")

254

255 if follow:

256 process = subprocess.Popen(

257 cmd,

258 stdout=sys.stdout,

259 stderr=sys.stderr,

260 )

261 process.wait()

262 else:

263 result = subprocess.run(cmd, capture_output=True, text=True)

264 print(result.stdout)

265

266 except subprocess.CalledProcessError as e:

267 print(f"Error accessing logs: {e}")

268 print(

269 "Make sure the service is installed and you have appropriate permissions."

270 )

271 raise typer.Exit(1)