|
@@ -4,6 +4,7 @@ import json
|
4
|
4
|
import argparse
|
5
|
5
|
import os
|
6
|
6
|
import subprocess
|
|
7
|
+import argcomplete
|
7
|
8
|
|
8
|
9
|
from os import path
|
9
|
10
|
|
|
@@ -26,6 +27,7 @@ class SiteGen:
|
26
|
27
|
templatesDir = ""
|
27
|
28
|
certRenewTime = ""
|
28
|
29
|
letsencryptCommand = ""
|
|
30
|
+ letsencryptArgs = []
|
29
|
31
|
letsencryptDir = ""
|
30
|
32
|
certDir = ""
|
31
|
33
|
|
|
@@ -38,12 +40,18 @@ class SiteGen:
|
38
|
40
|
self.templatesDir = path.join(self.confDir, "templates")
|
39
|
41
|
self.certRenewTime = config["certRenewTime"]
|
40
|
42
|
self.letsencryptCommand = config["letsencryptCommand"]
|
|
43
|
+ self.letsencryptArgs = config["letsencryptArgs"]
|
41
|
44
|
self.letsencryptDir = config["letsencryptDir"]
|
42
|
45
|
self.certDir = config["certDir"]
|
43
|
46
|
|
|
47
|
+ def make_dirs_p(self, folder):
|
|
48
|
+ if not path.isdir(folder):
|
|
49
|
+ os.makedirs(folder)
|
|
50
|
+
|
44
|
51
|
def make_dirs(self):
|
45
|
|
- if not path.isdir(self.certDir):
|
46
|
|
- os.makedirs(self.certDir)
|
|
52
|
+ self.make_dirs_p(self.certDir)
|
|
53
|
+ self.make_dirs_p(self.siteConfDir)
|
|
54
|
+ self.make_dirs_p(self.siteDir)
|
47
|
55
|
|
48
|
56
|
def get_hook_dir(self, hook_type, is_enabled):
|
49
|
57
|
return path.join(self.hooksEnabledDir if is_enabled else self.hooksAvailableDir, hook_type)
|
|
@@ -79,12 +87,14 @@ class SiteGen:
|
79
|
87
|
path.abspath(path.join(self.certDir, domain + "-chain.crt"))]
|
80
|
88
|
|
81
|
89
|
def execute(self, exe, args, get_output):
|
|
90
|
+ args = args.copy()
|
82
|
91
|
args.insert(0, exe)
|
83
|
92
|
proc = subprocess.Popen(args, stdout=(subprocess.PIPE if get_output else None))
|
84
|
93
|
out = proc.communicate()
|
85
|
94
|
return proc.returncode, out[0]
|
86
|
95
|
|
87
|
96
|
def execute_hooks(self, hook_type, hook_event, args):
|
|
97
|
+ args = args.copy()
|
88
|
98
|
args.insert(0, hook_event)
|
89
|
99
|
for hook_name in self.get_hook_files(hook_type, True):
|
90
|
100
|
self.execute(self.get_hook_file(hook_type, hook_name, True), args, False)
|
|
@@ -108,7 +118,7 @@ class SiteGen:
|
108
|
118
|
res, out = self.execute("openssl", ["x509", "-noout", "-in", cert_files[0], "-checkend", str(checkend)], True)
|
109
|
119
|
return res == 1
|
110
|
120
|
|
111
|
|
- def get_all_domains(self):
|
|
121
|
+ def get_all_certs(self):
|
112
|
122
|
files = os.listdir(self.certDir)
|
113
|
123
|
files.sort()
|
114
|
124
|
domains = []
|
|
@@ -117,13 +127,77 @@ class SiteGen:
|
117
|
127
|
domains.append(file[:-4])
|
118
|
128
|
return domains
|
119
|
129
|
|
120
|
|
- def cert_request(self, domain, logger):
|
|
130
|
+ def get_all_site_templates(self):
|
|
131
|
+ files = os.listdir(self.get_site_template_dir())
|
|
132
|
+ files.sort()
|
|
133
|
+ templates = []
|
|
134
|
+ for file in files:
|
|
135
|
+ if file.endswith(".conf"):
|
|
136
|
+ templates.append(file[:-5])
|
|
137
|
+ return templates
|
|
138
|
+
|
|
139
|
+ def get_all_sites(self):
|
|
140
|
+ files = os.listdir(self.siteConfDir)
|
|
141
|
+ files.sort()
|
|
142
|
+ templates = []
|
|
143
|
+ for file in files:
|
|
144
|
+ if file.endswith(".conf"):
|
|
145
|
+ templates.append(file[:-5])
|
|
146
|
+ return templates
|
|
147
|
+
|
|
148
|
+ def get_all_domains(self):
|
|
149
|
+ domains = list(set(self.get_all_sites() + self.get_all_certs()))
|
|
150
|
+ domains.sort()
|
|
151
|
+ return domains
|
|
152
|
+
|
|
153
|
+ def get_site_template_dir(self):
|
|
154
|
+ return path.join(self.confDir, "templates")
|
|
155
|
+
|
|
156
|
+ def get_site_conf_files(self, domain):
|
|
157
|
+ return [
|
|
158
|
+ path.join(self.siteConfDir, domain + ".conf"),
|
|
159
|
+ path.join(self.siteConfDir, domain + ".include")
|
|
160
|
+ ]
|
|
161
|
+
|
|
162
|
+ def get_site_template_conf_files(self, template):
|
|
163
|
+ return [
|
|
164
|
+ path.join(self.get_site_template_dir(), template + ".conf"),
|
|
165
|
+ path.join(self.get_site_template_dir(), template + ".include")
|
|
166
|
+ ]
|
|
167
|
+
|
|
168
|
+ def get_site_dir(self, domain):
|
|
169
|
+ return path.abspath(path.join(self.siteDir, domain))
|
|
170
|
+
|
|
171
|
+ def is_site_present(self, domain):
|
|
172
|
+ for file in self.get_site_conf_files(domain):
|
|
173
|
+ if path.isfile(file):
|
|
174
|
+ return True
|
|
175
|
+ return False
|
|
176
|
+
|
|
177
|
+ def is_site_template_present(self, template):
|
|
178
|
+ for file in self.get_site_template_conf_files(template):
|
|
179
|
+ if path.isfile(file):
|
|
180
|
+ return True
|
|
181
|
+ return False
|
|
182
|
+
|
|
183
|
+ def generate_site_conf_file(self, domain, template, outfile):
|
|
184
|
+ with open(template) as f:
|
|
185
|
+ content = f.read()
|
|
186
|
+ content = content.replace("%%HOST%%", domain).replace("%%ROOT%%", self.get_site_dir(domain))
|
|
187
|
+ with open(outfile, "w") as f:
|
|
188
|
+ f.write(content)
|
|
189
|
+
|
|
190
|
+ def cert_request(self, domain):
|
121
|
191
|
|
122
|
192
|
cert_files = self.get_cert_files(domain)
|
123
|
193
|
cert_files.insert(0, domain)
|
124
|
194
|
self.execute_hooks("cert", "pre", cert_files)
|
125
|
195
|
|
126
|
|
- res, out = self.execute(self.letsencryptCommand, [domain], False)
|
|
196
|
+ args = self.letsencryptArgs.copy()
|
|
197
|
+ args.append("-d")
|
|
198
|
+ args.append(domain)
|
|
199
|
+
|
|
200
|
+ res, out = self.execute(self.letsencryptCommand, args, False)
|
127
|
201
|
if res != 0:
|
128
|
202
|
raise SiteGenException("Certificate request failed with code %i" % res, res)
|
129
|
203
|
|
|
@@ -133,91 +207,158 @@ class SiteGen:
|
133
|
207
|
|
134
|
208
|
self.execute_hooks("cert", "post", cert_files)
|
135
|
209
|
|
136
|
|
- def certs_request(self, domains, logger):
|
|
210
|
+ def certs_request(self, domains):
|
137
|
211
|
for domain in domains:
|
138
|
|
- self.cert_request(domain, logger)
|
|
212
|
+ self.cert_request(domain)
|
139
|
213
|
|
140
|
|
- def cert_check(self, domain, logger):
|
|
214
|
+ def cert_check(self, domain):
|
141
|
215
|
if not self.is_cert_present(domain):
|
142
|
216
|
raise SiteGenException("Certificate not present: %s" % domain, 1)
|
143
|
217
|
if self.is_cert_gonna_expire(domain, self.certRenewTime):
|
144
|
|
- logger("%s: %s" % (domain, self.get_cert_end_date(domain)))
|
|
218
|
+ print("%s: %s" % (domain, self.get_cert_end_date(domain)))
|
145
|
219
|
return True
|
146
|
220
|
return False
|
147
|
221
|
|
148
|
|
- def certs_check(self, domains, logger):
|
|
222
|
+ def certs_check(self, domains):
|
149
|
223
|
for domain in domains:
|
150
|
|
- self.cert_check(domain, logger)
|
|
224
|
+ self.cert_check(domain)
|
151
|
225
|
|
152
|
|
- def cert_enddate(self, domain, logger):
|
|
226
|
+ def cert_enddate(self, domain):
|
153
|
227
|
if not self.is_cert_present(domain):
|
154
|
228
|
raise SiteGenException("Certificate not present: %s" % domain, 1)
|
155
|
|
- logger("%s: %s" % (domain, self.get_cert_end_date(domain)))
|
|
229
|
+ print("%s: %s" % (domain, self.get_cert_end_date(domain)))
|
156
|
230
|
|
157
|
|
- def certs_enddate(self, domains, logger):
|
|
231
|
+ def certs_enddate(self, domains):
|
158
|
232
|
for domain in domains:
|
159
|
|
- self.cert_enddate(domain, logger)
|
|
233
|
+ self.cert_enddate(domain)
|
160
|
234
|
|
161
|
|
- def cert_renew(self, domain, logger):
|
162
|
|
- if self.cert_check(domain, logger):
|
163
|
|
- self.cert_request(domain, logger)
|
|
235
|
+ def cert_renew(self, domain):
|
|
236
|
+ if self.cert_check(domain):
|
|
237
|
+ self.cert_request(domain)
|
164
|
238
|
|
165
|
|
- def certs_renew(self, domains, logger):
|
|
239
|
+ def certs_renew(self, domains):
|
166
|
240
|
for domain in domains:
|
167
|
|
- self.cert_renew(domain, logger)
|
|
241
|
+ self.cert_renew(domain)
|
|
242
|
+
|
|
243
|
+ def site_create(self, domain, template):
|
|
244
|
+ if self.is_site_present(domain):
|
|
245
|
+ raise SiteGenException("Site is present", 1)
|
|
246
|
+ if not self.is_site_template_present(template):
|
|
247
|
+ raise SiteGenException("Template is not present", 1)
|
|
248
|
+
|
|
249
|
+ args = [domain, self.get_site_dir(domain)]
|
|
250
|
+ conf_files = self.get_site_template_conf_files(template)
|
|
251
|
+ site_files = self.get_site_conf_files(domain)
|
|
252
|
+ for f in conf_files:
|
|
253
|
+ args.append(f)
|
|
254
|
+ for f in site_files:
|
|
255
|
+ args.append(f)
|
|
256
|
+
|
|
257
|
+ self.execute_hooks("site", "pre", args)
|
168
|
258
|
|
169
|
|
- def site_create(self, domain, logger):
|
170
|
|
- pass
|
|
259
|
+ i = 0
|
|
260
|
+ while i < len(conf_files):
|
|
261
|
+ self.generate_site_conf_file(domain, conf_files[i], site_files[i])
|
|
262
|
+ i += 1
|
171
|
263
|
|
172
|
|
- def site_remove(self, domain, logger):
|
173
|
|
- pass
|
|
264
|
+ self.execute_hooks("site", "post", args)
|
174
|
265
|
|
175
|
|
- def hook_enable(self, hook_type, hook_name, logger):
|
176
|
|
- if not self.is_hook_present(hook_type, hook_name, False):
|
|
266
|
+ def hook_enable(self, hook_type, hook_name):
|
|
267
|
+ if hook_type is None or not self.is_hook_present(hook_type, hook_name, False):
|
177
|
268
|
raise SiteGenException("Hook is not present", 1)
|
178
|
269
|
if self.is_hook_enabled(hook_type, hook_name):
|
179
|
270
|
raise SiteGenException("Hook is already enabled", 0)
|
180
|
|
- logger("Enabling %s %s" % (hook_type, hook_name))
|
|
271
|
+ print("Enabling %s %s" % (hook_type, hook_name))
|
181
|
272
|
hook_dir = self.get_hook_dir(hook_type, hook_name)
|
182
|
|
- if not path.isdir(hook_dir):
|
183
|
|
- os.makedirs(hook_dir)
|
|
273
|
+ self.make_dirs_p(hook_dir)
|
184
|
274
|
hook_file_available = self.get_hook_file(hook_type, hook_name, False)
|
185
|
275
|
hook_file_enabled = self.get_hook_file(hook_type, hook_name, True)
|
186
|
276
|
hook_relative_file = path.relpath(hook_file_available, self.get_hook_dir(hook_type, True))
|
187
|
277
|
|
188
|
278
|
os.symlink(hook_relative_file, hook_file_enabled)
|
189
|
279
|
|
190
|
|
- def hook_disable(self, hook_type, hook_name, logger):
|
191
|
|
- if not self.is_hook_present(hook_type, hook_name, False):
|
|
280
|
+ def hook_disable(self, hook_type, hook_name):
|
|
281
|
+ if hook_type is None or not self.is_hook_present(hook_type, hook_name, False):
|
192
|
282
|
raise SiteGenException("Hook is not present", 1)
|
193
|
283
|
if not self.is_hook_enabled(hook_type, hook_name):
|
194
|
284
|
raise SiteGenException("Hook is not enabled", 0)
|
195
|
|
- logger("Disabling %s %s" % (hook_type, hook_name))
|
|
285
|
+ print("Disabling %s %s" % (hook_type, hook_name))
|
196
|
286
|
os.remove(self.get_hook_file(hook_type, hook_name, True))
|
197
|
287
|
|
198
|
288
|
|
|
289
|
+def parse_domain(domain):
|
|
290
|
+ site_template = "default"
|
|
291
|
+ if ":" in domain:
|
|
292
|
+ split = domain.split(":")
|
|
293
|
+ domain = split[0]
|
|
294
|
+ site_template = split[1]
|
|
295
|
+ return domain, site_template
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+def parse_hook(hook):
|
|
299
|
+ if "." in hook:
|
|
300
|
+ split = hook.split(".")
|
|
301
|
+ return split[0], split[1]
|
|
302
|
+ return None, None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+def get_site_gen(prefix, **kwargs):
|
|
306
|
+ with open(kwargs.get("parsed_args").config, "r") as f:
|
|
307
|
+ config = json.load(f)
|
|
308
|
+ return SiteGen(config)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+def cert_completer(prefix, **kwargs):
|
|
312
|
+ site_gen = get_site_gen(prefix, **kwargs)
|
|
313
|
+ return site_gen.get_all_certs()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+def domain_completer(prefix, **kwargs):
|
|
317
|
+ site_gen = get_site_gen(prefix, **kwargs)
|
|
318
|
+ return site_gen.get_all_domains()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+def site_template_completer(prefix, **kwargs):
|
|
322
|
+ site_gen = get_site_gen(prefix, **kwargs)
|
|
323
|
+ return site_gen.get_all_site_templates()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+def site_create_completer(prefix, **kwargs):
|
|
327
|
+ if ":" in prefix:
|
|
328
|
+ domain, template = parse_domain(prefix)
|
|
329
|
+ templates = site_template_completer(prefix, **kwargs)
|
|
330
|
+ return [domain + ":" + elt for elt in templates]
|
|
331
|
+
|
|
332
|
+ return domain_completer(prefix, **kwargs)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+def hook_completer(prefix, **kwargs):
|
|
336
|
+ site_gen = get_site_gen(prefix, **kwargs)
|
|
337
|
+ return site_gen.get_all_certs()
|
|
338
|
+
|
|
339
|
+
|
199
|
340
|
def main():
|
200
|
341
|
parser = argparse.ArgumentParser(description='Manage apache websites and SSL certificates')
|
201
|
342
|
parser.add_argument('--config', dest='config', default='/etc/sitegen/sitegen.json', help='Configuration file path')
|
202
|
343
|
|
203
|
|
- parser.add_argument('--cert-request', metavar='cert_request', const='', nargs='?',
|
204
|
|
- help='Request/renew a certificate. Request/renew all certificates if no domain is specified')
|
205
|
|
- parser.add_argument('--cert-check', metavar='cert_check', const='', nargs='?',
|
206
|
|
- help='Check if certificate needs to be renewed. Check all if no domain is specified')
|
207
|
|
- parser.add_argument('--cert-renew', metavar='cert_renew', const='', nargs='?',
|
208
|
|
- help='Renew certificate if it needs to be. Renew all that needs to be if no domain is specified')
|
209
|
|
- parser.add_argument('--cert-enddate', metavar='cert_enddate', const='', nargs='?',
|
210
|
|
- help='Print certificate enddate. Print all certificates enddate if no domain is specified')
|
|
344
|
+ parser.add_argument('--cert-request', metavar='domain', const='', nargs='?',
|
|
345
|
+ help='Request/renew a certificate. Request/renew all certificates if no domain is specified').completer = domain_completer
|
|
346
|
+ parser.add_argument('--cert-check', metavar='domain', const='', nargs='?',
|
|
347
|
+ help='Check if certificate needs to be renewed. Check all if no domain is specified').completer = cert_completer
|
|
348
|
+ parser.add_argument('--cert-renew', metavar='domain', const='', nargs='?',
|
|
349
|
+ help='Renew certificate if it needs to be. Renew all that needs to be if no domain is specified').completer = cert_completer
|
|
350
|
+ parser.add_argument('--cert-enddate', metavar='enddate', const='', nargs='?',
|
|
351
|
+ help='Print certificate enddate. Print all certificates enddate if no domain is specified').completer = cert_completer
|
211
|
352
|
|
212
|
|
- parser.add_argument('--site-create', help='Create a site configuration', metavar='site_create')
|
213
|
|
- parser.add_argument('--site-remove', help='Remove a site configuration', metavar='site_remove')
|
|
353
|
+ parser.add_argument('--site-create', help='Create a site configuration in the form domain[:template]',
|
|
354
|
+ metavar='domain').completer = site_create_completer
|
214
|
355
|
|
215
|
|
- parser.add_argument('--hook-site-enable', help='Enable a site hook', dest='site_hook_enable', metavar='hook')
|
216
|
|
- parser.add_argument('--hook-site-disable', help='Disable a site hook', dest='site_hook_disable', metavar='hook')
|
217
|
|
-
|
218
|
|
- parser.add_argument('--hook-cert-enable', help='Enable a certificate hook', dest='hook_cert_enable', metavar='hook')
|
219
|
|
- parser.add_argument('--hook-cert-disable', help='Disable a certificate hook', dest='hook_cert_disable', metavar='hook')
|
|
356
|
+ parser.add_argument('--hook-enable', help='Enable a site hook in the form (site|cert):hook_name',
|
|
357
|
+ dest='hook_enable', metavar='hook').completer = hook_completer
|
|
358
|
+ parser.add_argument('--hook-disable', help='Disable a site hook in the form (site|cert):hook_name',
|
|
359
|
+ dest='hook_disable', metavar='hook').completer = hook_completer
|
220
|
360
|
|
|
361
|
+ argcomplete.autocomplete(parser)
|
221
|
362
|
args = parser.parse_args()
|
222
|
363
|
|
223
|
364
|
with open(args.config, "r") as f:
|
|
@@ -227,50 +368,42 @@ def main():
|
227
|
368
|
|
228
|
369
|
site_gen.make_dirs()
|
229
|
370
|
|
230
|
|
- logger = print
|
231
|
|
-
|
232
|
371
|
try:
|
233
|
372
|
if args.cert_request is not None:
|
234
|
373
|
if args.cert_request == "":
|
235
|
|
- site_gen.certs_request(site_gen.get_all_domains(), logger)
|
|
374
|
+ site_gen.certs_request(site_gen.get_all_certs())
|
236
|
375
|
else:
|
237
|
|
- site_gen.cert_request(args.cert_request, logger)
|
|
376
|
+ site_gen.cert_request(args.cert_request)
|
238
|
377
|
|
239
|
378
|
elif args.cert_check is not None:
|
240
|
379
|
if args.cert_check == "":
|
241
|
|
- site_gen.certs_check(site_gen.get_all_domains(), logger)
|
|
380
|
+ site_gen.certs_check(site_gen.get_all_certs())
|
242
|
381
|
else:
|
243
|
|
- site_gen.cert_check(args.cert_check, logger)
|
|
382
|
+ site_gen.cert_check(args.cert_check)
|
244
|
383
|
|
245
|
384
|
elif args.cert_renew is not None:
|
246
|
385
|
if args.cert_renew == "":
|
247
|
|
- site_gen.certs_renew(site_gen.get_all_domains(), logger)
|
|
386
|
+ site_gen.certs_renew(site_gen.get_all_certs())
|
248
|
387
|
else:
|
249
|
|
- site_gen.cert_renew(args.cert_renew, logger)
|
|
388
|
+ site_gen.cert_renew(args.cert_renew)
|
250
|
389
|
|
251
|
390
|
elif args.cert_enddate is not None:
|
252
|
391
|
if args.cert_enddate == "":
|
253
|
|
- site_gen.certs_enddate(site_gen.get_all_domains(), logger)
|
|
392
|
+ site_gen.certs_enddate(site_gen.get_all_certs())
|
254
|
393
|
else:
|
255
|
|
- site_gen.cert_enddate(args.cert_enddate, logger)
|
|
394
|
+ site_gen.cert_enddate(args.cert_enddate)
|
256
|
395
|
|
257
|
396
|
elif args.site_create is not None:
|
258
|
|
- site_gen.site_create(args.site_create, logger)
|
259
|
|
-
|
260
|
|
- elif args.site_remove is not None:
|
261
|
|
- site_gen.site_remove(args.site_remove, logger)
|
262
|
|
-
|
263
|
|
- elif args.site_hook_enable is not None:
|
264
|
|
- site_gen.hook_enable("site", args.site_hook_enable, logger)
|
265
|
|
-
|
266
|
|
- elif args.site_hook_disable is not None:
|
267
|
|
- site_gen.hook_disable("site", args.site_hook_disable, logger)
|
|
397
|
+ domain, site_template = parse_domain(args.site_create)
|
|
398
|
+ site_gen.site_create(domain, site_template)
|
268
|
399
|
|
269
|
|
- elif args.hook_cert_enable is not None:
|
270
|
|
- site_gen.hook_enable("cert", args.hook_cert_enable, logger)
|
|
400
|
+ elif args.hook_enable is not None:
|
|
401
|
+ hook_type, hook_name = parse_hook(args.hook_enable)
|
|
402
|
+ site_gen.hook_enable(hook_type, hook_name)
|
271
|
403
|
|
272
|
|
- elif args.hook_cert_disable is not None:
|
273
|
|
- site_gen.hook_disable("cert", args.hook_cert_disable, logger)
|
|
404
|
+ elif args.hook_disable is not None:
|
|
405
|
+ hook_type, hook_name = parse_hook(args.hook_disable)
|
|
406
|
+ site_gen.hook_disable(hook_type, hook_name)
|
274
|
407
|
|
275
|
408
|
else:
|
276
|
409
|
parser.print_help()
|