diff --git a/README.md b/README.md index b4251491..9c811ac8 100644 --- a/README.md +++ b/README.md @@ -246,13 +246,24 @@ the `node['mongodb']['config']['auth']` attribute to true in the chef json. If the auth configuration is true, it will try to create the `node['mongodb']['admin']` user, or update them if they already exist. Before using on a new database, ensure you're overwriting -the `node['mongodb']['admin']['username']` and `node['mongodb']['admin']['password']` to +the `node['mongodb']['authentication']['username']` and `node['mongodb']['authentication']['password']` to something besides their default values. +To update the admin username or password after already having deployed the recipe with authentication +as required, simply change `node['mongodb']['admin']['password']` to the new password while keeping the +value of `node['mongodb']['authentication']['password']` the old value. After the recipe runs successfully, +be sure to change the latter variable to the new password so that subsequent attempts to authenticate will +work. + There's also a user resource which has the actions `:add`, `:modify` and `:delete`. If modify is used on a user that doesn't exist, it will be added. If add is used on a user that exists, it will be modified. +If using this recipe with replication and sharding, ensure that the `node['mongodb']['key_file_content']` +is set. All nodes must have the same key file in order for the replica set to initialize successfully +when authentication is required. For mongos instances, set `node['mongodb']['mongos_create_admin']` to +`true` to force the creation of the admin user on mongos instances. + # LICENSE and AUTHOR: Author:: Markus Korn diff --git a/attributes/users.rb b/attributes/users.rb index 7bf8af8e..30d87ecd 100644 --- a/attributes/users.rb +++ b/attributes/users.rb @@ -1,8 +1,29 @@ +# The username / password combination that is used +# to authenticate with the mongo database +default['mongodb']['authentication']['username'] = 'admin' +default['mongodb']['authentication']['password'] = 'admin' + default['mongodb']['admin'] = { - 'username' => 'admin', - 'password' => 'admin', - 'roles' => %w(userAdminAnyDatabase dbAdminAnyDatabase), + 'username' => default['mongodb']['authentication']['username'], + 'password' => default['mongodb']['authentication']['password'], + 'roles' => %w(userAdminAnyDatabase dbAdminAnyDatabase clusterAdmin), 'database' => 'admin' } default['mongodb']['users'] = [] + +# Force creation of admin user. auth=true is an invalid +# setting for mongos so this is needed to ensure the admin +# user is created +default['mongodb']['mongos_create_admin'] = false + +# For connecting to mongo on localhost, retries to make after +# connection failures and delay in seconds to retry +default['mongodb']['user_management']['connection']['retries'] = 2 +default['mongodb']['user_management']['connection']['delay'] = 2 + +# For mongod replicasets, the delay in seconds and number +# of times to retry adding a user. Used to handle election +# of primary not being completed immediately +default['mongodb']['mongod_create_user']['retries'] = 2 +default['mongodb']['mongod_create_user']['delay'] = 10 diff --git a/libraries/mongodb.rb b/libraries/mongodb.rb index 3b1c8dcd..be6860d4 100644 --- a/libraries/mongodb.rb +++ b/libraries/mongodb.rb @@ -236,7 +236,10 @@ def self.configure_shards(node, shard_nodes) Chef::Log.info(shard_members.inspect) begin - connection = Mongo::Connection.new('localhost', node['mongodb']['config']['port'], :op_timeout => 5) + connection = nil + rescue_connection_failure do + connection = Mongo::Connection.new('localhost', node['mongodb']['config']['port'], :op_timeout => 5) + end rescue => e Chef::Log.warn("Could not connect to database: 'localhost:#{node['mongodb']['config']['port']}', reason #{e}") return @@ -244,6 +247,15 @@ def self.configure_shards(node, shard_nodes) admin = connection['admin'] + # If we require authentication on mongos / mongod, need to authenticate to run these commands + if node.recipe?('mongodb::user_management') + begin + admin.authenticate(node['mongodb']['authentication']['username'], node['mongodb']['authentication']['password']) + rescue Mongo::AuthenticationError => e + Chef::Log.warn("Unable to authenticate with database to add shards to mongos node: #{e}") + end + end + shard_members.each do |shard| cmd = BSON::OrderedHash.new cmd['addShard'] = shard @@ -267,7 +279,10 @@ def self.configure_sharded_collections(node, sharded_collections) require 'mongo' begin - connection = Mongo::Connection.new('localhost', node['mongodb']['config']['port'], :op_timeout => 5) + connection = nil + rescue_connection_failure do + connection = Mongo::Connection.new('localhost', node['mongodb']['config']['port'], :op_timeout => 5) + end rescue => e Chef::Log.warn("Could not connect to database: 'localhost:#{node['mongodb']['config']['port']}', reason #{e}") return @@ -275,6 +290,15 @@ def self.configure_sharded_collections(node, sharded_collections) admin = connection['admin'] + # If we require authentication on mongos / mongod, need to authenticate to run these commands + if node.recipe?('mongodb::user_management') + begin + admin.authenticate(node['mongodb']['authentication']['username'], node['mongodb']['authentication']['password']) + rescue Mongo::AuthenticationError => e + Chef::Log.warn("Unable to authenticate with database to configure databased on mongos node: #{e}") + end + end + databases = sharded_collections.keys.map { |x| x.split('.').first }.uniq Chef::Log.info("enable sharding for these databases: '#{databases.inspect}'") diff --git a/providers/user.rb b/providers/user.rb index bf3316e2..da5fb770 100644 --- a/providers/user.rb +++ b/providers/user.rb @@ -13,17 +13,17 @@ def add_user(username, password, roles = [], database) # Check if user is admin / admin, and warn that this should # be overridden to unique values if username == 'admin' && password == 'admin' - Chef::Log.warn('Default username / password detected for admin user'); - Chef::Log.warn('These should be overridden to different, unique values'); + Chef::Log.warn('Default username / password detected for admin user') + Chef::Log.warn('These should be overridden to different, unique values') end # If authentication is required on database # must authenticate as a userAdmin after an admin user has been created # this will fail on the first attempt, but user will still be created # because of the localhost exception - if node['mongodb']['config']['auth'] == true + if (@new_resource.connection['config']['auth'] == true) || (@new_resource.connection['mongos_create_admin'] == true) begin - admin.authenticate(@new_resource.connection['admin']['username'], @new_resource.connection['admin']['password']) + admin.authenticate(@new_resource.connection['authentication']['username'], @new_resource.connection['authentication']['password']) rescue Mongo::AuthenticationError => e Chef::Log.warn("Unable to authenticate as admin user. If this is a fresh install, ignore warning: #{e}") end @@ -31,8 +31,49 @@ def add_user(username, password, roles = [], database) # Create the user if they don't exist # Update the user if they already exist - db.add_user(username, password, false, :roles => roles) - Chef::Log.info("Created or updated user #{username} on #{database}") + begin + db.add_user(username, password, false, :roles => roles) + Chef::Log.info("Created or updated user #{username} on #{database}") + rescue Mongo::ConnectionFailure => e + if @new_resource.connection['is_replicaset'] + # Node is part of a replicaset and may not be initialized yet, going to retry if set to + i = 0 + while i < @new_resource.connection['mongod_create_user']['retries'] + begin + # See if we can get the current replicaset status back from the node + cmd = BSON::OrderedHash.new + cmd['replSetGetStatus'] = 1 + result = admin.command(cmd) + # Check if the current node in the replicaset status has an info message set (at this point, most likely + # a message about the election) + has_info_message = result['members'].select { |a| a['self'] && a.key?('infoMessage') }.count > 0 + if result['myState'] == 1 + # This node is a primary node, try to add the user + db.add_user(username, password, false, :roles => roles) + Chef::Log.info("Created or updated user #{username} on #{database} of primary replicaset node") + break + elsif result['myState'] == 2 && has_info_message == true + # This node is secondary but may be in the process of an election, retry + Chef::Log.info("Unable to add user to secondary, election may be in progress, retrying in #{@new_resource.connection['mongod_create_user']['delay']} seconds...") + elsif result['myState'] == 2 && has_info_message == false + # This node is secondary and not in the process of an election, bail out + Chef::Log.info('Current node appears to be a secondary node in replicaset, could not detect election in progress, not adding user') + break + end + rescue Mongo::ConnectionFailure => e + # Unable to connect to the node, may not be initialized yet + Chef::Log.warn("Unable to add user, retrying in #{@new_resource.connection['mongod_create_user']['delay']} second(s)... #{e}") + rescue Mongo::OperationFailure => e + # Unable to make either add call or replicaset call on node, should retry in case it was in the middle of being initialized + Chef::Log.warn("Unable to add user, retrying in #{@new_resource.connection['mongod_create_user']['delay']} second(s)... #{e}") + end + i += 1 + sleep(@new_resource.connection['mongod_create_user']['delay']) + end + else + Chef::Log.fatal("Unable to add user: #{e}") + end + end end # Drop a user from the database specified @@ -44,7 +85,14 @@ def delete_user(username, database) admin = connection.db('admin') db = connection.db(database) - admin.authenticate(@new_resource.connection['admin']['username'], @new_resource.connection['admin']['password']) + # Only try to authenticate with db if required + if (@new_resource.connection['config']['auth'] == true) || (@new_resource.connection['mongos_create_admin'] == true) + begin + admin.authenticate(@new_resource.connection['authentication']['username'], @new_resource.connection['authentication']['password']) + rescue Mongo::AuthenticationError => e + Chef::Log.warn("Unable to authenticate as admin user: #{e}") + end + end if user_exists?(username, connection) db.remove_user(username) @@ -55,16 +103,24 @@ def delete_user(username, database) end # Get the MongoClient connection -def retrieve_db +def retrieve_db(attempt = 0) require 'rubygems' require 'mongo' - Mongo::MongoClient.new( - @new_resource.connection['host'], - @new_resource.connection['port'], - :connect_timeout => 15, - :slave_ok => true - ) + begin + Mongo::MongoClient.new( + @new_resource.connection['host'], + @new_resource.connection['port'], + :connect_timeout => 15, + :slave_ok => true + ) + rescue Mongo::ConnectionFailure => e + if(attempt) < @new_resource.connection['user_management']['connection']['retries'] + Chef::Log.warn("Unable to connect to MongoDB instance, retrying in #{@new_resource.connection['user_management']['connection']['delay']} second(s)...") + sleep(@new_resource.connection['user_management']['connection']['delay']) + retrieve_db(attempt + 1) + end + end end action :add do diff --git a/recipes/user_management.rb b/recipes/user_management.rb index 5b956f59..bbeafbe0 100644 --- a/recipes/user_management.rb +++ b/recipes/user_management.rb @@ -5,7 +5,7 @@ # If authentication is required, # add the admin to the users array for adding/updating -users << admin if node['mongodb']['config']['auth'] == true +users << admin if (node['mongodb']['config']['auth'] == true) || (node['mongodb']['mongos_create_admin'] == true) users.concat(node['mongodb']['users']) @@ -16,6 +16,11 @@ roles user['roles'] database user['database'] connection node['mongodb'] - action :add + if node.recipe?('mongodb::mongos') || node.recipe?('mongodb::replicaset') + # If it's a replicaset or mongos, don't make any users until the end + action :nothing + subscribes :add, 'ruby_block[config_replicaset]', :delayed + subscribes :add, 'ruby_block[config_sharding]', :delayed + end end end