Coverage for src/meshadmin/cli/commands/service.py: 15%

142 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-22 07:09 +0200

1import os 

2import platform 

3import shutil 

4import subprocess 

5import sys 

6from pathlib import Path 

7from typing import Annotated 

8 

9import structlog 

10import typer 

11 

12from meshadmin.cli.utils import get_context_config 

13 

14service_app = typer.Typer() 

15logger = structlog.get_logger(__name__) 

16 

17 

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

19def 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] 

78Description=Meshadmin {context_name} 

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

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

81Before=sshd.service 

82 

83[Service] 

84#Type=notify 

85#NotifyAccess=main 

86SyslogIdentifier={context_name} 

87EnvironmentFile={network_dir}/env 

88ExecReload=/bin/kill -HUP $MAINPID 

89ExecStart={meshadmin_path} nebula start 

90Restart=always 

91 

92[Install] 

93WantedBy=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") 

107def 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") 

150def 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") 

176def 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") 

202def 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)