Coverage for gws-app/gws/server/manager.py: 57%

115 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +0200

1"""Server manager, which creates the server configuration files and the start script.""" 

2 

3from typing import cast 

4 

5import os 

6import re 

7 

8import gws 

9import gws.config 

10import gws.config.util 

11import gws.lib.osx 

12 

13from . import core 

14 

15 

16class TemplateArgs(gws.TemplateArgs): 

17 """Arguments for configuration templates.""" 

18 

19 root: gws.Root 

20 """Root object.""" 

21 serverDir: str 

22 """Absolute path to the ``gws/server`` directory.""" 

23 gwsEnv: dict 

24 """Environment variables whose names start with ``GWS_``.""" 

25 inContainer: bool 

26 """True if running in a container.""" 

27 uwsgi: str 

28 """uWSGI backend name, ``web`` or ``spool``, for the uWSGI config template.""" 

29 userName: str 

30 """Name of the user the servers run as.""" 

31 groupName: str 

32 """Group name of that user.""" 

33 homeDir: str 

34 """Home directory of that user.""" 

35 nginxConfig: str 

36 """nginx config path.""" 

37 nginxPid: str 

38 """nginx pid path.""" 

39 spoolConfig: str 

40 """Spooler config path.""" 

41 spoolPid: str 

42 """Spooler pid path.""" 

43 spoolSocket: str 

44 """Spooler socket path.""" 

45 syslogConfig: str 

46 """Syslog config path.""" 

47 syslogPid: str 

48 """Syslog pid path.""" 

49 webConfig: str 

50 """Web server config path.""" 

51 webPid: str 

52 """Web server pid path.""" 

53 webSocket: str 

54 """Web server socket path.""" 

55 

56 

57_DEFAULT_BASE_TIMEOUT = 60 

58_DEFAULT_SPOOL_TIMEOUT = 300 

59 

60_SERVER_DIR = gws.u.dirname(__file__) 

61 

62_DEFAULT_TEMPLATES = [ 

63 gws.Config(type='text', subject='server.rsyslog_config', path=f'{_SERVER_DIR}/templates/rsyslog_config.cx.txt'), 

64 gws.Config(type='text', subject='server.nginx_config', path=f'{_SERVER_DIR}/templates/nginx_config.cx.txt'), 

65 gws.Config(type='text', subject='server.uwsgi_config', path=f'{_SERVER_DIR}/templates/uwsgi_config.cx.txt'), 

66 gws.Config(type='text', subject='server.start_script', path=f'{_SERVER_DIR}/templates/start_script.cx.txt'), 

67] 

68 

69 

70class Object(gws.ServerManager): 

71 """Server manager.""" 

72 

73 def configure(self): 

74 self.config = self._add_defaults(self.config, 'gws.server.core.Config') 

75 self.config.log = self._add_defaults(self.config.log, 'gws.server.core.LogConfig') 

76 self.config.monitor = self._add_defaults(self.config.monitor, 'gws.server.core.MonitorConfig') 

77 self.config.qgis = self._add_defaults(self.config.qgis, 'gws.server.core.QgisConfig') 

78 self.config.spool = self._add_defaults(self.config.spool, 'gws.server.core.SpoolConfig') 

79 self.config.web = self._add_defaults(self.config.web, 'gws.server.core.WebConfig') 

80 

81 # deprecated 'enabled' keys 

82 if self.config.spool.enabled is not None: 

83 self.root.config_warning('"server.spool.enabled" is deprecated, use "server.withSpool"') 

84 if self.config.spool.enabled is False: 

85 self.config.withSpool = False 

86 if self.config.web.enabled is not None: 

87 self.root.config_warning('"server.web.enabled" is deprecated, use "server.withWeb"') 

88 if self.config.web.enabled is False: 

89 self.config.withWeb = False 

90 if self.config.monitor.enabled is not None: 

91 self.root.config_warning('"server.monitor.enabled" is deprecated, use "server.withMonitor"') 

92 if self.config.monitor.enabled is False: 

93 self.config.withMonitor = False 

94 if self.config.mapproxy is not None: 

95 self.root.config_warning('"server.mapproxy" is deprecated and ignored') 

96 if self.config.withMapproxy is not None: 

97 self.root.config_warning('"server.withMapproxy" is deprecated and ignored') 

98 if self.config.autoRun: 

99 self.root.config_warning('"server.autoRun" is deprecated and ignored') 

100 if self.config.timeZone is not None: 

101 self.root.config_warning('"server.timeZone" is deprecated and ignored, set the "TZ" environment variable') 

102 

103 if self.config.withMonitor and not self.config.withSpool: 

104 raise gws.ConfigurationError('"server.withMonitor" requires "server.withSpool"') 

105 

106 self.configure_environment() 

107 self.configure_templates() 

108 

109 def _add_defaults(self, value, type_name): 

110 """Merge a config value into the defaults of the given config type.""" 

111 return gws.u.merge(self.root.specs.read({}, type_name), value) 

112 

113 def configure_environment(self): 

114 """Overwrite the log level and the numbers of workers from the environment.""" 

115 

116 cfg = cast(core.Config, self.config) 

117 p = gws.env.GWS_LOG_LEVEL 

118 if p: 

119 cfg.log.level = p 

120 p = gws.env.GWS_WEB_WORKERS 

121 if p: 

122 cfg.web.workers = int(p) 

123 p = gws.env.GWS_SPOOL_WORKERS 

124 if p: 

125 cfg.spool.workers = int(p) 

126 

127 def configure_templates(self): 

128 """Create the templates from the ``templates`` config and add the default templates.""" 

129 gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES) 

130 

131 def create_server_configs(self, target_dir, script_path, pid_paths): 

132 ui = gws.lib.osx.user_info(gws.c.UID, gws.c.GID) 

133 

134 args = TemplateArgs( 

135 root=self.root, 

136 serverDir=_SERVER_DIR, 

137 gwsEnv={k: v for k, v in sorted(os.environ.items()) if k.startswith('GWS_')}, 

138 inContainer=gws.c.env.GWS_IN_CONTAINER, 

139 uwsgi='', 

140 userName=ui['pw_name'], 

141 groupName=ui['gr_name'], 

142 homeDir=ui['pw_dir'], 

143 nginxConfig='', 

144 nginxPid=pid_paths['nginx'], 

145 spoolConfig='', 

146 spoolPid=pid_paths['spool'], 

147 spoolSocket=f'{gws.c.TMP_DIR}/spool.uwsgi.sock', 

148 syslogConfig='', 

149 syslogPid='', 

150 webConfig='', 

151 webPid=pid_paths['web'], 

152 webSocket=f'{gws.c.TMP_DIR}/web.uwsgi.sock', 

153 ) 

154 

155 if args.inContainer: 

156 args.syslogPid = f'{gws.c.PIDS_DIR}/rsyslogd.pid' 

157 args.syslogConfig = self._create_config('server.rsyslog_config', f'{target_dir}/syslog.conf', args) 

158 

159 if self.cfg('withWeb'): 

160 args.uwsgi = 'web' 

161 args.webConfig = self._create_config('server.uwsgi_config', f'{target_dir}/uwsgi_web.ini', args) 

162 

163 if self.cfg('withSpool'): 

164 args.uwsgi = 'spool' 

165 args.spoolConfig = self._create_config('server.uwsgi_config', f'{target_dir}/uwsgi_spool.ini', args) 

166 

167 args.nginxConfig = self._create_config('server.nginx_config', f'{target_dir}/nginx.conf', args, True) 

168 

169 self._create_config('server.start_script', script_path, args) 

170 

171 def _create_config(self, subject: str, path: str, args: TemplateArgs, is_nginx=False) -> str: 

172 """Render a template, normalize the whitespace and write the result to a file, return the path. 

173 

174 The NGINX config is reformatted with one statement per line and indented blocks. 

175 """ 

176 tpl = gws.u.require(self.root.app.templateMgr.find_template(subject, where=[self])) 

177 res = tpl.render(gws.TemplateRenderInput(args=args)) 

178 

179 text = str(res.content) 

180 if is_nginx: 

181 text = re.sub(r'\s*{\s*', ' {\n', text) 

182 text = re.sub(r'\s*}\s*', '\n}\n', text) 

183 

184 lines = [] 

185 indent = 0 

186 

187 for line in text.split('\n'): 

188 line = re.sub(r'\s+', ' ', line.strip()) 

189 if not line: 

190 continue 

191 if is_nginx: 

192 if line == '}': 

193 indent -= 1 

194 lines.append((' ' * (indent * 4)) + line) 

195 if '{' in line: 

196 indent += 1 

197 else: 

198 lines.append(line) 

199 

200 text = '\n'.join(lines) + '\n' 

201 gws.u.write_file(path, text) 

202 return path