-
Notifications
You must be signed in to change notification settings - Fork 124
/
ring-admin
executable file
·2047 lines (1791 loc) · 72.8 KB
/
ring-admin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
import datetime
import getpass
from string import Template
from subprocess import check_output, CalledProcessError
import MySQLdb as mdb
import inspect
import os
import pycountry
import re
import simplejson
import smtplib
import string
import sys
import time
import urllib.request
import unicodedata
from unidecode import unidecode
DBHOST = 'localhost'
DBUSER = 'ring-admins'
DBPASS = ''
DB = 'ring'
ZONE = 'ring.nlnog.net'
GEOCODE = \
"https://nominatim.openstreetmap.org/reverse?format=json&addressdetails=1&"
ANSIBLEDIR = '/var/ring/ring-ansible'
ANSIBLE_HOSTFILE = 'nodes'
ANSIBLE_HOSTKEYFILE = 'roles/etcfiles/files/ssh_known_hosts'
ANSIBLE_USERFILE = 'roles/users/vars/ring-users.yml'
ANSIBLE_KEYORIGIN = 'auth.infra.ring.nlnog.net:/opt/keys'
ANSIBLE_KEYBASE = 'roles/users/files'
ANSIBLE_KEYDIR = ANSIBLE_KEYBASE + '/keys'
WEBDIR = '/var/ring/ring-web'
WEB_POSTDIR = WEBDIR + '/content/post'
WEB_LOGODIR = WEBDIR + '/content/images/ring-logos'
SCHEMA = '''
CREATE TABLE participants
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
company VARCHAR(255) UNIQUE NOT NULL,
url VARCHAR(255),
contact VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
nocemail VARCHAR(255) NOT NULL,
companydesc VARCHAR(64000),
public INTEGER,
tstamp INTEGER
);
CREATE TABLE users
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255) UNIQUE,
userid INTEGER,
shell VARCHAR(255),
active INTEGER,
participant INTEGER NOT NULL,
admin INTEGER,
email VARCHAR(255),
FOREIGN KEY(participant) REFERENCES participants(id)
);
CREATE TABLE machines
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
hostname VARCHAR(255) UNIQUE NOT NULL,
v4 VARCHAR(255) UNIQUE,
v6 VARCHAR(255) UNIQUE,
autnum INTEGER NOT NULL,
country VARCHAR(2),
state VARCHAR(2),
dc VARCHAR(2048),
geo VARCHAR(255),
owner INTEGER NOT NULL,
tstamp INTEGER,
active BOOLEAN,
alive_v4 TINYINT DEFAULT NULL,
alive_v6 TINYINT DEFAULT NULL,
last_active INTEGER,
FOREIGN KEY(owner) REFERENCES users(id)
);
CREATE TABLE sshkeys
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
keytype VARCHAR(255) NOT NULL,
sshkey VARCHAR(16384) NOT NULL,
keyid VARCHAR(255),
user INTEGER NOT NULL,
FOREIGN KEY(user) REFERENCES users(id)
);
CREATE TABLE sshhostkeys
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
keytype VARCHAR(255) NOT NULL,
sshkey VARCHAR(16384) NOT NULL,
keyid VARCHAR(255),
machine INTEGER NOT NULL,
FOREIGN KEY(machine) REFERENCES machines(id)
);
CREATE TABLE premarks
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
remark VARCHAR(64000),
tstamp INTEGER,
participant INTEGER NOT NULL,
FOREIGN KEY(participant) REFERENCES participants(id)
);
CREATE TABLE mremarks
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
remark VARCHAR(64000),
tstamp INTEGER,
machine INTEGER NOT NULL,
FOREIGN KEY(machine) REFERENCES machines(id)
);
CREATE TABLE ansible
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
hostname VARCHAR(128) DEFAULT NULL,
unreachable TINYINT DEFAULT NULL,
ok INTEGER DEFAULT NULL,
changed INTEGER DEFAULT NULL,
skipped INTEGER DEFAULT NULL,
failures INTEGER DEFAULT NULL
);
CREATE TABLE health
(
id INTEGER PRIMARY KEY AUTO_INCREMENT,
timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
hostname VARCHAR(255) DEFAULT NULL,
family int(11) DEFAULT NULL,
summary VARCHAR(1024) DEFAULT NULL
);
'''
WEBSITETEXT = '''
Title: $company ($countrycode) joined the RING
$company - AS $autnum - joined the RING today.
<blockquote>$companydesc</blockquote>
Users can connect to $hostname, which is located in $country.
'''
ADMINEMAIL='ring-admins@nlnog.net'
RINGUSERSEMAIL='ring-users@nlnog.net'
WELCOMEMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: Welcome to the RING, $company
Dear $company
The machine you provided is part of the ring now!
The following information is vital:
- Your company's username is "$username"
- The user "$username" has sudo rights on $hostname (please don't break ansible :-))
If you want to add additional SSH keys, update the 'ssh-keys' file located on
'manage.ring.nlnog.net' in your home directory. Please contact a RING Admin if
you cannot login to manage.ring.nlnog.net. If you update the 'ssh-keys' file
the changes are usually propagated within 30 to 60 minutes.
For further instruction on how to use the ring, see: https://ring.nlnog.net/user-guide/
You are welcome to join the ring-users mailinglist, which is used for
announcements and discussions among RING users. You can find the list at
http://mailman.nlnog.net/mailman/listinfo/ring-users
We also operate an IRC channel: #ring on IRCnet and we have a #ring channel on the
NLNOG Discord server: https://nlnog.net/discord.
Finally, you are invited to peer with our looking glass. The looking glass
service is available for everyone via https://lg.ring.nlnog.net/. Peering with this
Looking Glass is optional for ring users. If you want to peer with NLNOG RING, configure a
session with the following information:
AS: 199036
IPv4: 212.114.120.72
IPv6: 2001:7b8:62b:1:0:d4ff:fe72:7848
Type: eBGP Multi-Hop
Policy: import NONE from AS199036, export ANY
To configure the NLNOG RING side, submit a pull request for:
https://github.com/NLNOG/ring-ansible/blob/master/roles/openbgpd/vars/peers.yml
Kind regards,
NLNOG RING Admins
ps. An example ~/.ssh/config for usage on your workstation:
Host *.ring.nlnog.net
User $username
IdentityFile /Users/username/.ssh/id_rsa_nlnogring
IdentitiesOnly yes
'''
ANNOUNCEMAIL = '''From: $adminemail
To: $ringusers
Subject: $company ($countrycode) joined the RING
Dear All,
$company - AS $autnum - joined the RING today.
"$companydesc"
Users can connect to $hostname, which is located in $country.
Your ssh keys have been distributed to the new ring node, and the ssh host
keys of $hostname have been added to /etc/ssh/ssh_known_hosts on all RING
nodes.
Kind regards,
NLNOG RING Admins
'''
DOWNMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: Problem with NLNOG RING node for $company
Dear $company
It appears the node you made available to the NLNOG RING project is no longer
online. Can you please investigate and let us know whether you could remedy
this situation?
This concerns the machine with hostname "$hostname"
For details on the problem, please see: http://$hostname/status.json or run
'ring-health -r' on the command line.
Kind regards,
NLNOG RING Admins
'''
V4DOWNMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: Problem with IPv4 connectivity on NLNOG RING node for $company
Dear $company
It appears the node you made available to the NLNOG RING project is no longer
online on IPv4. Can you please investigate and let us know whether you could
remedy this situation?
This concerns the machine with hostname "$hostname"
For details on the problem, please see: http://$hostname/status.json or run
'ring-heaalth -r' on the command line.
Kind regards,
NLNOG RING Admins
'''
V6DOWNMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: Problem with IPv6 connectivity on NLNOG RING node for $company
Dear $company
It appears the node you made available to the NLNOG RING project is no longer
online on IPv6. Can you please investigate and let us know whether you could
remedy this situation?
This concerns the machine with hostname "$hostname"
For details on the problem, please see: http://$hostname/status.json or run
'ring-health -r' on the command line.
Kind regards,
NLNOG RING Admins
'''
REMOVEMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: NLNOG RING node for $company removed
Dear $company
Please be informed that the node with hostname "$hostname" is no longer
part of the NLNOG RING. The machine can be shut down or used for other purposes.
All references to the machine have been removed from our database. If you
have no additional nodes that are part of the RING your user account(s) will
be deactivated.
This email notification is part of an automated cleanup process to remove
RING nodes which have been unreachable for an extended period of time.
If you'd like a machine to rejoin the NLNOG RING please reply to this message
or fill in the form at https://ring.nlnog.net/contact/application-form/
Kind regards,
NLNOG RING Admins
'''
ANSIBLEREPORTEMAIL = '''From: $adminemail
To: $adminemail
Subject: NLNOG RING Ansible report
$contents
Please investigate new missing nodes and failed ansible runs.
Use "ring-admin send downmail <node>" to request assistance from the node owner.
Kind regards,
NLNOG RING dbmaster
'''
HUGOPOST = '''+++
author = "RING Admins"
title = "$company ($countrycode) joined the RING"
date = "$date"
description = "$company ($countrycode) joined the RING"
categories = [
"participants",
]
+++
$company - AS $autnum - joined the RING today.
> $companydesc
Users can connect to $hostname, which is located in $country.
'''
FAILEDUPGRADEMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: Failed Upgrade of NLNOG RING node for $company
Dear $company
It appears the node you made available to the NLNOG RING project is no longer
online after the scheduled upgrade to Ubuntu 22.04. Can you please investigate.
If you're able to fix the node, please do so and let us know.
If not, please provision a fresh install with the following details:
* Ubuntu 22.04
* user 'nlnog' with the SSH-keys mentioned on https://ring.nlnog.net/contact/
* make sure this user has passwordless 'sudo' rights
This concerns the machine with hostname "$hostname"
Kind regards,
NLNOG RING Admins
'''
CANNOTUPGRADEMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: Failed Upgrade of NLNOG RING node $hostname for $company
Dear $company
It appears the node you made available to the NLNOG RING project failed to
upgrade to Ubuntu 22.04. Can you please try to fix the upgrade, or if that fails
deploy a fresh install with the following details:
* Ubuntu 22.04
* user 'nlnog' with the SSH-keys mentioned on https://ring.nlnog.net/contact/
* make sure this user has passwordless 'sudo' rights
This concerns the machine with hostname "$hostname"
Kind regards,
NLNOG RING Admins
'''
DISKMAIL = '''From: $adminemail
To: $nocemail, $email
Cc: $adminemail
Subject: Problem with NLNOG RING node for $company
Dear $company
The node you made available to the NLNOG RING project has run out of available
disk space. Could you please remedy this?
Please make the root partition at least 20GB, and if your machine has a
dedicated /boot partition, please make sure it is at least 1GB so new kernels
can be installed.
This concerns the machine with hostname "$hostname"
Kind regards,
NLNOG RING Admins
'''
MAILHOST='localhost'
INACTIVE_DAYS=90
class commands(object):
def __init__(self):
super(commands, self).__init__()
def run(self, argv):
func = None
for i in reversed(range(len(argv))):
try:
func = getattr(self, 'cmd_' + '_'.join(argv[0:i + 1]))
args, kwargs = self.splitargv(argv[i + 1:])
break
except AttributeError:
pass
except UnicodeEncodeError:
pass
if not func:
return self.usage()
spec = inspect.getfullargspec(func)
reqcount = len(spec.args) - 1
if spec.defaults:
reqcount = reqcount - len(spec.defaults)
if reqcount and not len(args) and not len(kwargs):
kwargs = self.ask(func)
return func(*args, **kwargs)
def ask(self, func):
spec = inspect.getfullargspec(func)
d = dict()
for i, k in enumerate(spec.args[1:]):
print("%s:" % k),
if i >= (len(spec.args) - len(spec.defaults) - 1):
print("(optional)"),
v = raw_input()
d[k] = v
return d
def splitargv(self, argv):
args = list()
kwargs = dict()
for arg in argv:
if '=' in arg:
k, v = arg.split('=', 1)
kwargs[k] = v
else:
if kwargs:
raise SyntaxError
args.append(arg)
return args, kwargs
def usage(self):
cmds = []
for k, v in sorted(inspect.getmembers(self)):
if k.startswith('cmd_'):
cmd = k[4:].replace('_', ' ')
argspec = inspect.getfullargspec(v)
args = []
if argspec.defaults:
defcount = len(argspec.defaults)
for arg in argspec.args[1:-defcount]:
args.append('<%s>' % arg)
for arg in argspec.args[-defcount:]:
args.append('[%s]' % arg)
else:
for arg in argspec.args[1:]:
args.append('<%s>' % arg)
doc = v.__doc__
if not doc:
doc = ''
cmds.append(('%s %s' % (cmd, ' '.join(args)), doc))
maxcmdlen = max(len(cmd[0]) for cmd in cmds)
for cmd, doc in cmds:
print('%-*s %s' % (maxcmdlen + 2, cmd, doc))
return 1
def dbdo(self, q, v=None, script=False):
conn = mdb.connect(DBHOST, DBUSER, DBPASS, DB)
cur = conn.cursor()
if v:
vlist = list(v)
v = tuple(vlist)
cur.execute(q, v)
else:
cur.execute(q)
if not script:
conn.commit()
conn.close()
return cur.rowcount, cur.lastrowid
def dbquery(self, q, v=None):
conn = mdb.connect(DBHOST, DBUSER, DBPASS, DB)
cur = conn.cursor(mdb.cursors.DictCursor)
if v:
cur.execute(q, v)
else:
cur.execute(q)
for row in cur.fetchall():
yield row
conn.close()
def dbget(self, q, v=None):
conn = mdb.connect(DBHOST, DBUSER, DBPASS, DB)
cur = conn.cursor(mdb.cursors.DictCursor)
if v:
cur.execute(q, v)
else:
cur.execute(q)
result = cur.fetchall()
conn.close()
return result
def dbselect(self, table, **kwargs):
cols, vals = zip(*kwargs.items())
query = 'SELECT * FROM %s WHERE %s' % \
(table, ' '.join('%s=%%s AND' % col for col in cols))
query = re.sub(' AND$', '', query)
return self.dbquery(query, vals)
def dbselectmax(self, table, counter, where):
query = 'SELECT MAX(%s) FROM %s WHERE %s' % (counter, table, where)
res = list(self.dbquery(query))
if len(res) > 1:
raise KeyError
elif res:
return res[0]['MAX(%s)' % (counter)]
else:
return None
def dbselectone(self, table, **kwargs):
res = list(self.dbselect(table, **kwargs))
if len(res) > 1:
raise KeyError
elif res:
return res[0]
else:
return None
def dbinsert(self, table, **kwargs):
cols, vals = zip(*kwargs.items())
query = 'INSERT INTO %s (%s) VALUES(%s)' % \
(table, ','.join(cols), ','.join(['%s'] * len(vals)))
print(vals)
_, rowid = self.dbdo(query, vals)
return rowid
def dbinsertlist(self, table, cols, vals):
query = 'INSERT INTO %s (%s) VALUES(%s)' % \
(table, ','.join(cols), ','.join(['%s'] * len(vals)))
_, rowid = self.dbdo(query, vals)
return rowid
def dbdelete(self, table, **kwargs):
cols, vals = zip(*kwargs.items())
query = 'DELETE FROM %s WHERE %s' % \
(table, ' '.join('%s=%%s' % col for col in cols))
self.dbdo(query, vals)
def dbupdate(self, table, id, **kwargs):
cols, vals = zip(*kwargs.items())
query = 'UPDATE %s SET %s WHERE id=%s' % \
(table, ' '.join('%s=%%s' % col for col in cols), id)
self.dbdo(query, vals)
def dblist(self, table):
query = 'SELECT * FROM %s' % table
result = self.dbquery(query)
for row in result:
print(row)
def sendmail(self,sender,recipients,message):
server = smtplib.SMTP(MAILHOST)
message = unidecode(message)
server.sendmail(sender, recipients, message)
server.quit()
def ansible_checkout(self):
ret = os.system("cd " + ANSIBLEDIR + " && git checkout")
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
ret = os.system("cd " + ANSIBLEDIR + " && git pull")
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
def ansible_getsshkeys(self):
ret = os.system("rsync -r --delete --chmod=g+w " + ANSIBLE_KEYORIGIN + " " + ANSIBLEDIR + "/" + ANSIBLE_KEYBASE)
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
ret = os.system("cd " + ANSIBLEDIR + " && git add " + ANSIBLEDIR + "/" + ANSIBLE_KEYDIR)
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
def ansible_generate_hostfile(self):
f = open(ANSIBLEDIR + '/' + ANSIBLE_HOSTFILE,'w')
f2 = open(ANSIBLEDIR + '/' + ANSIBLE_HOSTFILE + '.active','w')
f.write("# Generated by ringadmin\n\n")
f2.write("# Generated by ringadmin\n\n")
q = 'SELECT * FROM users'
f.write("[ringnode]\n")
f2.write("[ringnode]\n")
users = self.dbquery(q)
for u in users:
if u['admin'] == 1:
continue
q2 = 'SELECT * from machines WHERE owner=' + str(u['id'])
machines = self.dbquery(q2)
for m in machines:
line = m['hostname'] + ' OWNER=' + u['username']
if m['v4']:
line = line + ' V4=' + m['v4']
if m['v6']:
line = line + ' V6=' + m['v6']
f.write(line + "\n")
if m['active'] != 0:
f2.write(line + "\n")
f.write("\n[infranode]\n")
f2.write("\n[infranode]\n")
users = self.dbquery(q)
for u in users:
if not u['admin']:
continue
q2 = 'SELECT * from machines WHERE owner=' + str(u['id'])
machines = self.dbquery(q2)
for m in machines:
line = m['hostname'] + ' OWNER=' + u['username']
if m['v4']:
line = line + ' V4=' + m['v4']
if m['v6']:
line = line + ' V6=' + m['v6']
f.write(line + "\n")
if m['active'] != 0:
f2.write(line + "\n")
users = self.dbquery(q)
for u in users:
if u['admin'] == 1:
continue
f.write("\n[" + u['username'] + "]\n")
f2.write("\n[" + u['username'] + "]\n")
q2 = 'SELECT * from machines WHERE owner=' + str(u['id'])
machines = self.dbquery(q2)
for m in machines:
line = m['hostname'] + ' OWNER=' + u['username']
if m['v4']:
line = line + ' V4=' + m['v4']
if m['v6']:
line = line + ' V6=' + m['v6']
f.write(line + "\n")
if m['active'] != 0:
f2.write(line + "\n")
f.close()
f2.close()
def ansible_generate_hostkeyfile(self):
f = open(ANSIBLEDIR + '/' + ANSIBLE_HOSTKEYFILE,'w')
f.write("# Generated by ringadmin\n\n")
q = 'SELECT * FROM users'
users = self.dbquery(q)
for u in users:
q2 = 'SELECT * from machines WHERE owner=' + str(u['id'])
machines = self.dbquery(q2)
for m in machines:
namestr = m['hostname']
if m['v4']:
namestr = namestr + ',' + m['v4']
if m['v6']:
namestr = namestr + ',' + m['v6']
q3 = 'SELECT * from sshhostkeys WHERE machine=' + str(m['id'])
hostkeys = self.dbquery(q3)
for k in hostkeys:
f.write('%s %s %s %s\n' % (namestr,k['keytype'],k['sshkey'],
k['keyid']))
f.close()
def ansible_generate_userfile(self):
f = open(ANSIBLEDIR + '/' + ANSIBLE_USERFILE,'w')
f.write("# Generated by ringadmin\n\n")
f.write("GROUPS:\n")
f.write(" ring-users:\n");
f.write(" gid: 5000\n");
f.write(" admin:\n");
f.write(" gid: 99\n");
f.write(" sudo: true\n\n");
f.write("USERS:\n")
f.write(" ubuntu:\n");
f.write(" state: absent\n");
f.write(" nlnog:\n");
f.write(" state: absent\n");
f.write(" ring:\n");
f.write(" state: absent\n\n");
q = 'SELECT * FROM users WHERE admin IS NULL or admin != 1'
users = self.dbquery(q)
for u in users:
q2 = 'SELECT * from participants WHERE id=' + str(u['participant'])
participants = list(self.dbquery(q2))
p = participants[0]
f.write(' ' + u['username'] + ':\n')
if (u['active'] == 0):
f.write(' state: absent\n')
else:
f.write(' state: present\n')
f.write(' company: \'' + p['company'] + '\'\n')
if u['email']:
f.write(' email: \'' + u['email'] + '\'\n')
else:
f.write(' email: \'' + p['nocemail'] + '\'\n')
f.write(' uid: ' + str(u['userid']) + '\n')
if u['shell']:
f.write(' shell: ' + u['shell'] + '\n')
f.write(' groups:\n')
f.write(' - ring-users\n\n')
f.close()
def ansible_push(self):
username=getpass.getuser()
ret = os.system("cd " + ANSIBLEDIR + " && git commit -am 'dbmaster commit by " + username + "'")
ret = os.system("cd " + ANSIBLEDIR + " && git push")
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
def ansible_process(self,send=None):
q = "select machines.*, users.username,users.admin from machines,users where machines.owner = users.id"
machines = self.dbget(q)
q = "select * from ansible where timestamp >= now() - INTERVAL 1 DAY order by timestamp desc"
ansible_runs = self.dbget(q)
q = "select * from health where family=4 and timestamp >= now() - INTERVAL 1 DAY order by timestamp desc"
health_v4 = self.dbget(q)
q = "select * from health where family=6 and timestamp >= now() - INTERVAL 1 DAY order by timestamp desc"
health_v6 = self.dbget(q)
for m in machines:
hostname = re.sub('.' + ZONE, '', m['hostname'])
# Process ansible runs
m['seen'] = 0
m['failed'] = 0
for r in ansible_runs:
if r['hostname'] == hostname:
m['seen'] = 1
if r['unreachable'] > 0 or r['failures'] > 0:
m['failed'] = m['failed'] + 1
# Process health reports
m['seen_v4'] = 0
for h in health_v4:
if h['hostname'] == hostname:
m['seen_v4'] = 1
m['ubuntu_release'] = simplejson.loads(h['summary'])['info'].get('ubuntu_release', 'unknown')
break
m['seen_v6'] = 0
for h in health_v6:
if h['hostname'] == hostname:
m['seen_v6'] = 1
m['ubuntu_release'] = simplejson.loads(h['summary'])['info'].get('ubuntu_release', 'uknown')
break
total = 0
active = 0
inactive = 0
activate = []
deactivate = []
failed = {}
failed_admin = {}
missing_admin = []
alive_v4 = []
dead_v4 = []
alive_v6 = []
dead_v6 = []
ubuntu = {}
# Hardcoded ignore list
exceptions = {}
#exceptions['hostname'] = 1
for m in machines:
if m['seen_v4'] == 1 and m['alive_v4'] != 1:
alive_v4.append(m['hostname'])
if m['seen_v4'] == 0 and m['alive_v4'] != 0:
dead_v4.append(m['hostname'])
if m['seen_v6'] == 1 and m['alive_v6'] != 1:
alive_v6.append(m['hostname'])
if m['seen_v6'] == 0 and m['alive_v6'] != 0:
dead_v6.append(m['hostname'])
if "ubuntu_release" in m:
if m["ubuntu_release"] in ubuntu:
ubuntu[m["ubuntu_release"]].append(m["hostname"])
else:
ubuntu[m["ubuntu_release"]] = [m["hostname"]]
if m['admin'] == 1:
if m['failed'] > 0:
failed_admin[m['hostname']] = m['failed']
elif m['seen'] == 0:
missing_admin.append(m['hostname'])
else:
total = total + 1
if m['failed'] > 0:
failed[m['hostname']] = m['failed']
if m['active'] != 1:
inactive = inactive + 1
if m['seen'] == 1 and m['failed'] == 0 and not m['hostname'] in exceptions:
activate.append(m['hostname'])
if m['active'] == 1:
active = active + 1
if m['seen'] == 0 and m['failed'] == 0 and not m['hostname'] in exceptions:
deactivate.append(m['hostname'])
report = ""
report = report + "Nodes: \t%d\n" % (total)
report = report + "Active: \t%d (%d new)\n" % (active+len(activate)-len(deactivate),len(activate))
report = report + "Inactive:\t%d (%d new)\n" % (inactive+len(deactivate)-len(activate),len(deactivate))
report = report + "Failed: \t%d\n" % (len(failed.keys()))
if len(exceptions) > 0:
report = report + "Ignored:\t%d\n" % (len(exceptions))
report = report + "\nUbuntu releases:\n"
for version in ubuntu.keys():
report = report + "%s: \t%d\n" % (version, len(ubuntu[version]))
report = report + "\nNew nodes seen:\n"
for a in activate:
report = report + " - %s\n" % (a)
report = report + "\nNew missing nodes:\n"
for d in deactivate:
report = report + " - %s\n" % (d)
report = report + "\nFailed ansible runs:\n"
for k,v in failed.items():
report = report + " - %s (%d)\n" % (k,v)
report = report + "\n"
report = report + "\nMissing admin nodes:\n"
for m in missing_admin:
report = report + " - %s\n" % (m)
report = report + "\nFailed runs on admin nodes:\n"
for k,v in failed_admin.items():
report = report + " - %s (%d)\n" % (k,v)
report = report + "\n"
# Run activate/deactive changes
for a in activate:
machid = self.hostname2mid(a)
if not machid:
sys.stderr.write('could not active machine %s: not found\n' % a)
self.activate_machine(machid)
print('machine %s marked as active' % (a))
report = report + "%d nodes activated.\n" % len(activate)
if len(deactivate) > 10:
report = report + "More than 10 missing nodes since last run. Automatic deactivation skipped.\n"
else:
for d in deactivate:
machid = self.hostname2mid(d)
if not machid:
sys.stderr.write('could not deactive machine %s: not found\n' % a)
self.deactivate_machine(machid)
print('machine %s marked as inactive' % (d))
report = report + "%d nodes deactivated.\n" % len(deactivate)
# Update alive/dead status
for a in alive_v4:
machid = self.hostname2mid(a)
if not machid:
pass
self.dbupdate('machines', machid, **{'alive_v4': 1})
for a in alive_v6:
machid = self.hostname2mid(a)
if not machid:
pass
self.dbupdate('machines', machid, **{'alive_v6': 1})
if len(dead_v4) > 10:
report = report + "More than 10 missing nodes on ipv4 since last run. Health update skipped.\n"
report = report + "(" + ','.join(dead_v4) + ")\n"
else:
for d in dead_v4:
machid = self.hostname2mid(d)
if not machid:
continue
self.dbupdate('machines', machid, **{'alive_v4': 0})
print('machine %s marked as dead on IPv4' % (d))
if len(dead_v6) > 10:
report = report + "More than 10 missing nodes on ipv6 since last run. Health update skipped.\n"
report = report + "(" + ','.join(dead_v6) + ")\n"
else:
for d in dead_v6:
machid = self.hostname2mid(d)
if not machid:
continue
self.dbupdate('machines', machid, **{'alive_v6': 0})
print('machine %s marked as dead on IPv6' % (d))
text = Template(ANSIBLEREPORTEMAIL)
mailbody = text.substitute(adminemail=ADMINEMAIL,contents=report)
print(mailbody)
if send and (len(activate) > 0 or len(deactivate) > 0):
print("Sending mail... ")
self.sendmail(ADMINEMAIL,ADMINEMAIL,mailbody)
print("Done.")
def web_checkout(self):
ret = os.system("cd " + WEBDIR + " && git checkout")
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
ret = os.system("cd " + WEBDIR + " && git pull")
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
def web_push(self):
username=getpass.getuser()
ret = os.system("cd " + WEBDIR + " && git commit -am 'dbmaster commit by " + username + "'")
ret = os.system("cd " + WEBDIR + " && git push")
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
def web_publish(self,username,postbody):
self.web_checkout()
post_filename = WEB_POSTDIR + "/" + username + "-joined-the-ring.md"
post_shortloc = re.sub(WEBDIR + '/','',post_filename)
logo_filename = WEB_LOGODIR + "/" + username + ".png"
logo_shortloc = re.sub(WEBDIR + '/','',logo_filename)
if not os.path.isfile(logo_filename):
sys.stderr.write('logo for %s missing; please add it to %s in the ring-web repository.'
% (username,logo_shortloc))
f = open(post_filename,'w')
f.write(postbody)
f.close()
ret = os.system("cd " + WEBDIR + " && git add " + post_shortloc)
if ret > 0:
sys.exit("subprocess returned error: %d" % (ret))
self.web_push()
def generate_hugopost(self, username, publish=None):
partid = self.username2pid(username)
if not partid:
sys.exit('user %s not found' % username)
p = self.dbselectone('participants', id=partid)
if not p:
sys.exit('participant %s not found' % partid)
u = self.dbselectone('users', participant=partid)
m = self.dbselect('machines', owner=u['id'])
if not m:
sys.exit('no machines found for %s' % username)
hostname = ''
autnum = ''
country = ''
state = ''
countrycode = ''
try:
mfirst = next(m)
hostname = mfirst['hostname']
autnum = str(mfirst['autnum'])
countrycode = mfirst['country']
except StopIteration:
pass
country = self.countryname(countrycode, state)
date = datetime.datetime.now().isoformat()[:10]
text = Template(HUGOPOST)
postbody = text.substitute(company=p['company'],
companydesc=p['companydesc'],
hostname=hostname, autnum=autnum,
country=country, countrycode=countrycode,
date=date)
print(postbody)
if publish:
print("Publishing post... ")
self.web_publish(username,postbody)
print("Done.")
def generate_welcomemail(self, username, send=None):
partid = self.username2pid(username)
if not partid:
sys.exit('user %s not found' % username)
p = self.dbselectone('participants', id=partid)
if not p:
sys.exit('participant %s not found' % partid)
u = self.dbselectone('users', participant=partid)
m = self.dbselect('machines', owner=u['id'])
if not m:
sys.exit('no machines found for %s' % username)