Michael Basnight 51454ac504 Furthering the guest agent prepare call
* Fixed minor guestagent bugs
* Added cast_with_consumer
* Added rpc call to instances
2012-03-18 21:49:17 -05:00

496 lines
18 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 OpenStack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Handles all processes within the Guest VM, considering it as a Platform
The :py:class:`GuestManager` class is a :py:class:`nova.manager.Manager` that
handles RPC calls relating to Platform specific operations.
**Related Flags**
"""
import logging
import os
import re
import sys
import uuid
from datetime import date
from sqlalchemy import create_engine
from sqlalchemy import exc
from sqlalchemy import interfaces
from sqlalchemy.sql.expression import text
# from nova.compute import power_state
# from nova.exception import ProcessExecutionError
# from reddwarf.db import api as dbapi
from reddwarf.common.exception import ProcessExecutionError
from reddwarf.common import utils
# from reddwarf.guestagent import utils as guest_utils
from reddwarf.guestagent.db import models
ADMIN_USER_NAME = "os_admin"
LOG = logging.getLogger(__name__)
FLUSH = text("""FLUSH PRIVILEGES;""")
ENGINE = None
MYSQLD_ARGS = None
PREPARING = False
def generate_random_password():
return str(uuid.uuid4())
def get_engine():
"""Create the default engine with the updated admin user"""
#TODO(rnirmal):Based on permissions issues being resolved we may revert
#url = URL(drivername='mysql', host='localhost',
# query={'read_default_file': '/etc/mysql/my.cnf'})
global ENGINE
if ENGINE:
return ENGINE
#ENGINE = create_engine(name_or_url=url)
pwd, err = utils.execute("sudo", "awk", "/password\\t=/{print $3}",
"/etc/mysql/my.cnf")
if not err:
ENGINE = create_engine("mysql://%s:%s@localhost:3306" %
(ADMIN_USER_NAME, pwd.strip()),
pool_recycle=7200, echo=True,
listeners=[KeepAliveConnection()])
else:
LOG.error(_(err))
return ENGINE
def load_mysqld_options():
try:
out, err = utils.execute("/usr/sbin/mysqld", "--print-defaults",
run_as_root=True)
arglist = re.split("\n", out)[1].split()
args = {}
for item in arglist:
if "=" in item:
key, value = item.split("=")
args[key.lstrip("--")] = value
else:
args[item.lstrip("--")] = None
return args
except ProcessExecutionError as e:
return None
class DBaaSAgent(object):
""" Database as a Service Agent Controller """
def create_user(self, users):
"""Create users and grant them privileges for the
specified databases"""
host = "%"
client = LocalSqlClient(get_engine())
with client:
for item in users:
user = models.MySQLUser()
user.deserialize(item)
# TODO(cp16net):Should users be allowed to create users
# 'os_admin' or 'debian-sys-maint'
t = text("""CREATE USER `%s`@:host IDENTIFIED BY '%s';"""
% (user.name, user.password))
client.execute(t, host=host)
for database in user.databases:
mydb = models.MySQLDatabase()
mydb.deserialize(database)
t = text("""
GRANT ALL PRIVILEGES ON `%s`.* TO `%s`@:host;"""
% (mydb.name, user.name))
client.execute(t, host=host)
def list_users(self):
"""List users that have access to the database"""
LOG.debug("---Listing Users---")
users = []
client = LocalSqlClient(get_engine())
with client:
mysql_user = models.MySQLUser()
t = text("""select User from mysql.user where host !=
'localhost';""")
result = client.execute(t)
LOG.debug("result = " + str(result))
for row in result:
LOG.debug("user = " + str(row))
mysql_user = models.MySQLUser()
mysql_user.name = row['User']
users.append(mysql_user.serialize())
LOG.debug("users = " + str(users))
return users
def delete_user(self, user):
"""Delete the specified users"""
client = LocalSqlClient(get_engine())
with client:
mysql_user = models.MySQLUser()
mysql_user.deserialize(user)
t = text("""DROP USER `%s`""" % mysql_user.name)
client.execute(t)
def create_database(self, databases):
"""Create the list of specified databases"""
client = LocalSqlClient(get_engine())
with client:
for item in databases:
mydb = models.MySQLDatabase()
mydb.deserialize(item)
t = text("""CREATE DATABASE IF NOT EXISTS
`%s` CHARACTER SET = %s COLLATE = %s;"""
% (mydb.name, mydb.character_set, mydb.collate))
client.execute(t)
def list_databases(self):
"""List databases the user created on this mysql instance"""
LOG.debug("---Listing Databases---")
databases = []
client = LocalSqlClient(get_engine())
with client:
# If you have an external volume mounted at /var/lib/mysql
# the lost+found directory will show up in mysql as a database
# which will create errors if you try to do any database ops
# on it. So we remove it here if it exists.
t = text('''
SELECT
schema_name as name,
default_character_set_name as charset,
default_collation_name as collation
FROM
information_schema.schemata
WHERE
schema_name not in
('mysql', 'information_schema', 'lost+found')
ORDER BY
schema_name ASC;
''')
database_names = client.execute(t)
LOG.debug("database_names = %r" % database_names)
for database in database_names:
LOG.debug("database = %s " % str(database))
mysql_db = models.MySQLDatabase()
mysql_db.name = database[0]
mysql_db.character_set = database[1]
mysql_db.collate = database[2]
databases.append(mysql_db.serialize())
LOG.debug("databases = " + str(databases))
return databases
def delete_database(self, database):
"""Delete the specified database"""
client = LocalSqlClient(get_engine())
with client:
mydb = models.MySQLDatabase()
mydb.deserialize(database)
t = text("""DROP DATABASE `%s`;""" % mydb.name)
client.execute(t)
def enable_root(self):
"""Enable the root user global access and/or reset the root password"""
host = "%"
user = models.MySQLUser()
user.name = "root"
user.password = generate_random_password()
client = LocalSqlClient(get_engine())
with client:
try:
t = text("""CREATE USER :user@:host;""")
client.execute(t, user=user.name, host=host, pwd=user.password)
except exc.OperationalError as err:
# Ignore, user is already created, just reset the password
# TODO(rnirmal): More fine grained error checking later on
LOG.debug(err)
with client:
t = text("""UPDATE mysql.user SET Password=PASSWORD(:pwd)
WHERE User=:user;""")
client.execute(t, user=user.name, pwd=user.password)
t = text("""GRANT ALL PRIVILEGES ON *.* TO :user@:host
WITH GRANT OPTION;""")
client.execute(t, user=user.name, host=host)
return user.serialize()
def disable_root(self):
"""Disable root access apart from localhost"""
host = "localhost"
pwd = generate_random_password()
user = "root"
client = LocalSqlClient(get_engine())
with client:
t = text("""DELETE FROM mysql.user where User=:user
and Host!=:host""")
client.execute(t, user=user, host=host)
t = text("""UPDATE mysql.user SET Password=PASSWORD(:pwd)
WHERE User=:user;""")
client.execute(t, pwd=pwd, user=user)
return True
def is_root_enabled(self):
"""Return True if root access is enabled; False otherwise."""
client = LocalSqlClient(get_engine())
with client:
mysql_user = models.MySQLUser()
t = text("""SELECT User FROM mysql.user where User = 'root'
and host != 'localhost';""")
result = client.execute(t)
LOG.debug("result = " + str(result))
return result.rowcount != 0
def prepare(self, databases, memory_mb):
"""Makes ready DBAAS on a Guest container."""
global PREPARING
PREPARING = True
from reddwarf.guestagent.pkg import PkgAgent
if not isinstance(self, PkgAgent):
raise TypeError("This must also be an instance of Pkg agent.")
preparer = DBaaSPreparer(self)
preparer.prepare()
self.create_database(databases)
PREPARING = False
def update_status(self):
"""Update the status of the MySQL service"""
global MYSQLD_ARGS
global PREPARING
# instance_id = guest_utils.get_instance_id()
if PREPARING:
#TODO(hub-cap): Fix the guest_status_update
# dbapi.guest_status_update(instance_id, power_state.BUILDING)
return
try:
out, err = utils.execute("/usr/bin/mysqladmin", "ping",
run_as_root=True)
#TODO(hub-cap): Fix the guest_status_update
# dbapi.guest_status_update(instance_id, power_state.RUNNING)
except ProcessExecutionError as e:
try:
out, err = utils.execute("ps", "-C", "mysqld", "h")
pid = out.split()[0]
# TODO(rnirmal): Need to create new statuses for instances
# where the mysql service is up, but unresponsive
#TODO(hub-cap): Fix the guest_status_update
# dbapi.guest_status_update(instance_id, power_state.BLOCKED)
except ProcessExecutionError as e:
if not MYSQLD_ARGS:
MYSQLD_ARGS = load_mysqld_options()
pid_file = MYSQLD_ARGS.get('pid-file',
'/var/run/mysqld/mysqld.pid')
if os.path.exists(pid_file):
pass
#TODO(hub-cap): Fix the guest_status_update
# dbapi.guest_status_update(instance_id,
# power_state.CRASHED)
else:
pass
#TODO(hub-cap): Fix the guest_status_update
# dbapi.guest_status_update(instance_id,
# power_state.SHUTDOWN)
class LocalSqlClient(object):
"""A sqlalchemy wrapper to manage transactions"""
def __init__(self, engine, use_flush=True):
self.engine = engine
self.use_flush = use_flush
def __enter__(self):
self.conn = self.engine.connect()
self.trans = self.conn.begin()
return self.conn
def __exit__(self, type, value, traceback):
if self.trans:
if type is not None: # An error occurred
self.trans.rollback()
else:
if self.use_flush:
self.conn.execute(FLUSH)
self.trans.commit()
self.conn.close()
def execute(self, t, **kwargs):
try:
return self.conn.execute(t, kwargs)
except:
self.trans.rollback()
self.trans = None
raise
class KeepAliveConnection(interfaces.PoolListener):
"""
A connection pool listener that ensures live connections are returned
from the connecction pool at checkout. This alleviates the problem of
MySQL connections timeing out.
"""
def checkout(self, dbapi_con, con_record, con_proxy):
"""Event triggered when a connection is checked out from the pool"""
try:
try:
dbapi_con.ping(False)
except TypeError:
dbapi_con.ping()
except dbapi_con.OperationalError, ex:
if ex.args[0] in (2006, 2013, 2014, 2045, 2055):
raise exc.DisconnectionError()
else:
raise
class DBaaSPreparer(object):
"""Prepares DBaaS on a Guest container."""
TIME_OUT = 1000
def __init__(self, pkg_agent):
""" By default login with root no password for initial setup. """
self.engine = create_engine("mysql://root:@localhost:3306", echo=True)
self.pkg = pkg_agent
def _generate_root_password(self, client):
""" Generate and set a random root password and forget about it. """
t = text("""UPDATE mysql.user SET Password=PASSWORD(:pwd)
WHERE User='root';""")
client.execute(t, pwd=generate_random_password())
def _init_mycnf(self, password):
"""
Install the set of mysql my.cnf templates from dbaas-mycnf package.
The package generates a template suited for the current
container flavor. Update the os_admin user and password
to the my.cnf file for direct login from localhost
"""
orig_mycnf = "/etc/mysql/my.cnf"
final_mycnf = "/var/lib/mysql/my.cnf"
tmp_mycnf = "/tmp/my.cnf.tmp"
dbaas_mycnf = "/etc/dbaas/my.cnf/my.cnf.default"
LOG.debug(_("Installing my.cnf templates"))
self.pkg.pkg_install("dbaas-mycnf", self.TIME_OUT)
if os.path.isfile(dbaas_mycnf):
utils.execute("sudo", "mv", orig_mycnf,
"%(name)s.%(date)s"
% {'name': orig_mycnf,
'date': date.today().isoformat()})
utils.execute("sudo", "cp", dbaas_mycnf, orig_mycnf)
mycnf_file = open(orig_mycnf, 'r')
tmp_file = open(tmp_mycnf, 'w')
for line in mycnf_file:
tmp_file.write(line)
if "[client]" in line:
tmp_file.write("user\t\t= %s\n" % ADMIN_USER_NAME)
tmp_file.write("password\t= %s\n" % password)
mycnf_file.close()
tmp_file.close()
utils.execute("sudo", "mv", tmp_mycnf, final_mycnf)
utils.execute("sudo", "rm", orig_mycnf)
utils.execute("sudo", "ln", "-s", final_mycnf, orig_mycnf)
def _remove_anonymous_user(self, client):
t = text("""DELETE FROM mysql.user WHERE User='';""")
client.execute(t)
def _remove_remote_root_access(self, client):
t = text("""DELETE FROM mysql.user
WHERE User='root'
AND Host!='localhost';""")
client.execute(t)
def _create_admin_user(self, client, password):
"""
Create a os_admin user with a random password
with all privileges similar to the root user
"""
t = text("CREATE USER :user@'localhost';")
client.execute(t, user=ADMIN_USER_NAME)
t = text("""
UPDATE mysql.user SET Password=PASSWORD(:pwd)
WHERE User=:user;
""")
client.execute(t, pwd=password, user=ADMIN_USER_NAME)
t = text("""
GRANT ALL PRIVILEGES ON *.* TO :user@'localhost'
WITH GRANT OPTION;
""")
client.execute(t, user=ADMIN_USER_NAME)
def _install_mysql(self):
"""Install mysql server. The current version is 5.1"""
LOG.debug(_("Installing mysql server"))
self.pkg.pkg_install("mysql-server-5.1", self.TIME_OUT)
#TODO(rnirmal): Add checks to make sure the package got installed
def _restart_mysql(self):
"""
Restart mysql after all the modifications are completed.
List of modifications:
- Remove existing ib_logfile*
"""
# TODO(rnirmal): To be replaced by the mounted volume location
# FIXME once we have volumes in place, use default till then
mysql_base_dir = "/var/lib/mysql"
try:
LOG.debug(_("Restarting mysql..."))
utils.execute("sudo", "service", "mysql", "stop")
# Remove the ib_logfile, if not mysql won't start.
# For some reason wildcards don't seem to work, so
# deleting both the files separately
utils.execute("sudo", "rm", "%s/ib_logfile0" % mysql_base_dir)
utils.execute("sudo", "rm", "%s/ib_logfile1" % mysql_base_dir)
utils.execute("sudo", "service", "mysql", "start")
except ProcessExecutionError:
LOG.error(_("Unable to restart mysql server."))
def prepare(self):
"""Prepare the guest machine with a secure mysql server installation"""
LOG.info(_("Preparing Guest as MySQL Server"))
try:
utils.execute("apt-get", "update", run_as_root=True)
except ProcessExecutionError as e:
LOG.error(_("Error updating the apt sources"))
self._install_mysql()
admin_password = generate_random_password()
client = LocalSqlClient(self.engine)
with client:
self._generate_root_password(client)
self._remove_anonymous_user(client)
self._remove_remote_root_access(client)
self._create_admin_user(client, admin_password)
self._init_mycnf(admin_password)
self._restart_mysql()
LOG.info(_("Dbaas preparation complete."))