Add new hooks and config for VIP
This commit is contained in:
parent
6218ed4920
commit
159217d016
44
config.yaml
Normal file
44
config.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
options:
|
||||
dataset-size:
|
||||
default: '80%'
|
||||
description: How much data do you want to keep in memory in the DB. This will be used to tune settings in the database server appropriately. Any more specific settings will override these defaults though. This currently sets innodb_buffer_pool_size or key_cache_size depending on the setting in preferred-storage-engine. If query-cache-type is set to 'ON' or 'DEMAND' 20% of this is given to query-cache-size. Suffix this value with 'K','M','G', or 'T' to get the relevant kilo/mega/etc. bytes. If suffixed with %, one will get that percentage of RAM devoted to dataset and (if enabled) query cache.
|
||||
type: string
|
||||
preferred-storage-engine:
|
||||
default: InnoDB
|
||||
type: string
|
||||
description: Tune the server for usage of this storage engine. Other possible value is MyISAM. Comma separated will cause settings to split resources evenly among given engines.
|
||||
tuning-level:
|
||||
default: safest
|
||||
type: string
|
||||
description: Valid values are 'safest', 'fast', and 'unsafe'. If set to safest, all settings are tuned to have maximum safety at the cost of performance. Fast will turn off most controls, but may lose data on crashes. unsafe will turn off all protections.
|
||||
query-cache-type:
|
||||
default: "OFF"
|
||||
type: string
|
||||
description: Query cache is usually a good idea, but can hurt concurrency. Valid values are "OFF", "ON", or "DEMAND". http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_query_cache_type
|
||||
query-cache-size:
|
||||
default: -1
|
||||
type: int
|
||||
description: Override the computed version from dataset-size. Still works if query-cache-type is "OFF" since sessions can override the cache type setting on their own.
|
||||
max-connections:
|
||||
default: -1
|
||||
type: int
|
||||
description: Maximum connections to allow. -1 means use the server's compiled in default.
|
||||
vip:
|
||||
type: string
|
||||
description: Virtual IP to use to front Percona XtraDB Cluster in active/active HA configuration
|
||||
vip_iface:
|
||||
type: string
|
||||
default: eth0
|
||||
description: Network interface on which to place the Virtual IP
|
||||
vip_cidr:
|
||||
type: int
|
||||
default: 24
|
||||
description: Netmask that will be used for the Virtual IP
|
||||
ha-bindiface:
|
||||
type: string
|
||||
default: eth0
|
||||
description: Default network interface on which HA cluster will bind to communication with the other members of the HA Cluster.
|
||||
ha-mcastport:
|
||||
type: int
|
||||
default: 5490
|
||||
description: Default multicast port number that will be used to communicate between HA Cluster nodes.
|
1
hooks/ha-relation-changed
Symbolic link
1
hooks/ha-relation-changed
Symbolic link
@ -0,0 +1 @@
|
||||
percona_hooks.py
|
1
hooks/ha-relation-joined
Symbolic link
1
hooks/ha-relation-joined
Symbolic link
@ -0,0 +1 @@
|
||||
percona_hooks.py
|
121
hooks/mysql.py
Normal file
121
hooks/mysql.py
Normal file
@ -0,0 +1,121 @@
|
||||
''' Helper for working with a MySQL database '''
|
||||
# TODO: Contribute to charm-helpers
|
||||
import MySQLdb
|
||||
import socket
|
||||
import os
|
||||
|
||||
from charmhelpers.core.host import pwgen
|
||||
from charmhelpers.core.hookenv import unit_get
|
||||
|
||||
|
||||
class MySQLHelper():
|
||||
def __init__(self, host='localhost'):
|
||||
self.host = host
|
||||
|
||||
def connect(self, user='root', password=None):
|
||||
self.connection = MySQLdb.connect(user=user, host=self.host,
|
||||
passwd=password)
|
||||
|
||||
def database_exists(self, db_name):
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute("SHOW DATABASES")
|
||||
databases = [i[0] for i in cursor.fetchall()]
|
||||
finally:
|
||||
cursor.close()
|
||||
return db_name in databases
|
||||
|
||||
def create_database(self, db_name):
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute("CREATE DATABASE {}".format(db_name))
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def grant_exists(self, db_name, db_user, remote_ip):
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
|
||||
remote_ip))
|
||||
grants = [i[0] for i in cursor.fetchall()]
|
||||
except MySQLdb.OperationalError:
|
||||
print "No grants found"
|
||||
return False
|
||||
finally:
|
||||
cursor.close()
|
||||
# TODO: review for different grants
|
||||
return "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants
|
||||
|
||||
def create_grant(self, db_name, db_user,
|
||||
remote_ip, password):
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
# TODO: review for different grants
|
||||
cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
|
||||
"IDENTIFIED BY '{}'".format(db_name,
|
||||
db_user,
|
||||
remote_ip,
|
||||
password))
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def cleanup_grant(self, db_user,
|
||||
remote_ip):
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute("DROP FROM mysql.user WHERE user='{}' "
|
||||
"AND HOST='{}'".format(db_user,
|
||||
remote_ip))
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
_root_passwd = '/var/lib/mysql/mysql.passwd'
|
||||
_named_passwd = '/var/lib/mysql/mysql-{}.passwd'
|
||||
|
||||
|
||||
def get_mysql_password(username=None):
|
||||
''' Retrieve or generate a mysql password for the provided username '''
|
||||
if username:
|
||||
_passwd_file = _named_passwd.format(username)
|
||||
else:
|
||||
_passwd_file = _root_passwd
|
||||
password = None
|
||||
if os.path.exists(_passwd_file):
|
||||
with open(_passwd_file, 'r') as passwd:
|
||||
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
|
||||
|
||||
|
||||
def get_mysql_root_password():
|
||||
''' Retrieve or generate mysql root password for service units '''
|
||||
return get_mysql_password()
|
||||
|
||||
|
||||
def configure_db(hostname,
|
||||
database,
|
||||
username):
|
||||
''' Configure access to database for username from hostname '''
|
||||
if hostname != unit_get('private-address'):
|
||||
remote_ip = socket.gethostbyname(hostname)
|
||||
else:
|
||||
remote_ip = '127.0.0.1'
|
||||
|
||||
password = get_mysql_password(username)
|
||||
m_helper = MySQLHelper()
|
||||
m_helper.connect(password=get_mysql_root_password())
|
||||
if not m_helper.database_exists(database):
|
||||
m_helper.create_database(database)
|
||||
if not m_helper.grant_exists(database,
|
||||
username,
|
||||
remote_ip):
|
||||
m_helper.create_grant(database,
|
||||
username,
|
||||
remote_ip, password)
|
||||
return password
|
@ -1,13 +1,20 @@
|
||||
#!/usr/bin/python
|
||||
# TODO: sync passwd files from seed to peers
|
||||
|
||||
import sys
|
||||
import os
|
||||
from charmhelpers.core.hookenv import (
|
||||
Hooks, UnregisteredHookError,
|
||||
log
|
||||
log,
|
||||
relation_get,
|
||||
relation_set,
|
||||
relation_ids,
|
||||
unit_get,
|
||||
config
|
||||
)
|
||||
from charmhelpers.core.host import (
|
||||
restart_on_change
|
||||
service_restart,
|
||||
file_hash
|
||||
)
|
||||
from charmhelpers.fetch import (
|
||||
apt_update,
|
||||
@ -20,12 +27,18 @@ from percona_utils import (
|
||||
render_template,
|
||||
get_host_ip,
|
||||
get_cluster_hosts,
|
||||
configure_sstuser
|
||||
configure_sstuser,
|
||||
seeded, mark_seeded,
|
||||
configure_mysql_root_password
|
||||
)
|
||||
from charmhelpers.contrib.hahelpers.cluster import (
|
||||
peer_units,
|
||||
oldest_peer
|
||||
oldest_peer,
|
||||
eligible_leader,
|
||||
is_clustered,
|
||||
is_leader
|
||||
)
|
||||
from mysql import configure_db
|
||||
|
||||
hooks = Hooks()
|
||||
|
||||
@ -33,20 +46,16 @@ hooks = Hooks()
|
||||
@hooks.hook('install')
|
||||
def install():
|
||||
setup_percona_repo()
|
||||
configure_mysql_root_password()
|
||||
render_config() # Render base configuation no cluster
|
||||
apt_update(fatal=True)
|
||||
apt_install(PACKAGES, fatal=True)
|
||||
configure_sstuser()
|
||||
|
||||
|
||||
@hooks.hook('cluster-relation-changed')
|
||||
@hooks.hook('upgrade-charm')
|
||||
@hooks.hook('config-changed')
|
||||
@restart_on_change({MY_CNF: ['mysql']})
|
||||
def cluster_changed():
|
||||
hosts = get_cluster_hosts()
|
||||
clustered = False
|
||||
if len(hosts) > 1:
|
||||
clustered = True
|
||||
def render_config(clustered=False, hosts=[]):
|
||||
if not os.path.exists(os.path.dirname(MY_CNF)):
|
||||
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',
|
||||
@ -55,10 +64,138 @@ def cluster_changed():
|
||||
'cluster_hosts': ",".join(hosts)}
|
||||
)
|
||||
)
|
||||
# This is horrid but stops the bootstrap node
|
||||
# restarting itself when new nodes start joining
|
||||
if clustered and oldest_peer(peer_units()):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@hooks.hook('cluster-relation-changed')
|
||||
@hooks.hook('upgrade-charm')
|
||||
@hooks.hook('config-changed')
|
||||
def cluster_changed():
|
||||
hosts = get_cluster_hosts()
|
||||
clustered = len(hosts) > 1
|
||||
pre_hash = file_hash(MY_CNF)
|
||||
render_config(clustered, hosts)
|
||||
if file_hash(MY_CNF) != pre_hash:
|
||||
oldest = oldest_peer(peer_units())
|
||||
if clustered and not oldest and not seeded():
|
||||
# Bootstrap node into seeded cluster
|
||||
service_restart('mysql')
|
||||
mark_seeded()
|
||||
elif not clustered:
|
||||
# Restart with new configuration
|
||||
service_restart('mysql')
|
||||
|
||||
LEADER_RES = 'res_mysql_vip'
|
||||
|
||||
|
||||
@hooks.hook('shared-db-relation-changed')
|
||||
def shared_db_changed():
|
||||
if not eligible_leader(LEADER_RES):
|
||||
log('MySQL service is peered, bailing shared-db relation'
|
||||
' as this service unit is not the leader')
|
||||
return
|
||||
|
||||
settings = relation_get()
|
||||
if is_clustered():
|
||||
db_host = config('vip')
|
||||
else:
|
||||
db_host = unit_get('private-address')
|
||||
singleset = set([
|
||||
'database',
|
||||
'username',
|
||||
'hostname'
|
||||
])
|
||||
|
||||
if singleset.issubset(settings):
|
||||
# Process a single database configuration
|
||||
password = configure_db(settings['hostname'],
|
||||
settings['database'],
|
||||
settings['username'])
|
||||
relation_set(db_host=db_host,
|
||||
password=password)
|
||||
else:
|
||||
# Process multiple database setup requests.
|
||||
# from incoming relation data:
|
||||
# nova_database=xxx nova_username=xxx nova_hostname=xxx
|
||||
# quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
|
||||
# create
|
||||
#{
|
||||
# "nova": {
|
||||
# "username": xxx,
|
||||
# "database": xxx,
|
||||
# "hostname": xxx
|
||||
# },
|
||||
# "quantum": {
|
||||
# "username": xxx,
|
||||
# "database": xxx,
|
||||
# "hostname": xxx
|
||||
# }
|
||||
#}
|
||||
#
|
||||
databases = {}
|
||||
for k, v in settings.iteritems():
|
||||
db = k.split('_')[0]
|
||||
x = '_'.join(k.split('_')[1:])
|
||||
if db not in databases:
|
||||
databases[db] = {}
|
||||
databases[db][x] = v
|
||||
return_data = {}
|
||||
for db in databases:
|
||||
if singleset.issubset(databases[db]):
|
||||
return_data['_'.join([db, 'password'])] = \
|
||||
configure_db(databases[db]['hostname'],
|
||||
databases[db]['database'],
|
||||
databases[db]['username'])
|
||||
if len(return_data) > 0:
|
||||
relation_set(**return_data)
|
||||
relation_set(db_host=db_host)
|
||||
|
||||
|
||||
@hooks.hook('ha-relation-joined')
|
||||
def ha_relation_joined():
|
||||
vip = config('vip')
|
||||
vip_iface = config('vip_iface')
|
||||
vip_cidr = config('vip_cidr')
|
||||
corosync_bindiface = config('ha-bindiface')
|
||||
corosync_mcastport = config('ha-mcastport')
|
||||
|
||||
if None in [vip, vip_cidr, vip_iface]:
|
||||
log('Insufficient VIP information to configure cluster')
|
||||
sys.exit(1)
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
for rel_id in relation_ids('ha'):
|
||||
relation_set(rid=rel_id,
|
||||
corosync_bindiface=corosync_bindiface,
|
||||
corosync_mcastport=corosync_mcastport,
|
||||
resources=resources,
|
||||
resource_params=resource_params,
|
||||
groups=groups)
|
||||
|
||||
|
||||
@hooks.hook('ha-relation-changed')
|
||||
def ha_relation_changed():
|
||||
clustered = relation_get('clustered')
|
||||
if (clustered and is_leader(LEADER_RES)):
|
||||
log('Cluster configured, notifying other services')
|
||||
# Tell all related services to start using the VIP
|
||||
for r_id in relation_ids('shared-db'):
|
||||
relation_set(rid=r_id,
|
||||
db_host=config('vip'))
|
||||
else:
|
||||
# TODO: Unset any data already set if not leader
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -1,8 +1,10 @@
|
||||
''' General utilities for percona '''
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE
|
||||
import socket
|
||||
import os
|
||||
from charmhelpers.core.host import (
|
||||
lsb_release
|
||||
lsb_release,
|
||||
)
|
||||
from charmhelpers.core.hookenv import (
|
||||
unit_get,
|
||||
@ -14,6 +16,7 @@ from charmhelpers.fetch import (
|
||||
apt_install,
|
||||
filter_installed_packages
|
||||
)
|
||||
from mysql import get_mysql_root_password
|
||||
|
||||
try:
|
||||
import jinja2
|
||||
@ -31,16 +34,30 @@ except ImportError:
|
||||
|
||||
PACKAGES = [
|
||||
'percona-xtradb-cluster-server-5.5',
|
||||
'percona-xtradb-cluster-client-5.5'
|
||||
'percona-xtradb-cluster-client-5.5',
|
||||
'python-mysqldb'
|
||||
]
|
||||
|
||||
KEY = "keys/repo.percona.com"
|
||||
REPO = """deb http://repo.percona.com/apt {release} main
|
||||
deb-src http://repo.percona.com/apt {release} main"""
|
||||
MY_CNF = "/etc/mysql/my.cnf"
|
||||
SEEDED_MARKER = "/var/lib/mysql/seeded"
|
||||
|
||||
|
||||
def seeded():
|
||||
''' Check whether service unit is already seeded '''
|
||||
return os.path.exists(SEEDED_MARKER)
|
||||
|
||||
|
||||
def mark_seeded():
|
||||
''' Mark service unit as seeded '''
|
||||
with open(SEEDED_MARKER, 'w') as seeded:
|
||||
seeded.write('done')
|
||||
|
||||
|
||||
def setup_percona_repo():
|
||||
''' Configure service unit to use percona repositories '''
|
||||
with open('/etc/apt/sources.list.d/percona.list', 'w') as sources:
|
||||
sources.write(REPO.format(release=lsb_release()['DISTRIB_CODENAME']))
|
||||
subprocess.check_call(['apt-key', 'add', KEY])
|
||||
@ -77,11 +94,26 @@ def get_cluster_hosts():
|
||||
unit, relid)))
|
||||
return hosts
|
||||
|
||||
SQL_SST_USER_SETUP = """mysql -u root << EOF
|
||||
SQL_SST_USER_SETUP = """mysql --user=root --password={} << EOF
|
||||
CREATE USER 'sstuser'@'localhost' IDENTIFIED BY 's3cretPass';
|
||||
GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'sstuser'@'localhost';
|
||||
EOF"""
|
||||
|
||||
|
||||
def configure_sstuser():
|
||||
subprocess.check_call(SQL_SST_USER_SETUP, shell=True)
|
||||
subprocess.check_call(SQL_SST_USER_SETUP.format(get_mysql_root_password()),
|
||||
shell=True)
|
||||
|
||||
|
||||
# TODO: mysql charmhelper
|
||||
def configure_mysql_root_password():
|
||||
''' Configure debconf with root password '''
|
||||
dconf = Popen(['debconf-set-selections'], stdin=PIPE)
|
||||
package = "percona-server-server"
|
||||
root_pass = get_mysql_root_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" %
|
||||
(package, package, root_pass))
|
||||
dconf.communicate()
|
||||
dconf.wait()
|
||||
|
1
hooks/shared-db-relation-changed
Symbolic link
1
hooks/shared-db-relation-changed
Symbolic link
@ -0,0 +1 @@
|
||||
percona_hooks.py
|
@ -17,3 +17,7 @@ provides:
|
||||
peers:
|
||||
cluster:
|
||||
interface: percona-cluster
|
||||
requires:
|
||||
ha:
|
||||
interface: hacluster
|
||||
scope: container
|
||||
|
@ -35,3 +35,5 @@ wsrep_cluster_name={{ cluster_name }}
|
||||
|
||||
# Authentication for SST method
|
||||
wsrep_sst_auth="sstuser:s3cretPass"
|
||||
|
||||
!includedir /etc/mysql/conf.d/
|
Loading…
Reference in New Issue
Block a user