Coverage for src/meshadmin/common/utils.py: 65%

109 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-10 16:08 +0200

1import json 

2import os 

3import platform 

4import stat 

5import subprocess 

6import tempfile 

7from datetime import datetime, timedelta, timezone 

8from pathlib import Path 

9 

10import httpx 

11import structlog 

12 

13logger = structlog.get_logger(__name__) 

14 

15 

16def get_nebula_install_path(): 

17 os_name = platform.system() 

18 

19 if os_name == "Darwin": 

20 base_path = Path(os.path.expanduser("~/Library/Application Support/nebula")) 

21 elif os_name == "Linux": 

22 base_path = Path("/opt/nebula") 

23 else: 

24 raise NotImplementedError(f"Unsupported operating system: {os_name}") 

25 

26 if not base_path.exists(): 

27 base_path.mkdir(parents=True, exist_ok=True) 

28 

29 return base_path 

30 

31 

32def download_nebula_binaries(mesh_admin_endpoint): 

33 os_name = platform.system() 

34 if os_name not in ["Linux", "Darwin"]: 

35 raise NotImplementedError( 

36 f"Unsupported operating system: {os_name}. Only Linux and Darwin are supported." 

37 ) 

38 

39 architecture = platform.machine() 

40 

41 binaries = ["nebula", "nebula-cert"] 

42 install_path = get_nebula_install_path() 

43 

44 for binary in binaries: 

45 binary_path = install_path / binary 

46 

47 if binary_path.exists(): 

48 logger.info(f"{binary} binary already exists", path=str(binary_path)) 

49 continue 

50 

51 url = f"{mesh_admin_endpoint}/api/v1/nebula/download/{os_name}/{architecture}/{binary}" 

52 logger.info(f"Downloading {binary}", url=url) 

53 

54 try: 

55 with httpx.stream("GET", url) as response: 

56 response.raise_for_status() 

57 with open(binary_path, "wb") as f: 

58 for chunk in response.iter_bytes(): 

59 f.write(chunk) 

60 

61 # Make the binary executable 

62 binary_path.chmod(binary_path.stat().st_mode | stat.S_IEXEC) 

63 logger.info(f"{binary} downloaded and installed", path=str(binary_path)) 

64 

65 except httpx.HTTPStatusError as e: 

66 logger.error( 

67 f"Failed to download {binary}", status_code=e.response.status_code 

68 ) 

69 raise RuntimeError(f"Failed to download {binary}: {e}") 

70 

71 return install_path 

72 

73 

74def get_nebula_path(): 

75 return get_nebula_binary_path() 

76 

77 

78def get_nebula_binary_path(): 

79 install_path = get_nebula_install_path() 

80 return install_path / "nebula" 

81 

82 

83def get_nebula_cert_binary_path(): 

84 install_path = get_nebula_install_path() 

85 return install_path / "nebula-cert" 

86 

87 

88def create_keys(): 

89 with tempfile.TemporaryDirectory() as tmpdir: 

90 res = subprocess.run( 

91 [ 

92 get_nebula_cert_binary_path(), 

93 "keygen", 

94 "-out-key", 

95 "private.key", 

96 "-out-pub", 

97 "public.key", 

98 ], 

99 capture_output=True, 

100 text=True, 

101 cwd=tmpdir, 

102 ) 

103 if res.returncode != 0: 

104 raise RuntimeError(f"Failed to create keys: {res.stderr}") 

105 

106 private_key = (Path(tmpdir) / "private.key").read_text() 

107 public_key = (Path(tmpdir) / "public.key").read_text() 

108 

109 return private_key, public_key 

110 

111 

112def sign_keys( 

113 ca_key: str, 

114 ca_crt: str, 

115 public_key: str, 

116 name: str, 

117 ip: str, 

118 groups: frozenset[str], 

119): 

120 with tempfile.TemporaryDirectory() as tmpdir: 

121 temp_path = Path(tmpdir) 

122 

123 ca_crt_path = temp_path / "ca.crt" 

124 ca_crt_path.write_text(ca_crt) 

125 ca_key_path = temp_path / "ca.key" 

126 ca_key_path.write_text(ca_key) 

127 

128 public_key_file = temp_path / "public.key" 

129 public_key_file.write_text(public_key) 

130 

131 command = [ 

132 str(get_nebula_cert_binary_path()), 

133 "sign", 

134 "-in-pub", 

135 "public.key", 

136 "-name", 

137 name, 

138 "-ip", 

139 ip, 

140 "-groups", 

141 " " + ",".join(groups) + " ", 

142 "-out-crt", 

143 "public.crt", 

144 ] 

145 print(f"cd {temp_path}") 

146 print(" ".join(command)) 

147 

148 res = subprocess.run( 

149 command, 

150 capture_output=True, 

151 text=True, 

152 cwd=tmpdir, 

153 ) 

154 assert res.returncode == 0, res.stderr 

155 cert = (Path(tmpdir) / "public.crt").read_text() 

156 return cert 

157 

158 

159def print_ca(cert: str): 

160 with tempfile.TemporaryDirectory() as tmpdir: 

161 temp_path = Path(tmpdir) 

162 

163 cert_path = temp_path / "cert.crt" 

164 cert_path.write_text(cert) 

165 

166 command = [ 

167 get_nebula_cert_binary_path(), 

168 "print", 

169 "-path", 

170 "cert.crt", 

171 "-json", 

172 ] 

173 res = subprocess.run( 

174 command, 

175 capture_output=True, 

176 text=True, 

177 cwd=tmpdir, 

178 ) 

179 

180 assert res.returncode == 0, res.stderr 

181 output = res.stdout 

182 json_output = json.loads(output) 

183 return json_output 

184 

185 

186def create_ca(ca_name): 

187 with tempfile.TemporaryDirectory() as tmpdir: 

188 res = subprocess.run( 

189 [str(get_nebula_cert_binary_path()), "ca", "-name", ca_name], 

190 capture_output=True, 

191 text=True, 

192 cwd=tmpdir, 

193 ) 

194 assert res.returncode == 0 

195 cert = (Path(tmpdir) / "ca.crt").read_text() 

196 key = (Path(tmpdir) / "ca.key").read_text() 

197 

198 return cert, key 

199 

200 

201def create_expiration_date(minutes=60) -> int: 

202 return int( 

203 ( 

204 datetime.utcnow().replace(tzinfo=timezone.utc) + timedelta(minutes) 

205 ).timestamp() 

206 ) 

207 

208 

209def get_public_ip(): 

210 res = httpx.get("https://checkip.amazonaws.com/") 

211 assert res.status_code == 200 

212 public_ip = res.text.strip() 

213 return public_ip 

214 

215 

216def get_default_config_path() -> Path: 

217 os_name = platform.system() 

218 if os_name == "Darwin": 

219 return Path(os.path.expanduser("~/Library/Application Support/meshadmin")) 

220 else: 

221 return Path("/etc/meshadmin")