2013-01-31 15:48:58 +01:00
#!/usr/bin/python
# (c) 2012, Elliott Foster <elliott@fourkitchens.com>
# Sponsored by Four Kitchens http://fourkitchens.com.
2014-04-06 00:33:22 +02:00
# (c) 2014, Epic Games, Inc.
2013-01-31 15:48:58 +01:00
#
# 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/>.
DOCUMENTATION = '''
- - -
module : mongodb_user
short_description : Adds or removes a user from a MongoDB database .
description :
- Adds or removes a user from a MongoDB database .
2013-02-09 20:43:17 +01:00
version_added : " 1.1 "
2013-01-31 15:48:58 +01:00
options :
login_user :
description :
- The username used to authenticate with
required : false
default : null
login_password :
description :
- The password used to authenticate with
required : false
default : null
login_host :
description :
- The host running the database
required : false
default : localhost
2013-02-09 20:43:17 +01:00
login_port :
2013-01-31 15:48:58 +01:00
description :
- The port to connect to
required : false
default : 27017
2015-09-03 12:20:54 +02:00
login_database :
2015-09-03 21:02:36 +02:00
version_added : " 2.0 "
2015-09-03 12:20:54 +02:00
description :
- The database where login credentials are stored
required : false
default : null
2014-04-06 00:33:22 +02:00
replica_set :
2014-04-06 01:31:33 +02:00
version_added : " 1.6 "
2014-04-06 00:33:22 +02:00
description :
- Replica set to connect to ( automatically connects to primary for writes )
required : false
default : null
2013-01-31 15:48:58 +01:00
database :
description :
- The name of the database to add / remove the user from
required : true
2015-02-16 15:59:24 +01:00
name :
2013-01-31 15:48:58 +01:00
description :
- The name of the user to add or remove
required : true
default : null
2015-02-16 15:59:24 +01:00
aliases : [ ' user ' ]
2013-01-31 15:48:58 +01:00
password :
description :
- The password to use for the user
required : false
default : null
2014-09-27 08:23:39 +02:00
ssl :
2014-09-30 00:45:55 +02:00
version_added : " 1.8 "
2014-09-27 08:23:39 +02:00
description :
- Whether to use an SSL connection when connecting to the database
default : False
2016-06-13 19:36:57 +02:00
ssl_cert_reqs :
version_added : " 2.2 "
description :
- Specifies whether a certificate is required from the other side of the connection , and whether it will be validated if provided .
required : false
default : " CERT_REQUIRED "
choices : [ " CERT_REQUIRED " , " CERT_OPTIONAL " , " CERT_NONE " ]
2013-08-12 22:03:31 +02:00
roles :
version_added : " 1.3 "
description :
2016-03-02 11:04:28 +01:00
- " The database user roles valid values could either be one or more of the following strings: ' read ' , ' readWrite ' , ' dbAdmin ' , ' userAdmin ' , ' clusterAdmin ' , ' readAnyDatabase ' , ' readWriteAnyDatabase ' , ' userAdminAnyDatabase ' , ' dbAdminAnyDatabase ' "
- " Or the following dictionary ' { db: DATABASE_NAME, role: ROLE_NAME } ' . "
- " This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required. "
2013-08-12 22:03:31 +02:00
required : false
default : " readWrite "
2013-01-31 15:48:58 +01:00
state :
state :
description :
- The database user state
required : false
default : present
choices : [ " present " , " absent " ]
2015-04-09 20:22:24 +02:00
update_password :
required : false
default : always
choices : [ ' always ' , ' on_create ' ]
version_added : " 2.1 "
description :
- C ( always ) will update passwords if they differ . C ( on_create ) will only set the password for newly created users .
2013-02-09 20:43:17 +01:00
notes :
- Requires the pymongo Python package on the remote host , version 2.4 .2 + . This
can be installed using pip or the OS package manager . @see http : / / api . mongodb . org / python / current / installation . html
2013-01-31 15:48:58 +01:00
requirements : [ " pymongo " ]
2015-06-16 20:32:39 +02:00
author : " Elliott Foster (@elliotttf) "
2013-01-31 15:48:58 +01:00
'''
2013-06-14 11:53:43 +02:00
EXAMPLES = '''
# Create 'burgers' database user with name 'bob' and password '12345'.
- mongodb_user : database = burgers name = bob password = 12345 state = present
2014-09-27 08:23:39 +02:00
# Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly)
- mongodb_user : database = burgers name = bob password = 12345 state = present ssl = True
2013-06-14 11:53:43 +02:00
# Delete 'burgers' database user with name 'bob'.
- mongodb_user : database = burgers name = bob state = absent
2013-08-12 22:03:31 +02:00
2013-08-13 07:23:58 +02:00
# Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style)
2013-08-12 22:03:31 +02:00
- mongodb_user : database = burgers name = ben password = 12345 roles = ' read ' state = present
- mongodb_user : database = burgers name = jim password = 12345 roles = ' readWrite,dbAdmin,userAdmin ' state = present
- mongodb_user : database = burgers name = joe password = 12345 roles = ' readWriteAnyDatabase ' state = present
2014-04-06 00:33:22 +02:00
# add a user to database in a replica set, the primary server is automatically discovered and written to
2015-12-01 18:16:29 +01:00
- mongodb_user : database = burgers name = bob replica_set = belcher password = 12345 roles = ' readWriteAnyDatabase ' state = present
2016-03-02 11:04:28 +01:00
# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is usefull for oplog access (MONGO_OPLOG_URL).
# please notice the credentials must be added to the 'admin' database because the 'local' database is not syncronized and can't receive user credentials
# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin"
# This syntax requires mongodb 2.6+ and pymongo 2.5+
- mongodb_user :
login_user : root
login_password : root_password
database : admin
user : oplog_reader
password : oplog_reader_password
state : present
replica_set : belcher
roles :
- { db : " local " , role : " read " }
2013-06-14 11:53:43 +02:00
'''
2016-06-13 19:36:57 +02:00
import ssl as ssl_lib
2013-01-31 15:48:58 +01:00
import ConfigParser
2014-04-06 00:33:22 +02:00
from distutils . version import LooseVersion
2013-01-31 15:48:58 +01:00
try :
from pymongo . errors import ConnectionFailure
from pymongo . errors import OperationFailure
2014-04-06 00:33:22 +02:00
from pymongo import version as PyMongoVersion
2013-06-10 22:43:50 +02:00
from pymongo import MongoClient
2013-01-31 15:48:58 +01:00
except ImportError :
2013-06-10 22:43:50 +02:00
try : # for older PyMongo 2.2
from pymongo import Connection as MongoClient
except ImportError :
pymongo_found = False
else :
pymongo_found = True
2013-01-31 15:48:58 +01:00
else :
pymongo_found = True
# =========================================
# MongoDB module specific support methods.
#
2016-05-16 17:25:52 +02:00
def check_compatibility ( module , client ) :
srv_info = client . server_info ( )
if LooseVersion ( srv_info [ ' version ' ] ) > = LooseVersion ( ' 3.2 ' ) and LooseVersion ( PyMongoVersion ) < = LooseVersion ( ' 3.2 ' ) :
module . fail_json ( msg = ' (Note: you must use pymongo 3.2+ with MongoDB >= 3.2) ' )
elif LooseVersion ( srv_info [ ' version ' ] ) > = LooseVersion ( ' 3.0 ' ) and LooseVersion ( PyMongoVersion ) < = LooseVersion ( ' 2.8 ' ) :
module . fail_json ( msg = ' (Note: you must use pymongo 2.8+ with MongoDB 3.0) ' )
elif LooseVersion ( srv_info [ ' version ' ] ) > = LooseVersion ( ' 2.6 ' ) and LooseVersion ( PyMongoVersion ) < = LooseVersion ( ' 2.7 ' ) :
module . fail_json ( msg = ' (Note: you must use pymongo 2.7+ with MongoDB 2.6) ' )
elif LooseVersion ( PyMongoVersion ) < = LooseVersion ( ' 2.5 ' ) :
module . fail_json ( msg = ' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param) ' )
2016-02-25 11:01:34 +01:00
def user_find ( client , user , db_name ) :
2015-04-09 20:03:14 +02:00
for mongo_user in client [ " admin " ] . system . users . find ( ) :
2016-02-25 11:01:34 +01:00
if mongo_user [ ' user ' ] == user and mongo_user [ ' db ' ] == db_name :
2015-04-09 20:03:14 +02:00
return mongo_user
return False
2013-08-13 07:23:58 +02:00
def user_add ( module , client , db_name , user , password , roles ) :
2016-02-06 02:10:44 +01:00
#pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated
2015-04-09 20:03:14 +02:00
#without reproducing a lot of the logic in database.py of pymongo
2014-04-06 00:33:22 +02:00
db = client [ db_name ]
2016-02-25 11:01:34 +01:00
2014-04-06 00:33:22 +02:00
if roles is None :
db . add_user ( user , password , False )
else :
2016-07-21 08:23:05 +02:00
db . add_user ( user , password , None , roles = roles )
2013-01-31 15:48:58 +01:00
2015-04-09 20:03:14 +02:00
def user_remove ( module , client , db_name , user ) :
2016-02-25 11:01:34 +01:00
exists = user_find ( client , user , db_name )
2015-04-09 20:03:14 +02:00
if exists :
2016-02-28 07:25:45 +01:00
if module . check_mode :
module . exit_json ( changed = True , user = user )
2015-04-09 20:03:14 +02:00
db = client [ db_name ]
db . remove_user ( user )
else :
module . exit_json ( changed = False , user = user )
2013-01-31 15:48:58 +01:00
def load_mongocnf ( ) :
config = ConfigParser . RawConfigParser ( )
mongocnf = os . path . expanduser ( ' ~/.mongodb.cnf ' )
try :
config . readfp ( open ( mongocnf ) )
creds = dict (
user = config . get ( ' client ' , ' user ' ) ,
password = config . get ( ' client ' , ' pass ' )
)
except ( ConfigParser . NoOptionError , IOError ) :
return False
return creds
2016-03-16 20:59:24 +01:00
def check_if_roles_changed ( uinfo , roles , db_name ) :
2016-03-16 21:07:58 +01:00
# We must be aware of users which can read the oplog on a replicaset
# Such users must have access to the local DB, but since this DB does not store users credentials
2016-03-16 20:59:24 +01:00
# and is not synchronized among replica sets, the user must be stored on the admin db
2016-03-16 21:07:58 +01:00
# Therefore their structure is the following :
2016-03-16 20:59:24 +01:00
# {
# "_id" : "admin.oplog_reader",
# "user" : "oplog_reader",
2016-03-16 21:07:58 +01:00
# "db" : "admin", # <-- admin DB
2016-03-16 20:59:24 +01:00
# "roles" : [
# {
# "role" : "read",
2016-03-16 21:07:58 +01:00
# "db" : "local" # <-- local DB
2016-03-16 20:59:24 +01:00
# }
# ]
# }
def make_sure_roles_are_a_list_of_dict ( roles , db_name ) :
output = list ( )
for role in roles :
if isinstance ( role , basestring ) :
new_role = { " role " : role , " db " : db_name }
output . append ( new_role )
else :
output . append ( role )
return output
roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict ( roles , db_name )
uinfo_roles = uinfo . get ( ' roles ' , [ ] )
if sorted ( roles_as_list_of_dict ) == sorted ( uinfo_roles ) :
return False
return True
2013-01-31 15:48:58 +01:00
# =========================================
# Module execution.
#
def main ( ) :
module = AnsibleModule (
argument_spec = dict (
login_user = dict ( default = None ) ,
login_password = dict ( default = None ) ,
login_host = dict ( default = ' localhost ' ) ,
login_port = dict ( default = ' 27017 ' ) ,
2015-09-03 12:20:54 +02:00
login_database = dict ( default = None ) ,
2014-04-06 00:33:22 +02:00
replica_set = dict ( default = None ) ,
2013-01-31 15:48:58 +01:00
database = dict ( required = True , aliases = [ ' db ' ] ) ,
2015-02-16 15:59:24 +01:00
name = dict ( required = True , aliases = [ ' user ' ] ) ,
2013-01-31 15:48:58 +01:00
password = dict ( aliases = [ ' pass ' ] ) ,
2016-03-19 23:59:34 +01:00
ssl = dict ( default = False , type = ' bool ' ) ,
2013-08-13 07:23:58 +02:00
roles = dict ( default = None , type = ' list ' ) ,
2013-01-31 15:48:58 +01:00
state = dict ( default = ' present ' , choices = [ ' absent ' , ' present ' ] ) ,
2015-04-09 20:22:24 +02:00
update_password = dict ( default = " always " , choices = [ " always " , " on_create " ] ) ,
2016-06-13 19:36:57 +02:00
ssl_cert_reqs = dict ( default = ' CERT_REQUIRED ' , choices = [ ' CERT_NONE ' , ' CERT_OPTIONAL ' , ' CERT_REQUIRED ' ] ) ,
2016-02-28 07:25:45 +01:00
) ,
supports_check_mode = True
2013-01-31 15:48:58 +01:00
)
if not pymongo_found :
module . fail_json ( msg = ' the python pymongo module is required ' )
login_user = module . params [ ' login_user ' ]
login_password = module . params [ ' login_password ' ]
login_host = module . params [ ' login_host ' ]
login_port = module . params [ ' login_port ' ]
2015-09-03 12:20:54 +02:00
login_database = module . params [ ' login_database ' ]
2016-03-02 11:04:28 +01:00
2014-04-06 00:33:22 +02:00
replica_set = module . params [ ' replica_set ' ]
2013-01-31 15:48:58 +01:00
db_name = module . params [ ' database ' ]
2015-02-16 15:59:24 +01:00
user = module . params [ ' name ' ]
2013-01-31 15:48:58 +01:00
password = module . params [ ' password ' ]
2014-09-27 08:23:39 +02:00
ssl = module . params [ ' ssl ' ]
2016-07-20 07:58:35 +02:00
ssl_cert_reqs = None
if ssl :
ssl_cert_reqs = getattr ( ssl_lib , module . params [ ' ssl_cert_reqs ' ] )
2013-08-12 22:03:31 +02:00
roles = module . params [ ' roles ' ]
2013-01-31 15:48:58 +01:00
state = module . params [ ' state ' ]
2015-04-09 20:22:24 +02:00
update_password = module . params [ ' update_password ' ]
2013-01-31 15:48:58 +01:00
try :
2015-06-30 20:46:45 +02:00
if replica_set :
2016-06-13 19:36:57 +02:00
client = MongoClient ( login_host , int ( login_port ) ,
replicaset = replica_set , ssl = ssl ,
ssl_cert_reqs = ssl_cert_reqs )
2015-06-30 20:46:45 +02:00
else :
2016-06-13 19:36:57 +02:00
client = MongoClient ( login_host , int ( login_port ) , ssl = ssl ,
ssl_cert_reqs = ssl_cert_reqs )
2014-04-06 00:33:22 +02:00
if login_user is None and login_password is None :
mongocnf_creds = load_mongocnf ( )
if mongocnf_creds is not False :
login_user = mongocnf_creds [ ' user ' ]
login_password = mongocnf_creds [ ' password ' ]
2015-03-31 22:43:40 +02:00
elif login_password is None or login_user is None :
2014-04-06 00:33:22 +02:00
module . fail_json ( msg = ' when supplying login arguments, both login_user and login_password must be provided ' )
if login_user is not None and login_password is not None :
2015-09-03 12:20:54 +02:00
client . admin . authenticate ( login_user , login_password , source = login_database )
2015-04-09 20:03:14 +02:00
elif LooseVersion ( PyMongoVersion ) > = LooseVersion ( ' 3.0 ' ) :
if db_name != " admin " :
module . fail_json ( msg = ' The localhost login exception only allows the first admin account to be created ' )
#else: this has to be the first admin user added
2014-01-13 14:37:26 +01:00
2014-04-06 00:33:22 +02:00
except ConnectionFailure , e :
module . fail_json ( msg = ' unable to connect to database: %s ' % str ( e ) )
2013-01-31 15:48:58 +01:00
2016-05-16 17:25:52 +02:00
check_compatibility ( module , client )
2013-01-31 15:48:58 +01:00
if state == ' present ' :
2015-04-09 20:22:24 +02:00
if password is None and update_password == ' always ' :
module . fail_json ( msg = ' password parameter required when adding a user unless update_password is set to on_create ' )
2016-07-21 08:23:05 +02:00
try :
uinfo = user_find ( client , user , db_name )
if update_password != ' always ' and uinfo :
password = None
if not check_if_roles_changed ( uinfo , roles , db_name ) :
module . exit_json ( changed = False , user = user )
2014-04-06 00:33:22 +02:00
2016-07-21 08:23:05 +02:00
if module . check_mode :
module . exit_json ( changed = True , user = user )
2016-02-28 07:25:45 +01:00
2014-04-06 00:33:22 +02:00
user_add ( module , client , db_name , user , password , roles )
except OperationFailure , e :
module . fail_json ( msg = ' Unable to add or update user: %s ' % str ( e ) )
2016-02-25 11:01:34 +01:00
# Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848
#newuinfo = user_find(client, user, db_name)
#if uinfo['role'] == newuinfo['role'] and CheckPasswordHere:
# module.exit_json(changed=False, user=user)
2013-01-31 15:48:58 +01:00
elif state == ' absent ' :
2014-04-06 00:33:22 +02:00
try :
2015-04-09 20:03:14 +02:00
user_remove ( module , client , db_name , user )
2014-04-06 00:33:22 +02:00
except OperationFailure , e :
module . fail_json ( msg = ' Unable to remove user: %s ' % str ( e ) )
2013-01-31 15:48:58 +01:00
module . exit_json ( changed = True , user = user )
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 *
2013-01-31 15:48:58 +01:00
main ( )