replication-manager.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import argparse
  2. import MySQLdb
  3. import os
  4. import re
  5. import secrets
  6. import string
  7. import sys
  8. from collections import OrderedDict
  9. from pki_utils import PKI
  10. host = 'dbmaster'
  11. user = 'replication-manager'
  12. def validate_name(value):
  13. pattern = re.compile("^([a-z0-9._-]+[a-z0-9]+)$")
  14. if not pattern.match(value):
  15. raise ValueError(f'Name does not match pattern {pattern}')
  16. return value
  17. def add_slave(cursor, **kwargs):
  18. name = validate_name(kwargs['name'])
  19. alphabet = string.ascii_letters + string.digits
  20. passwd = ''.join(secrets.choice(alphabet) for i in range(32))
  21. # Create key and certificate
  22. with open('certs/ca.pem', 'r') as ca_crt_file, open('certs/ca-key.pem', 'r') as ca_key_file:
  23. ca_crt_pem = ca_crt_file.read().encode('ascii')
  24. ca_key_pem = ca_key_file.read().encode('ascii')
  25. pki = PKI(ca_crt_pem=ca_crt_pem, ca_key_pem=ca_key_pem)
  26. pki.initialize_key()
  27. pki.create_certificate(common_name=name, days=365*10)
  28. subject = ''
  29. for label, attributes in pki.subject_attributes.items():
  30. assert len(attributes) == 1
  31. value = attributes[0]
  32. subject += f'/{label}={value}'
  33. # Configure slave in database
  34. username = name.replace('.', '_') # allows using username as binlog on the slave
  35. print(f'Creating slave user {username} with subject {subject} ...')
  36. cursor.execute("CREATE USER %s@'%%' IDENTIFIED BY %s", (username, passwd,))
  37. cursor.execute("GRANT RELOAD, REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO %s@'%%' REQUIRE SUBJECT %s", (username, subject,))
  38. cursor.execute("GRANT SELECT ON pdns.* TO %s@'%%' REQUIRE SUBJECT %s", (username, subject,))
  39. cursor.execute("FLUSH PRIVILEGES")
  40. print(f'Password is {passwd}')
  41. # Write key and certificate
  42. umask = os.umask(0o077)
  43. key_filename = f'certs/{name}-key.pem'
  44. crt_filename = f'certs/{name}-crt.pem'
  45. with open(key_filename, 'wb') as key_file, open(crt_filename, 'wb') as crt_file:
  46. key_file.write(pki.key_pem)
  47. crt_file.write(pki.crt_pem)
  48. os.umask(umask)
  49. print(key_filename, '(key)')
  50. print(crt_filename, '(certificate)')
  51. def list_slaves(cursor):
  52. cursor.execute("SELECT User, x509_subject FROM mysql.user WHERE x509_subject != ''")
  53. for row in cursor.fetchall():
  54. print(row[0], row[1].decode('utf-8'))
  55. def remove_slave(cursor, **kwargs):
  56. slavename = validate_name(kwargs['name'])
  57. cursor.execute("DROP USER %s@'%%'", (slavename,))
  58. cursor.execute("FLUSH PRIVILEGES")
  59. def main():
  60. parser = argparse.ArgumentParser(description='List, add, and remove pdns database replication slaves.')
  61. subparsers = parser.add_subparsers(dest='action', required=True)
  62. actions = {}
  63. # add
  64. description = 'Add a slave and generate TLS key/certificate. The slave replication password is read from stdin (first line).'
  65. subparser = subparsers.add_parser('add', help='Add a slave and generate TLS key/certificate', description=description)
  66. subparser.add_argument('--name', type=str, help='Slave identifier (usually hostname)', required=True)
  67. actions['add'] = add_slave
  68. # list
  69. subparser = subparsers.add_parser('list', help='List slaves', description='List slaves.')
  70. actions['list'] = list_slaves
  71. # remove
  72. subparser = subparsers.add_parser('remove', help='Remove a slave', description='Remove a slave.')
  73. subparser.add_argument('--name', type=str, help='Slave identifier (usually hostname)', required=True)
  74. actions['remove'] = remove_slave
  75. # Validate and extract arguments (errors out if insufficient arguments are given)
  76. args = parser.parse_args()
  77. kwargs = vars(args).copy()
  78. # Initialize database
  79. db = MySQLdb.connect(host=host, user=user, passwd=os.environ['DESECSTACK_DBMASTER_PASSWORD_replication_manager'])
  80. # Action!
  81. action = kwargs.pop('action')
  82. action_func = actions[action]
  83. try:
  84. action_func(db.cursor(), **kwargs)
  85. except Exception as e:
  86. raise e
  87. finally:
  88. db.close()
  89. if __name__ == "__main__":
  90. main()