Skip to content

Commit

Permalink
Merge pull request #13 from sous-chefs/pr/344
Browse files Browse the repository at this point in the history
User management in replicasets / sharding
  • Loading branch information
shortdudey123 authored Dec 9, 2016
2 parents 025c0c4 + e447e34 commit 11749b4
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 20 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 24 additions & 3 deletions attributes/users.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 26 additions & 2 deletions libraries/mongodb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,26 @@ 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
end

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
Expand All @@ -268,14 +280,26 @@ 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
end

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}'")

Expand Down
80 changes: 68 additions & 12 deletions providers/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,59 @@ 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
end

# 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
Expand All @@ -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)
Expand All @@ -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
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
Expand Down
9 changes: 7 additions & 2 deletions recipes/user_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'])

Expand All @@ -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

0 comments on commit 11749b4

Please sign in to comment.