contrail
Rev 2410 | Blame | Compare with Previous | Last modification | View Log | RSS feed
'''
Copyright (C) 2010-2011 Contrail consortium.
This file is part of ConPaaS, an integrated runtime environment
for elastic cloud applications.
ConPaaS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ConPaaS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ConPaaS. If not, see <http://www.gnu.org/licenses/>.
Created in February, 2012
@author: alesc, aaasz
'''
import os, io, socket
import ConfigParser
import MySQLdb
from os.path import join, devnull, exists
from os import kill, chown, setuid, setgid
from pwd import getpwnam
from signal import SIGINT, SIGTERM, SIGUSR2, SIGHUP
from subprocess import Popen
from shutil import rmtree, copy2
from Cheetah.Template import Template
from conpaas.core.misc import verify_port, verify_ip_port_list, verify_ip_or_domain
from conpaas.core.log import create_logger
S_INIT = 'INIT'
S_STARTING = 'STARTING'
S_RUNNING = 'RUNNING'
S_STOPPING = 'STOPPING'
S_STOPPED = 'STOPPED'
sql_logger = create_logger(__name__)
class MySQLServer(object):
"""
Holds configuration of the MySQL server.
"""
def __init__(self, config):
"""
Creates a configuration from `config`.
:param config: Configuration read with ConfigParser.
:type config: ConfigParser
:param _dummy_config: Set to `True` when used in unit testing.
:type boolean: boolean value.
"""
sql_logger.debug("Entering init MySQLServerConfiguration")
try:
hostname = socket.gethostname()
# Get connection settings
self.pid_file = "/var/lib/mysql/" + hostname + ".pid"
sql_logger.debug("Trying to get params from configuration file ")
self.conn_location = config.get('MySQL_root_connection', 'location')
self.conn_username = config.get("MySQL_root_connection", "username")
self.conn_password = config.get("MySQL_root_connection", "password")
sql_logger.debug("Got parameters for root connection to MySQL")
# Get MySQL configuration parameters
self.mysqldump_path = config.get('agent', 'MYSQLDUMP_PATH')
self.mycnf_filepath = config.get("MySQL_configuration","my_cnf_file")
self.path_mysql_ssr = config.get("MySQL_configuration","path_mysql_ssr")
self.mysqlconfig = self._load_configuration(self.mycnf_filepath)
# We need to remove the bind-address option (the default one is
# 127.0.0.1 = accepting conections only on local interface)
if self.mysqlconfig.has_option('mysqld', 'bind-address'):
self.mysqlconfig.remove_option('mysqld', 'bind-address')
except ConfigParser.Error, err:
#ex = AgentException(E_CONFIG_READ_FAILED, str(err))
sql_logger.exception('Could not read config file')
except IOError, err:
#ex = AgentException(E_CONFIG_NOT_EXIST, str(err))
sql_logger.exception('Config file doesn\'t exist')
# Creating a supervisor object
sql_logger.debug("Leaving init MySQLServerConfiguration")
def _load_dump(self, f):
sql_logger.debug("Entering load_dump")
try:
mysqldump = f.read()
d = open('/tmp/mysqldump.db', 'w')
d.write(mysqldump)
d.close()
os.system("mysql -u " + self.conn_username + " -p" + self.conn_password + " < /tmp/mysqldump.db")
os.system('rm /tmp/mysqldump.db')
sql_logger.debug("Leaving load_dump")
except Exception as e:
sql_logger.exception('Failed to load dump')
raise e
def start(self):
self.state = S_STARTING
devnull_fd = open(devnull, 'w')
proc = Popen([self.path_mysql_ssr, "start"], stdout=devnull_fd, stderr=devnull_fd, close_fds=True)
if proc.wait() != 0:
sql_logger.critical('Failed to start mysqld (code=%d)' % proc.returncode)
self.state = S_STOPPED
raise OSError('Failed to start web server (code=%d)' % proc.returncode)
self.state = S_RUNNING
sql_logger.info(' started')
def stop(self):
if self.state == S_RUNNING:
self.state = S_STOPPING
if exists(self.pid_file):
try:
pid = int(open(self.pid_file, 'r').read().strip())
except IOError as e:
sql_logger.exception('Failed to open PID file "%s"' % (self.pid_file))
raise e
except (ValueError, TypeError) as e:
sql_logger.exception('PID in "%s" is invalid' % (self.pid_file))
raise e
try:
kill(pid, self.stop_sig)
self.state = S_STOPPED
sql_logger.info('mysqld stopped')
except (IOError, OSError) as e:
sql_logger.exception('Failed to kill mysqld PID=%d' % (pid))
raise e
else:
sql_logger.critical('Could not find PID file %s to kill mysqld' % (self.pid_file))
raise IOError('Could not find PID file %s to kill mysqld' % (self.pid_file))
else:
sql_logger.warning('Request to kill mysqld while it is not running')
def restart(self):
sql_logger.debug("Entering MySQLServer restart")
try:
devnull_fd = open(devnull, 'w')
sql_logger.debug('Restarting with arguments:' + self.config.path_mysql_ssr + " restart")
proc = Popen([self.path_mysql_ssr, "restart"] , stdout=devnull_fd, stderr=devnull_fd, close_fds=True)
sql_logger.debug("Restarting mysql server")
proc.wait()
if exists(self.pid_file) == False:
sql_logger.critical('Failed to restart mysql server.)')
raise OSError('Failed to restart mysql server.')
self.state = S_RUNNING
sql_logger.info('MySQL restarted')
except IOError as e:
sql_logger.exception('Failed to open PID file "%s"' % (self._current_pid_file(increment=-1)))
self.state = S_STOPPED
raise e
except (ValueError, TypeError) as e:
sql_logger.exception('PID in "%s" is invalid' % (self._current_pid_file(increment=-1)))
self.state = S_STOPPED
raise e
sql_logger.debug("Leaving MySQLServer restart")
# This is necessary because of the options without value that are in mysql's my.cnf
# It is fixed in python 3+, where we could just use ConfigParser.
def _load_configuration(self, mycnf_filepath):
"""
Parses MySQL server configuration into
"""
config = ConfigParser.ConfigParser()
file = open(mycnf_filepath, 'r+w')
text = file.read()
file.close()
#comments inside
while text.count("#")>0:
text = text[0:text.index("#")] + text[text.index("\n",text.index("#")):]
zac = 0
while text.count("\n",zac)>1:
if ( text[text.index("\n",zac)+1:text.index("\n",text.index("\n",zac+1))].find("[") == -1
& text[text.index("\n",zac)+1:text.index("\n",text.index("\n",zac+1))].find("=") == -1):
text = text[0:text.index("\n",zac)+1] + text[text.index("\n",text.index("\n",zac+1)):]
zac = text.index("\n", zac+1)
# \n inside
while text.count("\t")>0:
text = text[0:text.index("\t")] + text[text.index("\t")+1:]
while text.count(" ")>0:
text = text[0:text.index(" ")] + text[text.index(" ")+1:]
while text.count("\n\n")>0:
text = text[0:text.index("\n\n")] + text[text.index("\n\n")+1:]
file = open(mycnf_filepath, 'w')
file.write(text)
file.close()
file = open(mycnf_filepath, 'r')
config.readfp(file)
file.close()
return config
class MySQLMaster(MySQLServer):
''' Class describing a MySQL replication master.
It can be initializad with a dump, or without '''
def __init__(self, config=None, master_server_id=0, dump=None):
sql_logger.debug("Entering init MySQLServerConfiguration")
MySQLServer.__init__(self, config)
# Configure it as a master
path=self.mycnf_filepath
self.mysqlconfig.set("mysqld", "server-id", master_server_id)
self.mysqlconfig.set("mysqld", "log_bin", "/var/log/mysql/mysql-bin.log")
os.remove(path)
newf = open(path,"w")
self.mysqlconfig.write(newf)
newf.close()
# Start the replication master
self.start()
#if dump != None:
# self.load_dump(dump)
# Change root password - not set as installation
os.system("mysqladmin -u root password " + self.conn_password)
'''Before creating a data snapshot or starting
the replication process, you should record the
position of the binary log on the master. You will
need this information when configuring the slave so
that the slave knows where within the binary log to
start executing events. See Section 15.1.1.4, Obtaining
the Replication Master Binary Log Coordinates.
1st session
mysql> FLUSH TABLES WITH READ LOCK;
2nd session
mysql > SHOW MASTER STATUS;
record the values
close 2nd session
on the master
mysqldump --all-databases --lock-all-tables >dbdump.db
1st session
mysql>UNLOCK TABLES;
'''
def take_snapshot(self):
'''1st session
'''
db1 = MySQLdb.connect(self.conn_location, self.conn_username, self.conn_password)
exc = db1.cursor()
exc.execute("FLUSH TABLES WITH READ LOCK;")
'''2nd session
'''
db2 = MySQLdb.connect(self.conn_location, self.conn_username, self.conn_password)
exc = db2.cursor()
exc.execute("SHOW MASTER STATUS;")
rows = exc.fetchall()
db2.close()
i = 0
ret = {}
for row in rows:
i = i+1
ret['position' + str(i)] = {'binfile': row[0], 'position': row[1], 'mysqldump_path': self.mysqldump_path}
# dump everything except the information_schema and mysql database
os.system("mysql --user=" + self.conn_username + " --password=" + self.conn_password + \
" --batch --skip-column-names " + \
"--execute=\"SHOW DATABASES\" | egrep -v \"information_schema|mysql|performance_schema|test\" " + \
"| xargs mysqldump --user=" + self.conn_username + " --password=" + self.conn_password + \
" --lock-all-tables --databases > " + self.mysqldump_path)
exc = db1.cursor()
exc.execute("UNLOCK TABLES;")
db1.close()
return ret
def register_slave(self, slave_ip):
sql_logger.debug("Entering init register_slave")
db = MySQLdb.connect(self.conn_location, self.conn_username, self.conn_password)
exc = db.cursor()
exc.execute('GRANT REPLICATION SLAVE ON *.*' \
' TO \'%s\'@\'%s\' IDENTIFIED BY \'%s\';' \
% (self.conn_username, slave_ip, self.conn_password))
db.close()
# TODO: test if successful
return
def status(self):
return {'state': self.state,
'port': self.port
}
def add_user(self, new_username, new_password):
db = MySQLdb.connect(self.conn_location, self.conn_username, self.conn_password)
exc = db.cursor()
exc.execute ("create user '" + new_username + "'@'localhost' identified by '" + new_password + "'")
exc.execute ("grant all privileges on *.* TO '" + new_username + "'@'localhost' with grant option;")
exc.execute ("create user '" + new_username + "'@'%' identified by '" + new_password + "'")
exc.execute ("grant all privileges on *.* TO '" + new_username + "'@'%' with grant option;")
db.close()
def remove_user(self, username):
db = MySQLdb.connect(self.conn_location, self.conn_username, self.conn_password)
exc = db.cursor()
exc.execute ("drop user '" + username +"'@'localhost'")
exc.execute ("drop user '" + username +"'@'%'")
db.close()
def get_users(self):
db = MySQLdb.connect(self.conn_location, self.conn_username, self.conn_password)
exc = db.cursor()
exc.execute("SELECT user, host FROM mysql.user")
rows = exc.fetchall()
db.close()
ret = {'opState': 'OK'}
i = 0
for row in rows:
i = i+1
ret['info' + str(i)] = {'location': row[1], 'username': row[0]}
return ret
class MySQLSlave(MySQLServer):
''' Class describing a MySQL replication slave.
It initializes with a, or without '''
def __init__(self, config = None, master_host = None, \
master_log_file = None, master_log_pos = 0, \
mysqldump_file = None, slave_server_id = 0):
MySQLServer.__init__(self, config)
path=self.mycnf_filepath
self.mysqlconfig.set("mysqld", "server-id", slave_server_id)
#self.mysqlconfig.set("mysqld", "skip-slave-start", slave_server_id)
os.remove(path)
newf=open(path,"w")
self.mysqlconfig.write(newf)
newf.close()
# Start the replication slave
self.start()
# Change root password - not set as installation
os.system("mysqladmin -u root password " + self.conn_password)
# Configure it as a slave
db = MySQLdb.connect(self.conn_location, self.conn_username, self.conn_password)
exc = db.cursor()
query=(("CHANGE MASTER TO MASTER_HOST='%s', " +
"MASTER_USER='%s', " +
"MASTER_PASSWORD='%s', "
"MASTER_LOG_FILE='%s', " +
"MASTER_LOG_POS=%s;") % (master_host, self.conn_username, self.conn_password, master_log_file, master_log_pos))
exc.execute(query)
# TODO: get the dump from the master ( replication from another slave?? )
if mysqldump_file != None:
self._load_dump(mysqldump_file)
# Start the slave thread
query=('START SLAVE;')
exc.execute(query)
db.close()
def status(self):
return {'state': self.state,
'port': self.port
}