Hello Madhuvishy, Chasemp, Yuvipanda, I'd like you to do a code review. Please visit
https://gerrit.wikimedia.org/r/336157 to review the following change. Change subject: labstore: Remove create-dbusers ...................................................................... labstore: Remove create-dbusers create-dbusers was replaced by maintain-dbusers in c5f0338767a4394b3d82de3283455379f62eee21. This change removes the remnant and now unused file and a reference to it that only made sense during the migration. Change-Id: I1cf97904cc20582d256ccb3c3e7e74c720a3a80a --- D modules/labstore/files/create-dbusers M modules/role/files/labs/db/maintain-dbusers.py 2 files changed, 1 insertion(+), 334 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/operations/puppet refs/changes/57/336157/1 diff --git a/modules/labstore/files/create-dbusers b/modules/labstore/files/create-dbusers deleted file mode 100755 index edea94a..0000000 --- a/modules/labstore/files/create-dbusers +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/python3 -""" -This script does the following: - - - Check if users / service groups in opted in projects have - a replica.my.cnf file with mysql credentials - - If they do not exist, create a mysql user, give them - appropriate grants and write the replica.my.cnf file - - If there is no replica.my.cnf but grants already exist, do - nothing. This needs to be fixed with appropriate solution - later on - either by recreating the replica.my.cnf or... - something else - - perms for grant files (how the user gets the creds + password) - are 0400 and chattr +i making them user readonly and immutable. -""" -import logging -import argparse -import ldap3 -import pymysql -import yaml -import os -import string -import random -import configparser -import io -import time -import subprocess - - -class User: - - def __init__(self, project, name, uid, kind): - self.project = project - self.name = name - self.uid = int(uid) - - if kind not in ['user', 'servicegroup']: - raise("{} not allowed".format(kind)) - - self.kind = kind - - self.project_root = '/exp/project' - - @property - def db_username(self): - """ - The db username to use for this user. - - Guaranteed to be of the form (s|u)\d+ - """ - prefix = 'u' if self.kind == 'user' else 's' - return prefix + str(self.uid) - - @property - def homedir(self): - prefix = os.path.join(self.project_root, self.project) - - if self.kind == 'user': - trail = os.path.join(prefix, - 'home', - self.name, - ) - else: - trail = os.path.join(prefix, - 'project', - self.name[len(self.project) + 1:], - ) - - return trail - - def __repr__(self): - return "%s(name=%s, uid=%s)" % ( - self.kind, self.name, self.uid) - - @classmethod - def from_ldap_servicegroups(cls, conn, project): - - logging.debug('Collecting servicegroups from ldap for {}'.format(project)) - conn.search( - 'ou=people,ou=servicegroups,dc=wikimedia,dc=org', - '(cn=%s.*)' % project, - ldap3.SEARCH_SCOPE_WHOLE_SUBTREE, - attributes=['uidNumber', 'cn'], - time_limit=5 - ) - - users = [] - for resp in conn.response: - attrs = resp['attributes'] - users.append(cls(project, attrs['cn'][0], attrs['uidNumber'][0], 'servicegroup')) - - logging.debug('Found {} ldap servicegroups'.format(len(users))) - logging.debug('First 10 members of ldap servicegroups: {}'.format(users[:10])) - return users - - @classmethod - def from_ldap_users(cls, conn, project): - - logging.debug('Collecting ldap users for {}'.format(project)) - conn.search( - 'ou=groups,dc=wikimedia,dc=org', - '(cn=project-%s)' % project, - ldap3.SEARCH_SCOPE_WHOLE_SUBTREE, - attributes=['member'] - ) - - users = [] - members = conn.response[0]['attributes']['member'] - - logging.debug('Found {} ldap users'.format(len(members))) - logging.debug('First 10 members of ldap users: {} ...'.format(members[:10])) - - # example member: uid=foo,ou=people,dc=wikimedia,dc=org - for member in members: - - # strip off all but 'uid=foo' - search_string = member.replace(',ou=people,dc=wikimedia,dc=org', '') - - conn.search( - 'ou=people,dc=wikimedia,dc=org', - '(%s)' % search_string, - ldap3.SEARCH_SCOPE_WHOLE_SUBTREE, - attributes=['uid', 'uidNumber'] - ) - - # Example of ldap query response: - # [ - # {'type': 'searchResEntry', - # 'attributes': { - # 'uid': ['foo'], - # 'uidNumber': ['1111']}, - # 'raw_attributes': { - # 'uid': [b'foo'], - # 'uidNumber': [b'1111'] - # }, - # 'dn': 'uid=foo,ou=people,dc=wikimedia,dc=org'} - # ] - - if len(conn.response) == 0: - logging.error( - 'No entry found for user {user} in project {project}'.format( - user=member, - project=project, - ) - ) - continue - - attrs = conn.response[0]['attributes'] - users.append(cls( - project, - attrs['uid'][0], - attrs['uidNumber'][0], - 'user' - )) - - logging.debug('Found {} ldap users with uidNumer'.format(len(users))) - return users - - def write_user_file(self, path, content): - logging.debug('writing path %s' % (path,)) - f = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_NOFOLLOW) - try: - os.write(f, content.encode('utf-8')) - # uid == gid - os.fchown(f, self.uid, self.uid) - os.fchmod(f, 0o400) - - # Prevent removal or modification of the credentials file - subprocess.check_output(['/usr/bin/chattr', - '+i', - path]) - except: - os.remove(path) - raise - finally: - os.close(f) - - -class CredentialCreator: - PASSWORD_LENGTH = 16 - PASSWORD_CHARS = string.ascii_letters + string.digits - - GRANT_SQL_TEMPLATE = """ - CREATE USER '{user_name}'@'%' IDENTIFIED BY '{user_pass}'; - GRANT SELECT, SHOW VIEW ON `%\_p`.* TO '{user_name}'@'%'; - GRANT ALL PRIVILEGES ON `{user_name}\_\_%`.* TO '{user_name}'@'%';""" - - def __init__(self, hosts, username, password): - self.conns = [ - pymysql.connect(host, username, password) - for host in hosts - ] - - @staticmethod - def _generate_pass(): - sysrandom = random.SystemRandom() # Uses /dev/urandom - return ''.join(sysrandom.sample( - CredentialCreator.PASSWORD_CHARS, - CredentialCreator.PASSWORD_LENGTH)) - - def write_credentials_file(self, path, user): - - password = self._generate_pass() - replica_config = configparser.ConfigParser() - - replica_config['client'] = { - 'user': user.db_username, - 'password': password - } - - self.create_user(user, password) - # Because ConfigParser can only write to a file - # and not just return the value as a string directly - replica_buffer = io.StringIO() - replica_config.write(replica_buffer) - - logging.info("creating replica.my.cnf for %s as %s", user.name, user.db_username) - user.write_user_file(path, replica_buffer.getvalue()) - logging.debug('write: {} {}\n'.format(path, replica_buffer.getvalue())) - - def check_user_exists(self, user): - exists = True - for conn in self.conns: - conn.ping(True) - cur = conn.cursor() - try: - cur.execute('SELECT * FROM mysql.user WHERE User = %s', user.db_username) - result = cur.fetchone() - finally: - cur.close() - exists = exists and (result is not None) - return exists - - def create_user(self, user, password): - for conn in self.conns: - conn.ping(True) - cur = conn.cursor() - try: - # is ok, because password is guaranteed to never - # contain a quote (only alphanumeric) and username - # is guaranteed to be (u|s)\d+. - sql = CredentialCreator.GRANT_SQL_TEMPLATE.format( - user_name=user.db_username, - user_pass=password - ) - - logging.debug('sql: {}'.format(str(sql))) - cur.execute(sql) - logging.info('Created user %s as %s in %s', user.name, user.db_username, conn.host) - - finally: - cur.close() - - -if __name__ == '__main__': - - argparser = argparse.ArgumentParser() - - argparser.add_argument('--config', - default='/etc/create-dbusers.yaml', - help='Path to YAML config file') - - argparser.add_argument('--debug', - help='Turn on debug logging', - action='store_true') - - argparser.add_argument('--project', - help='Project name to create db users for', - default='tools') - - argparser.add_argument('--interval', - help='Seconds between between runs', - type=int, - default=0) - - args = argparser.parse_args() - - loglvl = logging.DEBUG if args.debug else logging.INFO - logging.basicConfig(format='%(message)s', - level=loglvl) - - with open(args.config) as f: - config = yaml.safe_load(f) - - labsdb_hosts = [ - k for k in config['labsdbs']['hosts'] - if config['labsdbs']['hosts'][k]['grant-type'] == 'legacy' - ] - cgen = CredentialCreator( - labsdb_hosts, - config['labsdbs']['username'], - config['labsdbs']['password'] - ) - - pid = os.getpid() - logging.info('starting pid %s create-dbusers run' % (pid,)) - logging.debug(str(args)) - - while True: - - servers = ldap3.ServerPool([ - ldap3.Server(host, connect_timeout=1) - for host in config['ldap']['hosts'] - ], ldap3.POOLING_STRATEGY_ROUND_ROBIN, active=True, exhaust=True) - - with ldap3.Connection( - servers, read_only=True, - user=config['ldap']['username'], - auto_bind=True, - password=config['ldap']['password'] - ) as conn: - users = User.from_ldap_users(conn, args.project) - servicegroups = User.from_ldap_servicegroups(conn, args.project) - all_users = users + servicegroups - - for user in all_users: - replica_path = os.path.join(user.homedir, 'replica.my.cnf') - #logging.debug(replica_path) - if os.path.exists(user.homedir) and not os.path.exists(replica_path): - if not cgen.check_user_exists(user): - # No replica.my.cnf and no user in db - # Generate new creds and put them in there! - cgen.write_credentials_file(replica_path, user) - else: - logging.debug('missing replica.my.cnf for user %s despite grants present in db', user.name) - - logging.info('completed pid %s create-dbusers run' % (pid,)) - - if not args.interval: - break - - time.sleep(args.interval) diff --git a/modules/role/files/labs/db/maintain-dbusers.py b/modules/role/files/labs/db/maintain-dbusers.py index 2a7386e..56accf2 100644 --- a/modules/role/files/labs/db/maintain-dbusers.py +++ b/modules/role/files/labs/db/maintain-dbusers.py @@ -9,8 +9,7 @@ mutate the DB in some way. They are also supposed to be idempotent - if they have nothing to do, they should not do anything. -Some of the functions are one-time only, allowing migration from -create-dbusers. These are: +Some of the functions are one-time only. These are: ## harvest_cnf_files ## -- To view, visit https://gerrit.wikimedia.org/r/336157 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I1cf97904cc20582d256ccb3c3e7e74c720a3a80a Gerrit-PatchSet: 1 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Tim Landscheidt <t...@tim-landscheidt.de> Gerrit-Reviewer: Chasemp <r...@wikimedia.org> Gerrit-Reviewer: Madhuvishy <mviswanat...@wikimedia.org> Gerrit-Reviewer: Yuvipanda <yuvipa...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits