From 8466a9ad7be719bad3045d4350d2da3a01c7c70c Mon Sep 17 00:00:00 2001 From: Colin Hubert Date: Thu, 11 Sep 2014 17:45:43 -0400 Subject: [PATCH 1/2] User management in replicasets / sharding Commit history from squashing * handling attempting to create users on secondary / uninitialized replica sets * waiting for replica set initialization before creating users * updating read me for instructions on key file in replica sets * removing random semi-colons from line termination * switching from immediately to delayed * adding ability to force creation of admin users in mongos since auth = true is not a valid config flag * adding authentication setting for admin separate from user creation * instructions on how to update admin password and making the defaults more concise * adding new attributes for handling mongos and mongod nodes * adding fallback strategy for mongod replica set nodes * changing subscribed ruby_block * using a check if recipe is in run list instead of basing it off attributes * adding retry attempts to connection for when service is in the middle of restarting (mostly mongos) * adding authentication to mongos commands where required in user_management is used * giving a little more time to the connection retries as mongos can be slow on some machines * adding rescue connection failures to configuring shards since it can't connect while mongos is restarting * rubocop warnings / typo * adding instructions on mongos specific config * fixing typo in node configuration value --- README.md | 13 ++++++- attributes/users.rb | 27 +++++++++++-- libraries/mongodb.rb | 28 ++++++++++++- providers/user.rb | 80 ++++++++++++++++++++++++++++++++------ recipes/user_management.rb | 9 ++++- 5 files changed, 137 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 932b8ac..9261c34 100644 --- a/README.md +++ b/README.md @@ -252,13 +252,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: ## Original Author diff --git a/attributes/users.rb b/attributes/users.rb index 7bf8af8..30d87ec 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 d4e814c..d5a1963 100644 --- a/libraries/mongodb.rb +++ b/libraries/mongodb.rb @@ -237,7 +237,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 @@ -245,6 +248,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 @@ -268,7 +280,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 @@ -276,6 +291,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 ce3ef4e..c0e6a0a 100644 --- a/providers/user.rb +++ b/providers/user.rb @@ -23,9 +23,9 @@ def add_user(username, password, database, roles = []) # 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 @@ -33,8 +33,49 @@ def add_user(username, password, database, roles = []) # 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 @@ -46,7 +87,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) @@ -57,16 +105,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( - '127.0.0.1', - @new_resource.connection['config']['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 c390af5..8bf7a7f 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 From e447e34f395f61fa99911664712fcefc30860998 Mon Sep 17 00:00:00 2001 From: Grant Ridder Date: Thu, 8 Dec 2016 23:43:58 -0800 Subject: [PATCH 2/2] Fix rubocop offenses --- libraries/mongodb.rb | 4 ++-- providers/user.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/mongodb.rb b/libraries/mongodb.rb index d5a1963..372054d 100644 --- a/libraries/mongodb.rb +++ b/libraries/mongodb.rb @@ -239,7 +239,7 @@ def self.configure_shards(node, shard_nodes) begin connection = nil rescue_connection_failure do - connection = Mongo::Connection.new('localhost', node['mongodb']['config']['port'], :op_timeout => 5) + 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}") @@ -282,7 +282,7 @@ def self.configure_sharded_collections(node, sharded_collections) begin connection = nil rescue_connection_failure do - connection = Mongo::Connection.new('localhost', node['mongodb']['config']['port'], :op_timeout => 5) + 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}") diff --git a/providers/user.rb b/providers/user.rb index c0e6a0a..4f78cd6 100644 --- a/providers/user.rb +++ b/providers/user.rb @@ -34,7 +34,7 @@ def add_user(username, password, database, roles = []) # Create the user if they don't exist # Update the user if they already exist begin - db.add_user(username, password, false, :roles => roles) + 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'] @@ -51,7 +51,7 @@ def add_user(username, password, database, roles = []) 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) + 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 @@ -113,11 +113,11 @@ def retrieve_db(attempt = 0) Mongo::MongoClient.new( @new_resource.connection['host'], @new_resource.connection['port'], - :connect_timeout => 15, - :slave_ok => true + connect_timeout: 15, + slave_ok: true ) - rescue Mongo::ConnectionFailure => e - if(attempt) < @new_resource.connection['user_management']['connection']['retries'] + rescue Mongo::ConnectionFailure + 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)