From 30b31dc3f35d0025414f78763901f7f903101fb4 Mon Sep 17 00:00:00 2001 From: James Page Date: Wed, 18 Sep 2013 12:21:48 +0100 Subject: [PATCH] Lots of refactoring --- config.yaml | 6 ++++ hooks/mysql.py | 53 ++++++++++++++++++++-------------- hooks/percona_hooks.py | 65 ++++++++++++++++++++++++++++++------------ hooks/percona_utils.py | 18 +++++++----- revision | 2 +- templates/my.cnf | 4 +-- 6 files changed, 98 insertions(+), 50 deletions(-) diff --git a/config.yaml b/config.yaml index de05bab..ffb00d9 100644 --- a/config.yaml +++ b/config.yaml @@ -42,3 +42,9 @@ options: type: int default: 5490 description: Default multicast port number that will be used to communicate between HA Cluster nodes. + root-password: + type: string + description: Root password for MySQL access; must be configured pre-deployment for Active-Active clusters. + sst-password: + type: string + description: Re-sync account password for new cluster nodes; must be configured pre-deployment for Active-Active clusters. diff --git a/hooks/mysql.py b/hooks/mysql.py index 8bd8262..55278d7 100644 --- a/hooks/mysql.py +++ b/hooks/mysql.py @@ -1,11 +1,18 @@ ''' Helper for working with a MySQL database ''' # TODO: Contribute to charm-helpers -import MySQLdb import socket import os +from charmhelpers.core.host import pwgen, write_file, mkdir +from charmhelpers.core.hookenv import unit_get, service_name +from charmhelpers.fetch import apt_install, filter_installed_packages -from charmhelpers.core.host import pwgen -from charmhelpers.core.hookenv import unit_get + +try: + import MySQLdb +except ImportError: + apt_install(filter_installed_packages(['python-mysqldb']), + fatal=True) + import MySQLdb class MySQLHelper(): @@ -39,7 +46,6 @@ class MySQLHelper(): remote_ip)) grants = [i[0] for i in cursor.fetchall()] except MySQLdb.OperationalError: - print "No grants found" return False finally: cursor.close() @@ -69,33 +75,38 @@ class MySQLHelper(): finally: cursor.close() -_root_passwd = '/var/lib/mysql/mysql.passwd' -_named_passwd = '/var/lib/mysql/mysql-{}.passwd' +_root_passwd = '/var/lib/charm/{}/mysql.passwd' +_named_passwd = '/var/lib/charm/{}/mysql-{}.passwd' -def get_mysql_password(username=None): - ''' Retrieve or generate a mysql password for the provided username ''' +def get_mysql_password(username=None, password=None): + ''' Retrieve, generate or store a mysql password for + the provided username ''' if username: - _passwd_file = _named_passwd.format(username) + _passwd_file = _named_passwd.format(service_name(), + username) else: - _passwd_file = _root_passwd - password = None + _passwd_file = _root_passwd.format(service_name()) + _password = None if os.path.exists(_passwd_file): with open(_passwd_file, 'r') as passwd: - password = passwd.read().strip() + _password = passwd.read().strip() else: - if not os.path.exists(os.path.dirname(_passwd_file)): - os.makedirs(os.path.dirname(_passwd_file)) - password = pwgen(length=32) - with open(_passwd_file, 'w') as passwd: - passwd.write(password) - os.chmod(_passwd_file, 0600) - return password + mkdir(os.path.dirname(_passwd_file), + owner='root', group='root', + perms=0770) + # Force permissions - for some reason the chmod in makedirs fails + os.chmod(os.path.dirname(_passwd_file), 0770) + _password = password or pwgen(length=32) + write_file(_passwd_file, _password, + owner='root', group='root', + perms=0660) + return _password -def get_mysql_root_password(): +def get_mysql_root_password(password=None): ''' Retrieve or generate mysql root password for service units ''' - return get_mysql_password() + return get_mysql_password(username=None, password=password) def configure_db(hostname, diff --git a/hooks/percona_hooks.py b/hooks/percona_hooks.py index deb1caa..d8a158d 100755 --- a/hooks/percona_hooks.py +++ b/hooks/percona_hooks.py @@ -1,8 +1,12 @@ #!/usr/bin/python -# TODO: sync passwd files from seed to peers +# TODO: Support relevant configuration options +# TODO: Add db and db-admin hooks for mysql compat +# TODO: Add README.md including squid-deb-proxy updates for repo.percona.com +# TODO: Support changes to root and sstuser passwords import sys import os +import glob from charmhelpers.core.hookenv import ( Hooks, UnregisteredHookError, log, @@ -10,7 +14,8 @@ from charmhelpers.core.hookenv import ( relation_set, relation_ids, unit_get, - config + config, + service_name ) from charmhelpers.core.host import ( service_restart, @@ -31,6 +36,7 @@ from percona_utils import ( seeded, mark_seeded, configure_mysql_root_password ) +from mysql import get_mysql_password from charmhelpers.contrib.hahelpers.cluster import ( peer_units, oldest_peer, @@ -39,6 +45,10 @@ from charmhelpers.contrib.hahelpers.cluster import ( is_leader ) from mysql import configure_db +from unison import ( + ssh_authorized_peers, + sync_to_peers +) hooks = Hooks() @@ -46,11 +56,11 @@ hooks = Hooks() @hooks.hook('install') def install(): setup_percona_repo() - configure_mysql_root_password() - render_config() # Render base configuation no cluster + configure_mysql_root_password(config('root-password')) + render_config() # Render base configuation (no cluster) apt_update(fatal=True) apt_install(PACKAGES, fatal=True) - configure_sstuser() + configure_sstuser(config('sst-password')) def render_config(clustered=False, hosts=[]): @@ -58,18 +68,32 @@ def render_config(clustered=False, hosts=[]): os.makedirs(os.path.dirname(MY_CNF)) with open(MY_CNF, 'w') as conf: conf.write(render_template(os.path.basename(MY_CNF), - {'cluster_name': 'juju_cluster', - 'private_address': get_host_ip(), - 'clustered': clustered, - 'cluster_hosts': ",".join(hosts)} - ) - ) + {'cluster_name': 'juju_cluster', + 'private_address': get_host_ip(), + 'clustered': clustered, + 'cluster_hosts': ",".join(hosts), + 'sst_password': get_mysql_password(username='sstuser', + password=config('sst-password')) + }) + ) + # TODO: set 0640 and change group to mysql if avaliable + os.chmod(MY_CNF, 0644) + + +@hooks.hook('cluster-relation-joined') +def cluster_relation_joined(): + ssh_authorized_peers(peer_interface='cluster', + user='juju_ssh', group='root', + ensure_local_user=True) @hooks.hook('cluster-relation-changed') @hooks.hook('upgrade-charm') @hooks.hook('config-changed') def cluster_changed(): + ssh_authorized_peers(peer_interface='cluster', + user='juju_ssh', group='root', + ensure_local_user=True) hosts = get_cluster_hosts() clustered = len(hosts) > 1 pre_hash = file_hash(MY_CNF) @@ -84,6 +108,11 @@ def cluster_changed(): # Restart with new configuration service_restart('mysql') + if eligible_leader(LEADER_RES): + files = glob.glob('/var/lib/charm/{}/*.passwd'.format(service_name())) + sync_to_peers(peer_interface='cluster', + user='juju_ssh', paths=files) + LEADER_RES = 'res_mysql_vip' @@ -149,6 +178,10 @@ def shared_db_changed(): relation_set(**return_data) relation_set(db_host=db_host) + files = glob.glob('/var/lib/charm/{}/*.passwd'.format(service_name())) + sync_to_peers(peer_interface='cluster', + user='juju_ssh', paths=files) + @hooks.hook('ha-relation-joined') def ha_relation_joined(): @@ -162,18 +195,12 @@ def ha_relation_joined(): log('Insufficient VIP information to configure cluster') sys.exit(1) - resources = { - 'res_mysql_vip': 'ocf:heartbeat:IPaddr2', - } - + resources = {'res_mysql_vip': 'ocf:heartbeat:IPaddr2'} resource_params = { 'res_mysql_vip': 'params ip="%s" cidr_netmask="%s" nic="%s"' % \ (vip, vip_cidr, vip_iface), } - - groups = { - 'grp_percona_cluster': 'res_mysql_vip', - } + groups = {'grp_percona_cluster': 'res_mysql_vip'} for rel_id in relation_ids('ha'): relation_set(rid=rel_id, diff --git a/hooks/percona_utils.py b/hooks/percona_utils.py index a312738..750ef62 100644 --- a/hooks/percona_utils.py +++ b/hooks/percona_utils.py @@ -14,7 +14,7 @@ from charmhelpers.core.hookenv import ( ) from charmhelpers.fetch import ( apt_install, - filter_installed_packages + filter_installed_packages, ) from mysql import get_mysql_root_password @@ -35,7 +35,7 @@ except ImportError: PACKAGES = [ 'percona-xtradb-cluster-server-5.5', 'percona-xtradb-cluster-client-5.5', - 'python-mysqldb' + 'unison' ] KEY = "keys/repo.percona.com" @@ -72,6 +72,7 @@ def render_template(template_name, context, template_dir=TEMPLATES_DIR): return template.render(context) +# TODO: goto charm-helpers (I use this everywhere) def get_host_ip(hostname=None): hostname = hostname or unit_get('private-address') try: @@ -94,23 +95,26 @@ def get_cluster_hosts(): unit, relid))) return hosts +# TODO: refactor to use mysql helper when it support setting arbitary +# permissions SQL_SST_USER_SETUP = """mysql --user=root --password={} << EOF -CREATE USER 'sstuser'@'localhost' IDENTIFIED BY 's3cretPass'; +CREATE USER 'sstuser'@'localhost' IDENTIFIED BY '{}'; GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'sstuser'@'localhost'; EOF""" -def configure_sstuser(): - subprocess.check_call(SQL_SST_USER_SETUP.format(get_mysql_root_password()), +def configure_sstuser(sst_password): + subprocess.check_call(SQL_SST_USER_SETUP.format(get_mysql_root_password(), + sst_password), shell=True) # TODO: mysql charmhelper -def configure_mysql_root_password(): +def configure_mysql_root_password(password): ''' Configure debconf with root password ''' dconf = Popen(['debconf-set-selections'], stdin=PIPE) package = "percona-server-server" - root_pass = get_mysql_root_password() + root_pass = get_mysql_root_password(password) dconf.stdin.write("%s %s/root_password password %s\n" % (package, package, root_pass)) dconf.stdin.write("%s %s/root_password_again password %s\n" % diff --git a/revision b/revision index b393560..e85087a 100644 --- a/revision +++ b/revision @@ -1 +1 @@ -23 \ No newline at end of file +31 diff --git a/templates/my.cnf b/templates/my.cnf index abea528..be1e241 100644 --- a/templates/my.cnf +++ b/templates/my.cnf @@ -34,6 +34,6 @@ wsrep_sst_method=xtrabackup wsrep_cluster_name={{ cluster_name }} # Authentication for SST method -wsrep_sst_auth="sstuser:s3cretPass" +wsrep_sst_auth="sstuser:{{ sst_password }}" -!includedir /etc/mysql/conf.d/ \ No newline at end of file +!includedir /etc/mysql/conf.d/