Skip to content

Commit

Permalink
(MODULES-1947) Improve support for MongoDB authentication and replicaset
Browse files Browse the repository at this point in the history
management.
Adds the ability to create an 'administration' MongoDB user account,
which then gets stored in a mongorc.js file which enables Puppet to
connect to MongoDB without credentials.
Admin username and password can be over-ridden via 'admin_username' and
'admin_password' parameters.
Replica set configuration can be completed as part of mongodb::server
class by either providing a list of members using
'replset_members', or a full replica set config hash using
'replset_config'. Alternatively, mongodb::replset can be used to
configure replicaset seperately.
Any attempt to manage mongodb_db or mongodb_user resources on non-master
replicaset members will generate a warning instead of failing.
  • Loading branch information
fatmcgav committed Oct 23, 2015
1 parent 9262b8a commit 56c0014
Show file tree
Hide file tree
Showing 21 changed files with 812 additions and 152 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -7,3 +7,5 @@ spec/fixtures/
coverage/
.idea/
*.iml
.ruby-*
log/
36 changes: 36 additions & 0 deletions README.md
Expand Up @@ -398,6 +398,22 @@ Use this setting to enable shard server mode for mongod.
Use this setting to configure replication with replica sets. Specify a replica
set name as an argument to this set. All hosts must have the same set name.

#####`replset_members`
An array of member hosts for the replica set.
Mutually exclusive with `replset_config` param.

#####`replset_config`
A hash that is used to configure the replica set.
Mutually exclusive with `replset_members` param.

```puppet
class mongodb::server {
replset => 'rsmain',
replset_config => { 'rsmain' => { ensure => present, members => ['host1:27017', 'host2:27017', 'host3:27017'] } }
}
```

#####`rest`
Set to true to enable a simple REST interface. Default: false

Expand Down Expand Up @@ -473,6 +489,23 @@ You should not set this for MongoDB versions < 3.x
#####`restart`
Specifies whether the service should be restarted on config changes. Default: 'true'

#####`create_admin`
Allows to create admin user for admin database.
Redefine these parameters if needed:

#####`admin_username`
Administrator user name

#####`admin_password`
Administrator user password

#####`admin_roles`
Administrator user roles

#####`store_creds`
Store admin credentials in mongorc.js file. Uses with `create_admin` parameter


####Class: mongodb::mongos
class. This class should only be used if you want to implement sharding within
your mongodb deployment.
Expand Down Expand Up @@ -565,6 +598,9 @@ The maximum amount of two second tries to wait MongoDB startup. Default: 10
#### Provider: mongodb_user
'mongodb_user' can be used to create and manage users within MongoDB database.

*Note:* if replica set is enabled, replica initialization has to come before
any user operations.

```puppet
mongodb_user { testuser:
name => 'testuser',
Expand Down
55 changes: 48 additions & 7 deletions lib/puppet/provider/mongodb.rb
@@ -1,4 +1,5 @@
require 'yaml'
require 'json'
class Puppet::Provider::Mongodb < Puppet::Provider

# Without initvars commands won't work.
Expand Down Expand Up @@ -74,9 +75,44 @@ def self.get_conn_string
"#{ip_real}:#{port_real}"
end

def self.db_ismaster
cmd_ismaster = 'printjson(db.isMaster())'
if mongorc_file
cmd_ismaster = mongorc_file + cmd_ismaster
end
out = mongo(['admin', '--quiet', '--host', get_conn_string, '--eval', cmd_ismaster])
out.gsub!(/ObjectId\(([^)]*)\)/, '\1')
out.gsub!(/ISODate\((.+?)\)/, '\1 ')
out.gsub!(/^Error\:.+/, '')
res = JSON.parse out

return res['ismaster']
end

def db_ismaster
self.class.db_ismaster
end

def self.auth_enabled
auth_enabled = false
file = get_mongod_conf_file
config = YAML.load_file(file)
if config.kind_of?(Hash)
auth_enabled = config['security.authorization']
else # It has to be a key-value store
config = {}
File.readlines(file).collect do |line|
k,v = line.split('=')
config[k.rstrip] = v.lstrip.chomp if k and v
end
auth_enabled = config['auth']
end
return auth_enabled
end

# Mongo Command Wrapper
def self.mongo_eval(cmd, db = 'admin')
retry_count = 10
def self.mongo_eval(cmd, db = 'admin', retries = 10, host = nil)
retry_count = retries
retry_sleep = 3
if mongorc_file
cmd = mongorc_file + cmd
Expand All @@ -85,25 +121,30 @@ def self.mongo_eval(cmd, db = 'admin')
out = nil
retry_count.times do |n|
begin
out = mongo([db, '--quiet', '--host', get_conn_string, '--eval', cmd])
if host
out = mongo([db, '--quiet', '--host', host, '--eval', cmd])
else
out = mongo([db, '--quiet', '--host', get_conn_string, '--eval', cmd])
end
rescue => e
debug "Request failed: '#{e.message}' Retry: '#{n}'"
Puppet.debug "Request failed: '#{e.message}' Retry: '#{n}'"
sleep retry_sleep
next
end
break
end

if !out
fail "Could not evalute MongoDB shell command: #{cmd}"
raise Puppet::ExecutionFailure, "Could not evalute MongoDB shell command: #{cmd}"
end

out.gsub!(/ObjectId\(([^)]*)\)/, '\1')
out.gsub!(/^Error\:.+/, '')
out
end

def mongo_eval(cmd, db = 'admin')
self.class.mongo_eval(cmd, db)
def mongo_eval(cmd, db = 'admin', retries = 10, host = nil)
self.class.mongo_eval(cmd, db, retries, host)
end

# Mongo Version checker
Expand Down
12 changes: 10 additions & 2 deletions lib/puppet/provider/mongodb_database/mongodb.rb
Expand Up @@ -26,11 +26,19 @@ def self.prefetch(resources)
end

def create
mongo_eval('db.dummyData.insert({"created_by_puppet": 1})', @resource[:name])
if db_ismaster
mongo_eval('db.dummyData.insert({"created_by_puppet": 1})', @resource[:name])
else
Puppet.warning 'Database creation is available only from master host'
end
end

def destroy
mongo_eval('db.dropDatabase()', @resource[:name])
if db_ismaster
mongo_eval('db.dropDatabase()', @resource[:name])
else
Puppet.warning 'Database removal is available only from master host'
end
end

def exists?
Expand Down
103 changes: 60 additions & 43 deletions lib/puppet/provider/mongodb_replset/mongo.rb
Expand Up @@ -15,8 +15,6 @@
false
end

commands :mongo => 'mongo'

mk_resource_methods

def initialize(resource={})
Expand Down Expand Up @@ -67,31 +65,43 @@ def flush
private

def db_ismaster(host)
mongo_command("db.isMaster()", host)
mongo_command('db.isMaster()', host)
end

def rs_initiate(conf, master)
return mongo_command("rs.initiate(#{conf})", master)
if auth_enabled
return mongo_command("rs.initiate(#{conf})", initialize_host)
else
return mongo_command("rs.initiate(#{conf})", master)
end
end

def rs_status(host)
mongo_command("rs.status()", host)
mongo_command('rs.status()', host)
end

def rs_add(host, master)
mongo_command("rs.add(\"#{host}\")", master)
mongo_command("rs.add('#{host}')", master)
end

def rs_remove(host, master)
mongo_command("rs.remove(\"#{host}\")", master)
mongo_command("rs.remove('#{host}')", master)
end

def rs_arbiter
@resource[:arbiter]
end

def rs_add_arbiter(host, master)
mongo_command("rs.addArb(\"#{host}\")", master)
mongo_command("rs.addArb('#{host}')", master)
end

def auth_enabled
self.class.auth_enabled
end

def initialize_host
@resource[:initialize_host]
end

def master_host(hosts)
Expand All @@ -104,17 +114,7 @@ def master_host(hosts)
false
end

def self.get_mongod_conf_file
if File.exists? '/etc/mongod.conf'
file = '/etc/mongod.conf'
else
file = '/etc/mongodb.conf'
end
file
end

def self.get_replset_properties

conn_string = get_conn_string
output = mongo_command('rs.conf()', conn_string)
if output['members']
Expand All @@ -135,31 +135,37 @@ def self.get_replset_properties
end

def alive_members(hosts)
alive = []
hosts.select do |host|
begin
Puppet.debug "Checking replicaset member #{host} ..."
status = rs_status(host)
if status.has_key?('errmsg') and status['errmsg'] == 'not running with --replSet'
raise Puppet::Error, "Can't configure replicaset #{self.name}, host #{host} is not supposed to be part of a replicaset."
end

if auth_enabled and status.has_key?('errmsg') and (status['errmsg'].include? "unauthorized" or status['errmsg'].include? "not authorized")
Puppet.warning "Host #{host} is available, but you are unauthorized because of authentication is enabled: #{auth_enabled}"
alive.push(host)
end

if status.has_key?('set')
if status['set'] != self.name
raise Puppet::Error, "Can't configure replicaset #{self.name}, host #{host} is already part of another replicaset."
end

# This node is alive and supposed to be a member of our set
Puppet.debug "Host #{host} is available for replset #{status['set']}"
true
alive.push(host)
elsif status.has_key?('info')
Puppet.debug "Host #{host} is alive but unconfigured: #{status['info']}"
true
alive.push(host)
end
rescue Puppet::ExecutionFailure
Puppet.warning "Can't connect to replicaset member #{host}."

false
end
end
return alive
end

def set_members
Expand All @@ -176,14 +182,14 @@ def set_members
# Find the alive members so we don't try to add dead members to the replset
alive_hosts = alive_members(@property_flush[:members])
dead_hosts = @property_flush[:members] - alive_hosts
raise Puppet::Error, "Can't connect to any member of replicaset #{self.name}." if alive_hosts.empty?
Puppet.debug "Alive members: #{alive_hosts.inspect}"
Puppet.debug "Dead members: #{dead_hosts.inspect}" unless dead_hosts.empty?
raise Puppet::Error, "Can't connect to any member of replicaset #{self.name}." if alive_hosts.empty?
else
alive_hosts = []
end

if @property_flush[:ensure] == :present and @property_hash[:ensure] != :present
if @property_flush[:ensure] == :present and @property_hash[:ensure] != :present and !master_host(alive_hosts)
Puppet.debug "Initializing the replset #{self.name}"

# Create a replset configuration
Expand All @@ -201,12 +207,35 @@ def set_members
if output['ok'] == 0
raise Puppet::Error, "rs.initiate() failed for replicaset #{self.name}: #{output['errmsg']}"
end

# Check that the replicaset has finished initialization
retry_limit = 10
retry_sleep = 3

retry_limit.times do |n|
begin
if db_ismaster(alive_hosts[0])['ismaster']
Puppet.debug 'Replica set initialization has successfully ended'
return
else
Puppet.debug "Wainting for replica initialization. Retry: #{n}"
sleep retry_sleep
next
end
end
end
raise Puppet::Error, "rs.initiate() failed for replicaset #{self.name}: host #{alive_hosts[0]} didn't become master"

else
# Add members to an existing replset
Puppet.debug "Adding member to existing replset #{self.name}"
if master = master_host(alive_hosts)
current_hosts = db_ismaster(master)['hosts']
master_data = db_ismaster(master)
current_hosts = master_data['hosts']
current_hosts = current_hosts + master_data['arbiters'] if master_data.has_key?('arbiters')
Puppet.debug "Current Hosts are: #{current_hosts.inspect}"
newhosts = alive_hosts - current_hosts
Puppet.debug "New Hosts are: #{newhosts.inspect}"
newhosts.each do |host|
output = {}
if rs_arbiter == host
Expand All @@ -225,39 +254,27 @@ def set_members
end

def mongo_command(command, host, retries=4)
self.class.mongo_command(command,host,retries)
self.class.mongo_command(command, host, retries)
end

def self.mongo_command(command, host=nil, retries=4)
# Allow waiting for mongod to become ready
# Wait for 2 seconds initially and double the delay at each retry
wait = 2
begin
args = Array.new
args << '--quiet'
args << ['--host',host] if host
args << ['--eval',"printjson(#{command})"]
output = mongo(args.flatten)
output = mongo_eval("printjson(#{command})", 'admin', retries, host)
rescue Puppet::ExecutionFailure => e
if e =~ /Error: couldn't connect to server/ and wait <= 2**max_wait
info("Waiting #{wait} seconds for mongod to become available")
sleep wait
wait *= 2
retry
else
raise
end
Puppet.debug "Got an exception: #{e}"
raise
end

# Dirty hack to remove JavaScript objects
output.gsub!(/ISODate\((.+?)\)/, '\1 ')
output.gsub!(/Timestamp\((.+?)\)/, '[\1]')
output.gsub!(/ObjectId\(([^)]*)\)/, '\1')

#Hack to avoid non-json empty sets
output = "{}" if output == "null\n"

# Parse the JSON output and return
JSON.parse(output)

end

end

0 comments on commit 56c0014

Please sign in to comment.