2012-07-12 01:00:55 +02:00
#!/usr/bin/python
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
# Sponsored by Four Kitchens http://fourkitchens.com.
#
# This file is part of Ansible
#
# Ansible 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.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
2012-09-29 16:15:41 +02:00
DOCUMENTATION = '''
- - -
module : mysql_user
short_description : Adds or removes a user from a MySQL database .
description :
- Adds or removes a user from a MySQL database .
2013-11-28 03:23:03 +01:00
version_added : " 0.6 "
2012-09-29 16:15:41 +02:00
options :
name :
description :
- name of the user ( role ) to add or remove
required : true
2015-10-22 14:45:50 +02:00
user_anonymous :
description :
- username is to be ignored and anonymous users with no username
handled
required : false
choices : [ " yes " , " no " ]
default : no
2012-09-29 16:15:41 +02:00
password :
description :
2015-10-27 18:17:24 +01:00
- set the user ' s password. (Required when adding a user)
2012-09-29 16:15:41 +02:00
required : false
default : null
2015-12-12 02:29:45 +01:00
encrypted :
2014-10-02 19:05:35 +02:00
description :
2015-06-23 22:57:18 +02:00
- Indicate that the ' password ' field is a ` mysql_native_password ` hash
2014-10-02 19:05:35 +02:00
required : false
2015-06-23 22:57:18 +02:00
choices : [ " yes " , " no " ]
default : " no "
version_added : " 2.0 "
2012-09-29 16:15:41 +02:00
host :
description :
- the ' host ' part of the MySQL username
required : false
default : localhost
2015-10-22 14:45:50 +02:00
host_all :
description :
- override the host option , making ansible apply changes to
all hostnames for a given user . This option cannot be used
when creating users
required : false
choices : [ " yes " , " no " ]
default : " no "
2012-09-29 16:15:41 +02:00
login_user :
description :
- The username used to authenticate with
required : false
default : null
login_password :
description :
2012-09-30 12:21:35 +02:00
- The password used to authenticate with
2012-09-29 16:15:41 +02:00
required : false
default : null
login_host :
description :
- Host running the database
required : false
default : localhost
2013-09-05 17:25:34 +02:00
login_port :
description :
- Port of the MySQL server
required : false
default : 3306
2013-10-11 15:14:00 +02:00
version_added : ' 1.4 '
2013-03-15 16:40:54 +01:00
login_unix_socket :
description :
- The path to a Unix domain socket for local connections
required : false
default : null
2012-09-29 16:15:41 +02:00
priv :
description :
2012-09-30 12:21:35 +02:00
- " MySQL privileges string in the format: C(db.table:priv1,priv2) "
2012-09-29 16:15:41 +02:00
required : false
default : null
2013-10-03 13:00:17 +02:00
append_privs :
description :
- Append the privileges defined by priv to the existing ones for this
user instead of overwriting existing ones .
required : false
choices : [ " yes " , " no " ]
default : " no "
version_added : " 1.4 "
2012-09-29 16:15:41 +02:00
state :
description :
2013-07-29 23:48:49 +02:00
- Whether the user should exist . When C ( absent ) , removes
the user .
2012-09-29 16:15:41 +02:00
required : false
default : present
choices : [ " present " , " absent " ]
2013-07-01 12:56:04 +02:00
check_implicit_admin :
description :
- Check if mysql allows login as root / nopassword before trying supplied credentials .
required : false
2015-05-18 22:33:46 +02:00
choices : [ " yes " , " no " ]
default : " no "
2013-07-20 18:33:42 +02:00
version_added : " 1.3 "
2015-02-20 18:30:27 +01:00
update_password :
required : false
default : always
choices : [ ' always ' , ' on_create ' ]
2015-03-31 21:31:54 +02:00
version_added : " 2.0 "
2015-02-20 18:30:27 +01:00
description :
- C ( always ) will update passwords if they differ . C ( on_create ) will only set the password for newly created users .
2014-09-30 09:08:32 +02:00
config_file :
description :
2015-04-22 21:58:56 +02:00
- Specify a config file from which user and password are to be read
2014-09-30 09:08:32 +02:00
required : false
2015-04-22 21:58:56 +02:00
default : ' ~/.my.cnf '
version_added : " 2.0 "
2012-09-29 16:15:41 +02:00
notes :
2012-10-01 09:18:54 +02:00
- Requires the MySQLdb Python package on the remote host . For Ubuntu , this
is as easy as apt - get install python - mysqldb .
2015-07-22 22:55:35 +02:00
- Both C ( login_password ) and C ( login_user ) are required when you are
2012-10-01 09:18:54 +02:00
passing credentials . If none are present , the module will attempt to read
the credentials from C ( ~ / . my . cnf ) , and finally fall back to using the MySQL
default login of ' root ' with no password .
2013-03-01 23:17:00 +01:00
- " MySQL server installs with default login_user of ' root ' and no password. To secure this user
2013-02-28 17:25:09 +01:00
as part of an idempotent playbook , you must create at least two tasks : the first must change the root user ' s password,
without providing any login_user / login_password details . The second must drop a ~ / . my . cnf file containing
the new root credentials . Subsequent runs of the playbook will then succeed by reading the new credentials from
2013-03-01 23:17:00 +01:00
the file . "
2015-06-23 22:57:18 +02:00
- Currently , there is only support for the ` mysql_native_password ` encryted password hash module .
2013-02-28 17:25:09 +01:00
2014-09-30 09:08:32 +02:00
requirements : [ " MySQLdb " ]
2015-12-12 02:29:45 +01:00
author : " Jonathan Mainguy (@Jmainguy) "
2012-09-29 16:15:41 +02:00
'''
2013-03-15 15:58:27 +01:00
EXAMPLES = """
2015-12-14 17:46:32 +01:00
# Removes anonymous user account for localhost (the name parameter is required, but ignored)
- mysql_user : name = anonymous user_anonymous = yes host = localhost state = absent
# Removes all anonymous user accounts
- mysql_user : name = anonymous user_anonymous = yes host_all = yes state = absent
2013-06-14 11:53:43 +02:00
# Create database user with name 'bob' and password '12345' with all database privileges
- mysql_user : name = bob password = 12345 priv = * . * : ALL state = present
2015-06-23 22:57:18 +02:00
# Create database user with name 'bob' and previously hashed mysql native password '*EE0D72C1085C46C5278932678FBE2C6A782821B4' with all database privileges
- mysql_user : name = bob password = ' *EE0D72C1085C46C5278932678FBE2C6A782821B4 ' encrypted = yes priv = * . * : ALL state = present
2014-01-14 13:00:25 +01:00
# Creates database user 'bob' and password '12345' with all database privileges and 'WITH GRANT OPTION'
- mysql_user : name = bob password = 12345 priv = * . * : ALL , GRANT state = present
2015-05-04 13:54:03 +02:00
# Modify user Bob to require SSL connections. Note that REQUIRESSL is a special privilege that should only apply to *.* by itself.
2015-02-25 15:49:05 +01:00
- mysql_user : name = bob append_privs = true priv = * . * : REQUIRESSL state = present
2015-01-08 17:26:22 +01:00
2015-10-22 14:45:50 +02:00
# Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials.
2013-06-14 11:53:43 +02:00
- mysql_user : login_user = root login_password = 123456 name = sally state = absent
2015-10-22 14:45:50 +02:00
# Ensure no user named 'sally' exists at all
- mysql_user : name = sally host_all = yes state = absent
2014-05-20 20:41:18 +02:00
# Specify grants composed of more than one word
- mysql_user : name = replication password = 12345 priv = * . * : " REPLICATION CLIENT " state = present
2015-12-08 18:09:50 +01:00
# Revoke all privileges for user 'bob' and password '12345'
2014-07-22 17:24:09 +02:00
- mysql_user : name = bob password = 12345 priv = * . * : USAGE state = present
2013-06-14 11:53:43 +02:00
# Example privileges string format
mydb . * : INSERT , UPDATE / anotherdb . * : SELECT / yetanotherdb . * : ALL
# Example using login_unix_socket to connect to server
- mysql_user : name = root password = abc123 login_unix_socket = / var / run / mysqld / mysqld . sock
2013-03-15 15:58:27 +01:00
# Example .my.cnf file for setting the root password
2013-06-14 11:53:43 +02:00
2013-03-15 15:58:27 +01:00
[ client ]
user = root
password = n < _665 { vS43y
"""
import getpass
2013-10-18 23:42:40 +02:00
import tempfile
2015-05-06 23:44:40 +02:00
import re
2014-10-02 19:05:35 +02:00
import string
2012-07-12 01:00:55 +02:00
try :
2012-07-22 19:11:39 +02:00
import MySQLdb
2012-07-12 01:00:55 +02:00
except ImportError :
2012-07-22 19:11:39 +02:00
mysqldb_found = False
else :
mysqldb_found = True
2012-07-12 01:00:55 +02:00
2014-11-26 17:26:53 +01:00
VALID_PRIVS = frozenset ( ( ' CREATE ' , ' DROP ' , ' GRANT ' , ' GRANT OPTION ' ,
' LOCK TABLES ' , ' REFERENCES ' , ' EVENT ' , ' ALTER ' ,
' DELETE ' , ' INDEX ' , ' INSERT ' , ' SELECT ' , ' UPDATE ' ,
2014-11-25 10:46:09 +01:00
' CREATE TEMPORARY TABLES ' , ' TRIGGER ' , ' CREATE VIEW ' ,
' SHOW VIEW ' , ' ALTER ROUTINE ' , ' CREATE ROUTINE ' ,
2014-12-10 17:53:55 +01:00
' EXECUTE ' , ' FILE ' , ' CREATE TABLESPACE ' , ' CREATE USER ' ,
' PROCESS ' , ' PROXY ' , ' RELOAD ' , ' REPLICATION CLIENT ' ,
' REPLICATION SLAVE ' , ' SHOW DATABASES ' , ' SHUTDOWN ' ,
2015-01-08 17:26:22 +01:00
' SUPER ' , ' ALL ' , ' ALL PRIVILEGES ' , ' USAGE ' , ' REQUIRESSL ' ) )
2014-11-25 10:46:09 +01:00
class InvalidPrivsError ( Exception ) :
pass
2012-07-12 01:00:55 +02:00
# ===========================================
# MySQL module specific support methods.
#
2015-05-08 19:40:26 +02:00
def connect ( module , login_user = None , login_password = None , config_file = ' ' ) :
2014-09-30 09:08:32 +02:00
config = {
' host ' : module . params [ ' login_host ' ] ,
' db ' : ' mysql '
}
if module . params [ ' login_unix_socket ' ] :
config [ ' unix_socket ' ] = module . params [ ' login_unix_socket ' ]
else :
config [ ' port ' ] = module . params [ ' login_port ' ]
2015-04-22 20:32:39 +02:00
if os . path . exists ( config_file ) :
config [ ' read_default_file ' ] = config_file
2015-04-30 21:49:32 +02:00
# If login_user or login_password are given, they should override the
# config file
if login_user is not None :
2014-09-30 09:08:32 +02:00
config [ ' user ' ] = login_user
2015-04-30 21:49:32 +02:00
if login_password is not None :
2014-09-30 09:08:32 +02:00
config [ ' passwd ' ] = login_password
2015-04-30 21:49:32 +02:00
2014-09-30 09:08:32 +02:00
db_connection = MySQLdb . connect ( * * config )
return db_connection . cursor ( )
2015-06-23 22:57:18 +02:00
# User Authentication Management was change in MySQL 5.7
# This is a generic check for if the server version is less than version 5.7
def server_version_check ( cursor ) :
cursor . execute ( " SELECT VERSION() " ) ;
result = cursor . fetchone ( )
version_str = result [ 0 ]
version = version_str . split ( ' . ' )
if ( int ( version [ 0 ] ) < = 5 and int ( version [ 1 ] ) < 7 ) :
return True
else :
return False
2015-10-22 14:45:50 +02:00
def user_exists ( cursor , user , host , host_all ) :
if host_all :
2015-11-04 17:37:18 +01:00
cursor . execute ( " SELECT count(*) FROM user WHERE user = %s " , user )
2015-10-22 14:45:50 +02:00
else :
cursor . execute ( " SELECT count(*) FROM user WHERE user = %s AND host = %s " , ( user , host ) )
2012-07-12 01:00:55 +02:00
count = cursor . fetchone ( )
return count [ 0 ] > 0
2015-12-14 17:40:20 +01:00
def user_add ( cursor , user , host , host_all , password , encrypted , new_priv ) :
2015-10-22 14:45:50 +02:00
# we cannot create users without a proper hostname
if host_all :
return False
2015-06-23 22:57:18 +02:00
if password and encrypted :
cursor . execute ( " CREATE USER %s @ %s IDENTIFIED BY PASSWORD %s " , ( user , host , password ) )
elif password and not encrypted :
2014-10-02 19:05:35 +02:00
cursor . execute ( " CREATE USER %s @ %s IDENTIFIED BY %s " , ( user , host , password ) )
2015-12-14 17:40:20 +01:00
2012-07-12 01:00:55 +02:00
if new_priv is not None :
for db_table , priv in new_priv . iteritems ( ) :
2012-07-22 19:11:39 +02:00
privileges_grant ( cursor , user , host , db_table , priv )
2012-07-12 01:00:55 +02:00
return True
2014-10-02 19:05:35 +02:00
def is_hash ( password ) :
ishash = False
2015-06-23 22:57:18 +02:00
if len ( password ) == 41 and password [ 0 ] == ' * ' :
if frozenset ( password [ 1 : ] ) . issubset ( string . hexdigits ) :
ishash = True
2014-10-02 19:05:35 +02:00
return ishash
2015-12-14 17:40:20 +01:00
def user_mod ( cursor , user , host , host_all , password , encrypted , new_priv , append_privs ) :
2012-07-12 01:00:55 +02:00
changed = False
Only revoke GRANT OPTION when user actually has it
When revoking privileges from a user, the GRANT OPTION is always
revoked, even if the user doesn't have it. If the user exists, this
doesn't give an error, but if the user doesn't exist, it does:
mysql> GRANT ALL ON test.* TO 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE GRANT OPTION ON test.* FROM 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE GRANT OPTION ON test.* FROM 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE ALL ON test.* FROM 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE GRANT OPTION ON test.* FROM 'test'@'localhost';
ERROR 1141 (42000): There is no such grant defined for user 'test' on
host 'localhost'
Additionally, in MySQL 5.6 this breaks replication because of
http://bugs.mysql.com/bug.php?id=68892.
Rather than revoking the GRANT OPTION and catching the error, check if
the user actually has it and only revoke it when he does.
2013-07-23 13:00:29 +02:00
grant_option = False
2012-07-12 01:00:55 +02:00
2015-10-22 14:45:50 +02:00
if host_all :
hostnames = user_get_hostnames ( cursor , user )
else :
hostnames = [ host ]
for host in hostnames :
2015-12-14 17:40:20 +01:00
# Handle clear text and hashed passwords.
if bool ( password ) :
# Determine what user management method server uses
old_user_mgmt = server_version_check ( cursor )
2015-06-23 22:57:18 +02:00
if old_user_mgmt :
2015-12-14 17:40:20 +01:00
cursor . execute ( " SELECT password FROM user WHERE user = %s AND host = %s " , ( user , host ) )
else :
cursor . execute ( " SELECT authentication_string FROM user WHERE user = %s AND host = %s " , ( user , host ) )
2015-10-22 14:45:50 +02:00
current_pass_hash = cursor . fetchone ( )
2015-12-14 17:40:20 +01:00
if encrypted :
encrypted_string = ( password )
if is_hash ( password ) :
if current_pass_hash [ 0 ] != encrypted_string :
if old_user_mgmt :
cursor . execute ( " SET PASSWORD FOR %s @ %s = %s " , ( user , host , password ) )
else :
cursor . execute ( " ALTER USER %s @ %s IDENTIFIED WITH mysql_native_password AS %s " , ( user , host , password ) )
changed = True
else :
module . fail_json ( msg = " encrypted was specified however it does not appear to be a valid hash expecting: *SHA1(SHA1(your_password)) " )
2015-06-23 22:57:18 +02:00
else :
if old_user_mgmt :
2015-12-14 17:40:20 +01:00
cursor . execute ( " SELECT PASSWORD( %s ) " , ( password , ) )
2015-06-23 22:57:18 +02:00
else :
2015-12-14 17:40:20 +01:00
cursor . execute ( " SELECT CONCAT( ' * ' , UCASE(SHA1(UNHEX(SHA1( %s ))))) " , ( password , ) )
new_pass_hash = cursor . fetchone ( )
if current_pass_hash [ 0 ] != new_pass_hash [ 0 ] :
if old_user_mgmt :
cursor . execute ( " SET PASSWORD FOR %s @ %s = PASSWORD( %s ) " , ( user , host , password ) )
else :
cursor . execute ( " ALTER USER %s @ %s IDENTIFIED BY %s " , ( user , host , password ) )
changed = True
# Handle privileges
if new_priv is not None :
curr_priv = privileges_get ( cursor , user , host )
# If the user has privileges on a db.table that doesn't appear at all in
# the new specification, then revoke all privileges on it.
for db_table , priv in curr_priv . iteritems ( ) :
# If the user has the GRANT OPTION on a db.table, revoke it first.
if " GRANT " in priv :
grant_option = True
if db_table not in new_priv :
if user != " root " and " PROXY " not in priv and not append_privs :
privileges_revoke ( cursor , user , host , db_table , priv , grant_option )
changed = True
# If the user doesn't currently have any privileges on a db.table, then
# we can perform a straight grant operation.
for db_table , priv in new_priv . iteritems ( ) :
if db_table not in curr_priv :
privileges_grant ( cursor , user , host , db_table , priv )
changed = True
2012-07-12 01:00:55 +02:00
2015-10-22 14:45:50 +02:00
# Handle privileges
if new_priv is not None :
curr_priv = privileges_get ( cursor , user , host )
# If the user has privileges on a db.table that doesn't appear at all in
# the new specification, then revoke all privileges on it.
for db_table , priv in curr_priv . iteritems ( ) :
# If the user has the GRANT OPTION on a db.table, revoke it first.
if " GRANT " in priv :
grant_option = True
if db_table not in new_priv :
if user != " root " and " PROXY " not in priv and not append_privs :
privileges_revoke ( cursor , user , host , db_table , priv , grant_option )
changed = True
# If the user doesn't currently have any privileges on a db.table, then
# we can perform a straight grant operation.
for db_table , priv in new_priv . iteritems ( ) :
if db_table not in curr_priv :
privileges_grant ( cursor , user , host , db_table , priv )
changed = True
# If the db.table specification exists in both the user's current privileges
# and in the new privileges, then we need to see if there's a difference.
db_table_intersect = set ( new_priv . keys ( ) ) & set ( curr_priv . keys ( ) )
for db_table in db_table_intersect :
priv_diff = set ( new_priv [ db_table ] ) ^ set ( curr_priv [ db_table ] )
if ( len ( priv_diff ) > 0 ) :
if not append_privs :
privileges_revoke ( cursor , user , host , db_table , curr_priv [ db_table ] , grant_option )
privileges_grant ( cursor , user , host , db_table , new_priv [ db_table ] )
changed = True
2012-07-12 01:00:55 +02:00
return changed
2015-10-22 14:45:50 +02:00
def user_delete ( cursor , user , host , host_all ) :
if host_all :
hostnames = user_get_hostnames ( cursor , user )
for hostname in hostnames :
cursor . execute ( " DROP USER %s @ %s " , ( user , hostname ) )
else :
cursor . execute ( " DROP USER %s @ %s " , ( user , host ) )
2012-07-12 01:00:55 +02:00
return True
2015-10-22 14:45:50 +02:00
def user_get_hostnames ( cursor , user ) :
cursor . execute ( " SELECT Host FROM mysql.user WHERE user = %s " , user )
hostnames_raw = cursor . fetchall ( )
hostnames = [ ]
for hostname_raw in hostnames_raw :
hostnames . append ( hostname_raw [ 0 ] )
return hostnames
2012-07-22 19:11:39 +02:00
def privileges_get ( cursor , user , host ) :
2012-07-12 01:00:55 +02:00
""" MySQL doesn ' t have a better method of getting privileges aside from the
SHOW GRANTS query syntax , which requires us to then parse the returned string .
Here ' s an example of the string that is returned from MySQL:
GRANT USAGE ON * . * TO ' user ' @ ' localhost ' IDENTIFIED BY ' pass ' ;
This function makes the query and returns a dictionary containing the results .
The dictionary format is the same as that returned by privileges_unpack ( ) below .
"""
output = { }
2014-11-25 10:46:09 +01:00
cursor . execute ( " SHOW GRANTS FOR %s @ %s " , ( user , host ) )
2012-07-12 01:00:55 +02:00
grants = cursor . fetchall ( )
2012-11-15 02:02:39 +01:00
def pick ( x ) :
if x == ' ALL PRIVILEGES ' :
return ' ALL '
else :
return x
2012-07-12 01:00:55 +02:00
for grant in grants :
2014-12-25 23:36:51 +01:00
res = re . match ( " GRANT (.+) ON (.+) TO ' .* ' @ ' .+ ' ( IDENTIFIED BY PASSWORD ' .+ ' )? ?(.*) " , grant [ 0 ] )
2012-07-12 01:00:55 +02:00
if res is None :
2014-12-01 16:15:27 +01:00
raise InvalidPrivsError ( ' unable to parse the MySQL grant string: %s ' % grant [ 0 ] )
2012-07-12 01:00:55 +02:00
privileges = res . group ( 1 ) . split ( " , " )
2012-11-15 02:02:39 +01:00
privileges = [ pick ( x ) for x in privileges ]
2012-10-24 14:32:49 +02:00
if " WITH GRANT OPTION " in res . group ( 4 ) :
2012-10-31 01:42:07 +01:00
privileges . append ( ' GRANT ' )
2015-01-08 17:26:22 +01:00
if " REQUIRE SSL " in res . group ( 4 ) :
privileges . append ( ' REQUIRESSL ' )
2013-08-26 18:16:34 +02:00
db = res . group ( 2 )
2012-07-12 01:00:55 +02:00
output [ db ] = privileges
return output
def privileges_unpack ( priv ) :
""" Take a privileges string, typically passed as a parameter, and unserialize
it into a dictionary , the same format as privileges_get ( ) above . We have this
custom format to avoid using YAML / JSON strings inside YAML playbooks . Example
of a privileges string :
mydb . * : INSERT , UPDATE / anotherdb . * : SELECT / yetanother . * : ALL
The privilege USAGE stands for no privileges , so we add that in on * . * if it ' s
not specified in the string , as MySQL will always provide this by default .
"""
output = { }
2015-05-06 23:44:40 +02:00
privs = [ ]
2014-12-09 23:14:16 +01:00
for item in priv . strip ( ) . split ( ' / ' ) :
pieces = item . strip ( ) . split ( ' : ' )
2015-08-19 06:22:31 +02:00
dbpriv = pieces [ 0 ] . rsplit ( " . " , 1 )
# Do not escape if privilege is for database '*' (all databases)
if dbpriv [ 0 ] . strip ( ' ` ' ) != ' * ' :
pieces [ 0 ] = " ` %s `. %s " % ( dbpriv [ 0 ] . strip ( ' ` ' ) , dbpriv [ 1 ] )
2015-05-06 23:44:40 +02:00
if ' ( ' in pieces [ 1 ] :
output [ pieces [ 0 ] ] = re . split ( r ' , \ s*(?=[^)]*(?: \ (|$)) ' , pieces [ 1 ] . upper ( ) )
for i in output [ pieces [ 0 ] ] :
privs . append ( re . sub ( r ' \ (.* \ ) ' , ' ' , i ) )
else :
output [ pieces [ 0 ] ] = pieces [ 1 ] . upper ( ) . split ( ' , ' )
privs = output [ pieces [ 0 ] ]
new_privs = frozenset ( privs )
2014-11-25 10:46:09 +01:00
if not new_privs . issubset ( VALID_PRIVS ) :
raise InvalidPrivsError ( ' Invalid privileges specified: %s ' % new_privs . difference ( VALID_PRIVS ) )
2012-07-12 01:00:55 +02:00
if ' *.* ' not in output :
output [ ' *.* ' ] = [ ' USAGE ' ]
2015-01-08 17:26:22 +01:00
# if we are only specifying something like REQUIRESSL in *.* we still need
# to add USAGE as a privilege to avoid syntax errors
if priv . find ( ' REQUIRESSL ' ) != - 1 and ' USAGE ' not in output [ ' *.* ' ] :
output [ ' *.* ' ] . append ( ' USAGE ' )
2012-07-12 01:00:55 +02:00
return output
2015-05-06 00:54:02 +02:00
def privileges_revoke ( cursor , user , host , db_table , priv , grant_option ) :
2014-12-01 19:38:47 +01:00
# Escape '%' since mysql db.execute() uses a format string
db_table = db_table . replace ( ' % ' , ' %% ' )
Only revoke GRANT OPTION when user actually has it
When revoking privileges from a user, the GRANT OPTION is always
revoked, even if the user doesn't have it. If the user exists, this
doesn't give an error, but if the user doesn't exist, it does:
mysql> GRANT ALL ON test.* TO 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE GRANT OPTION ON test.* FROM 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE GRANT OPTION ON test.* FROM 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE ALL ON test.* FROM 'test'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> REVOKE GRANT OPTION ON test.* FROM 'test'@'localhost';
ERROR 1141 (42000): There is no such grant defined for user 'test' on
host 'localhost'
Additionally, in MySQL 5.6 this breaks replication because of
http://bugs.mysql.com/bug.php?id=68892.
Rather than revoking the GRANT OPTION and catching the error, check if
the user actually has it and only revoke it when he does.
2013-07-23 13:00:29 +02:00
if grant_option :
2014-11-25 10:46:09 +01:00
query = [ " REVOKE GRANT OPTION ON %s " % mysql_quote_identifier ( db_table , ' table ' ) ]
query . append ( " FROM %s @ %s " )
query = ' ' . join ( query )
cursor . execute ( query , ( user , host ) )
2015-05-26 19:36:46 +02:00
priv_string = " , " . join ( [ p for p in priv if p not in ( ' GRANT ' , ' REQUIRESSL ' ) ] )
2015-05-06 00:54:02 +02:00
query = [ " REVOKE %s ON %s " % ( priv_string , mysql_quote_identifier ( db_table , ' table ' ) ) ]
2014-11-25 10:46:09 +01:00
query . append ( " FROM %s @ %s " )
query = ' ' . join ( query )
2015-01-08 22:41:15 +01:00
cursor . execute ( query , ( user , host ) )
2012-07-12 01:00:55 +02:00
2012-07-22 19:11:39 +02:00
def privileges_grant ( cursor , user , host , db_table , priv ) :
2014-12-01 19:38:47 +01:00
# Escape '%' since mysql db.execute uses a format string and the
# specification of db and table often use a % (SQL wildcard)
db_table = db_table . replace ( ' % ' , ' %% ' )
2015-05-26 19:36:46 +02:00
priv_string = " , " . join ( [ p for p in priv if p not in ( ' GRANT ' , ' REQUIRESSL ' ) ] )
2014-11-25 10:46:09 +01:00
query = [ " GRANT %s ON %s " % ( priv_string , mysql_quote_identifier ( db_table , ' table ' ) ) ]
query . append ( " TO %s @ %s " )
2012-10-18 19:27:18 +02:00
if ' GRANT ' in priv :
2015-01-08 22:41:15 +01:00
query . append ( " WITH GRANT OPTION " )
2015-01-08 17:26:22 +01:00
if ' REQUIRESSL ' in priv :
2015-01-08 22:41:15 +01:00
query . append ( " REQUIRE SSL " )
2014-11-25 10:46:09 +01:00
query = ' ' . join ( query )
2015-01-08 22:41:15 +01:00
cursor . execute ( query , ( user , host ) )
2013-03-25 14:53:04 +01:00
2012-07-12 01:00:55 +02:00
# ===========================================
# Module execution.
#
2012-07-22 19:11:39 +02:00
def main ( ) :
module = AnsibleModule (
argument_spec = dict (
2012-07-31 00:15:24 +02:00
login_user = dict ( default = None ) ,
login_password = dict ( default = None ) ,
login_host = dict ( default = " localhost " ) ,
2015-03-03 23:23:07 +01:00
login_port = dict ( default = 3306 , type = ' int ' ) ,
2012-08-03 12:35:18 +02:00
login_unix_socket = dict ( default = None ) ,
2012-08-01 06:21:36 +02:00
user = dict ( required = True , aliases = [ ' name ' ] ) ,
2015-10-22 14:45:50 +02:00
user_anonymous = dict ( type = " bool " , default = " no " ) ,
2015-06-16 06:08:06 +02:00
password = dict ( default = None , no_log = True ) ,
2015-06-23 22:57:18 +02:00
encrypted = dict ( default = False , type = ' bool ' ) ,
2012-07-22 19:11:39 +02:00
host = dict ( default = " localhost " ) ,
2015-10-22 14:45:50 +02:00
host_all = dict ( type = " bool " , default = " no " ) ,
2012-07-22 19:11:39 +02:00
state = dict ( default = " present " , choices = [ " absent " , " present " ] ) ,
priv = dict ( default = None ) ,
2015-05-18 22:33:46 +02:00
append_privs = dict ( default = False , type = ' bool ' ) ,
check_implicit_admin = dict ( default = False , type = ' bool ' ) ,
2015-02-20 18:30:27 +01:00
update_password = dict ( default = " always " , choices = [ " always " , " on_create " ] ) ,
2015-04-30 17:40:04 +02:00
config_file = dict ( default = " ~/.my.cnf " ) ,
2012-07-22 19:11:39 +02:00
)
)
2014-09-30 09:08:32 +02:00
login_user = module . params [ " login_user " ]
login_password = module . params [ " login_password " ]
2012-07-22 19:11:39 +02:00
user = module . params [ " user " ]
2015-10-22 14:45:50 +02:00
user_anonymous = module . params [ " user_anonymous " ]
2012-07-31 00:15:24 +02:00
password = module . params [ " password " ]
2015-06-23 22:57:18 +02:00
encrypted = module . boolean ( module . params [ " encrypted " ] )
2015-05-21 15:32:12 +02:00
host = module . params [ " host " ] . lower ( )
2015-10-22 14:45:50 +02:00
host_all = module . params [ " host_all " ]
2012-07-22 19:11:39 +02:00
state = module . params [ " state " ]
priv = module . params [ " priv " ]
2013-07-01 12:56:04 +02:00
check_implicit_admin = module . params [ ' check_implicit_admin ' ]
2014-09-30 09:08:32 +02:00
config_file = module . params [ ' config_file ' ]
2013-10-03 13:00:17 +02:00
append_privs = module . boolean ( module . params [ " append_privs " ] )
2015-02-20 18:30:27 +01:00
update_password = module . params [ ' update_password ' ]
2012-07-22 19:11:39 +02:00
2015-10-22 14:45:50 +02:00
if user_anonymous :
user = ' '
2015-05-08 19:40:26 +02:00
config_file = os . path . expanduser ( os . path . expandvars ( config_file ) )
2012-07-22 19:11:39 +02:00
if not mysqldb_found :
module . fail_json ( msg = " the python mysqldb module is required " )
if priv is not None :
try :
priv = privileges_unpack ( priv )
2014-11-25 10:46:09 +01:00
except Exception , e :
module . fail_json ( msg = " invalid privileges string: %s " % str ( e ) )
2012-07-12 01:00:55 +02:00
2013-07-01 12:56:04 +02:00
cursor = None
2012-07-12 01:00:55 +02:00
try :
2013-07-01 12:56:04 +02:00
if check_implicit_admin :
try :
2014-09-30 09:08:32 +02:00
cursor = connect ( module , ' root ' , ' ' , config_file )
2013-07-01 12:56:04 +02:00
except :
pass
if not cursor :
2014-09-30 09:08:32 +02:00
cursor = connect ( module , login_user , login_password , config_file )
2012-11-09 14:28:21 +01:00
except Exception , e :
2015-05-08 19:40:26 +02:00
module . fail_json ( msg = " unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials. Exception message: %s " % e )
2012-07-12 01:00:55 +02:00
if state == " present " :
2015-10-22 14:45:50 +02:00
if user_exists ( cursor , user , host , host_all ) :
2014-11-25 10:46:09 +01:00
try :
2015-02-20 18:30:27 +01:00
if update_password == ' always ' :
2015-12-14 17:40:20 +01:00
changed = user_mod ( cursor , user , host , host_all , password , encrypted , priv , append_privs )
2015-02-20 18:30:27 +01:00
else :
2015-12-14 17:40:20 +01:00
changed = user_mod ( cursor , user , host , host_all , None , encrypted , priv , append_privs )
2015-02-20 18:30:27 +01:00
2015-01-12 23:36:57 +01:00
except ( SQLParseError , InvalidPrivsError , MySQLdb . Error ) , e :
2014-12-04 22:35:07 +01:00
module . fail_json ( msg = str ( e ) )
2012-07-12 01:00:55 +02:00
else :
2012-07-31 00:15:24 +02:00
if password is None :
module . fail_json ( msg = " password parameter required when adding a user " )
2015-10-22 14:45:50 +02:00
if host_all :
module . fail_json ( msg = " host_all parameter cannot be used when adding a user " )
2014-11-25 10:46:09 +01:00
try :
2015-12-14 17:40:20 +01:00
changed = user_add ( cursor , user , host , host_all , password , encrypted , priv )
2015-01-12 23:36:57 +01:00
except ( SQLParseError , InvalidPrivsError , MySQLdb . Error ) , e :
2014-11-25 10:46:09 +01:00
module . fail_json ( msg = str ( e ) )
2012-07-12 01:00:55 +02:00
elif state == " absent " :
2015-10-22 14:45:50 +02:00
if user_exists ( cursor , user , host , host_all ) :
changed = user_delete ( cursor , user , host , host_all )
2012-07-12 01:00:55 +02:00
else :
changed = False
2012-07-22 19:11:39 +02:00
module . exit_json ( changed = changed , user = user )
2012-07-12 01:00:55 +02:00
2013-12-02 21:13:49 +01:00
# import module snippets
2013-12-02 21:11:23 +01:00
from ansible . module_utils . basic import *
2014-11-25 10:46:09 +01:00
from ansible . module_utils . database import *
if __name__ == ' __main__ ' :
main ( )