Skip to content

Commit 56c0014

Browse files
committed
(MODULES-1947) Improve support for MongoDB authentication and replicaset
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.
1 parent 9262b8a commit 56c0014

21 files changed

Lines changed: 812 additions & 152 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ spec/fixtures/
77
coverage/
88
.idea/
99
*.iml
10+
.ruby-*
11+
log/

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,22 @@ Use this setting to enable shard server mode for mongod.
398398
Use this setting to configure replication with replica sets. Specify a replica
399399
set name as an argument to this set. All hosts must have the same set name.
400400

401+
#####`replset_members`
402+
An array of member hosts for the replica set.
403+
Mutually exclusive with `replset_config` param.
404+
405+
#####`replset_config`
406+
A hash that is used to configure the replica set.
407+
Mutually exclusive with `replset_members` param.
408+
409+
```puppet
410+
class mongodb::server {
411+
replset => 'rsmain',
412+
replset_config => { 'rsmain' => { ensure => present, members => ['host1:27017', 'host2:27017', 'host3:27017'] } }
413+
414+
}
415+
```
416+
401417
#####`rest`
402418
Set to true to enable a simple REST interface. Default: false
403419

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

492+
#####`create_admin`
493+
Allows to create admin user for admin database.
494+
Redefine these parameters if needed:
495+
496+
#####`admin_username`
497+
Administrator user name
498+
499+
#####`admin_password`
500+
Administrator user password
501+
502+
#####`admin_roles`
503+
Administrator user roles
504+
505+
#####`store_creds`
506+
Store admin credentials in mongorc.js file. Uses with `create_admin` parameter
507+
508+
476509
####Class: mongodb::mongos
477510
class. This class should only be used if you want to implement sharding within
478511
your mongodb deployment.
@@ -565,6 +598,9 @@ The maximum amount of two second tries to wait MongoDB startup. Default: 10
565598
#### Provider: mongodb_user
566599
'mongodb_user' can be used to create and manage users within MongoDB database.
567600

601+
*Note:* if replica set is enabled, replica initialization has to come before
602+
any user operations.
603+
568604
```puppet
569605
mongodb_user { testuser:
570606
name => 'testuser',

lib/puppet/provider/mongodb.rb

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'yaml'
2+
require 'json'
23
class Puppet::Provider::Mongodb < Puppet::Provider
34

45
# Without initvars commands won't work.
@@ -74,9 +75,44 @@ def self.get_conn_string
7475
"#{ip_real}:#{port_real}"
7576
end
7677

78+
def self.db_ismaster
79+
cmd_ismaster = 'printjson(db.isMaster())'
80+
if mongorc_file
81+
cmd_ismaster = mongorc_file + cmd_ismaster
82+
end
83+
out = mongo(['admin', '--quiet', '--host', get_conn_string, '--eval', cmd_ismaster])
84+
out.gsub!(/ObjectId\(([^)]*)\)/, '\1')
85+
out.gsub!(/ISODate\((.+?)\)/, '\1 ')
86+
out.gsub!(/^Error\:.+/, '')
87+
res = JSON.parse out
88+
89+
return res['ismaster']
90+
end
91+
92+
def db_ismaster
93+
self.class.db_ismaster
94+
end
95+
96+
def self.auth_enabled
97+
auth_enabled = false
98+
file = get_mongod_conf_file
99+
config = YAML.load_file(file)
100+
if config.kind_of?(Hash)
101+
auth_enabled = config['security.authorization']
102+
else # It has to be a key-value store
103+
config = {}
104+
File.readlines(file).collect do |line|
105+
k,v = line.split('=')
106+
config[k.rstrip] = v.lstrip.chomp if k and v
107+
end
108+
auth_enabled = config['auth']
109+
end
110+
return auth_enabled
111+
end
112+
77113
# Mongo Command Wrapper
78-
def self.mongo_eval(cmd, db = 'admin')
79-
retry_count = 10
114+
def self.mongo_eval(cmd, db = 'admin', retries = 10, host = nil)
115+
retry_count = retries
80116
retry_sleep = 3
81117
if mongorc_file
82118
cmd = mongorc_file + cmd
@@ -85,25 +121,30 @@ def self.mongo_eval(cmd, db = 'admin')
85121
out = nil
86122
retry_count.times do |n|
87123
begin
88-
out = mongo([db, '--quiet', '--host', get_conn_string, '--eval', cmd])
124+
if host
125+
out = mongo([db, '--quiet', '--host', host, '--eval', cmd])
126+
else
127+
out = mongo([db, '--quiet', '--host', get_conn_string, '--eval', cmd])
128+
end
89129
rescue => e
90-
debug "Request failed: '#{e.message}' Retry: '#{n}'"
130+
Puppet.debug "Request failed: '#{e.message}' Retry: '#{n}'"
91131
sleep retry_sleep
92132
next
93133
end
94134
break
95135
end
96136

97137
if !out
98-
fail "Could not evalute MongoDB shell command: #{cmd}"
138+
raise Puppet::ExecutionFailure, "Could not evalute MongoDB shell command: #{cmd}"
99139
end
100140

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

105-
def mongo_eval(cmd, db = 'admin')
106-
self.class.mongo_eval(cmd, db)
146+
def mongo_eval(cmd, db = 'admin', retries = 10, host = nil)
147+
self.class.mongo_eval(cmd, db, retries, host)
107148
end
108149

109150
# Mongo Version checker

lib/puppet/provider/mongodb_database/mongodb.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,19 @@ def self.prefetch(resources)
2626
end
2727

2828
def create
29-
mongo_eval('db.dummyData.insert({"created_by_puppet": 1})', @resource[:name])
29+
if db_ismaster
30+
mongo_eval('db.dummyData.insert({"created_by_puppet": 1})', @resource[:name])
31+
else
32+
Puppet.warning 'Database creation is available only from master host'
33+
end
3034
end
3135

3236
def destroy
33-
mongo_eval('db.dropDatabase()', @resource[:name])
37+
if db_ismaster
38+
mongo_eval('db.dropDatabase()', @resource[:name])
39+
else
40+
Puppet.warning 'Database removal is available only from master host'
41+
end
3442
end
3543

3644
def exists?

lib/puppet/provider/mongodb_replset/mongo.rb

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
false
1616
end
1717

18-
commands :mongo => 'mongo'
19-
2018
mk_resource_methods
2119

2220
def initialize(resource={})
@@ -67,31 +65,43 @@ def flush
6765
private
6866

6967
def db_ismaster(host)
70-
mongo_command("db.isMaster()", host)
68+
mongo_command('db.isMaster()', host)
7169
end
7270

7371
def rs_initiate(conf, master)
74-
return mongo_command("rs.initiate(#{conf})", master)
72+
if auth_enabled
73+
return mongo_command("rs.initiate(#{conf})", initialize_host)
74+
else
75+
return mongo_command("rs.initiate(#{conf})", master)
76+
end
7577
end
7678

7779
def rs_status(host)
78-
mongo_command("rs.status()", host)
80+
mongo_command('rs.status()', host)
7981
end
8082

8183
def rs_add(host, master)
82-
mongo_command("rs.add(\"#{host}\")", master)
84+
mongo_command("rs.add('#{host}')", master)
8385
end
8486

8587
def rs_remove(host, master)
86-
mongo_command("rs.remove(\"#{host}\")", master)
88+
mongo_command("rs.remove('#{host}')", master)
8789
end
8890

8991
def rs_arbiter
9092
@resource[:arbiter]
9193
end
9294

9395
def rs_add_arbiter(host, master)
94-
mongo_command("rs.addArb(\"#{host}\")", master)
96+
mongo_command("rs.addArb('#{host}')", master)
97+
end
98+
99+
def auth_enabled
100+
self.class.auth_enabled
101+
end
102+
103+
def initialize_host
104+
@resource[:initialize_host]
95105
end
96106

97107
def master_host(hosts)
@@ -104,17 +114,7 @@ def master_host(hosts)
104114
false
105115
end
106116

107-
def self.get_mongod_conf_file
108-
if File.exists? '/etc/mongod.conf'
109-
file = '/etc/mongod.conf'
110-
else
111-
file = '/etc/mongodb.conf'
112-
end
113-
file
114-
end
115-
116117
def self.get_replset_properties
117-
118118
conn_string = get_conn_string
119119
output = mongo_command('rs.conf()', conn_string)
120120
if output['members']
@@ -135,31 +135,37 @@ def self.get_replset_properties
135135
end
136136

137137
def alive_members(hosts)
138+
alive = []
138139
hosts.select do |host|
139140
begin
140141
Puppet.debug "Checking replicaset member #{host} ..."
141142
status = rs_status(host)
142143
if status.has_key?('errmsg') and status['errmsg'] == 'not running with --replSet'
143144
raise Puppet::Error, "Can't configure replicaset #{self.name}, host #{host} is not supposed to be part of a replicaset."
144145
end
146+
147+
if auth_enabled and status.has_key?('errmsg') and (status['errmsg'].include? "unauthorized" or status['errmsg'].include? "not authorized")
148+
Puppet.warning "Host #{host} is available, but you are unauthorized because of authentication is enabled: #{auth_enabled}"
149+
alive.push(host)
150+
end
151+
145152
if status.has_key?('set')
146153
if status['set'] != self.name
147154
raise Puppet::Error, "Can't configure replicaset #{self.name}, host #{host} is already part of another replicaset."
148155
end
149156

150157
# This node is alive and supposed to be a member of our set
151158
Puppet.debug "Host #{host} is available for replset #{status['set']}"
152-
true
159+
alive.push(host)
153160
elsif status.has_key?('info')
154161
Puppet.debug "Host #{host} is alive but unconfigured: #{status['info']}"
155-
true
162+
alive.push(host)
156163
end
157164
rescue Puppet::ExecutionFailure
158165
Puppet.warning "Can't connect to replicaset member #{host}."
159-
160-
false
161166
end
162167
end
168+
return alive
163169
end
164170

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

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

189195
# Create a replset configuration
@@ -201,12 +207,35 @@ def set_members
201207
if output['ok'] == 0
202208
raise Puppet::Error, "rs.initiate() failed for replicaset #{self.name}: #{output['errmsg']}"
203209
end
210+
211+
# Check that the replicaset has finished initialization
212+
retry_limit = 10
213+
retry_sleep = 3
214+
215+
retry_limit.times do |n|
216+
begin
217+
if db_ismaster(alive_hosts[0])['ismaster']
218+
Puppet.debug 'Replica set initialization has successfully ended'
219+
return
220+
else
221+
Puppet.debug "Wainting for replica initialization. Retry: #{n}"
222+
sleep retry_sleep
223+
next
224+
end
225+
end
226+
end
227+
raise Puppet::Error, "rs.initiate() failed for replicaset #{self.name}: host #{alive_hosts[0]} didn't become master"
228+
204229
else
205230
# Add members to an existing replset
231+
Puppet.debug "Adding member to existing replset #{self.name}"
206232
if master = master_host(alive_hosts)
207-
current_hosts = db_ismaster(master)['hosts']
233+
master_data = db_ismaster(master)
234+
current_hosts = master_data['hosts']
235+
current_hosts = current_hosts + master_data['arbiters'] if master_data.has_key?('arbiters')
208236
Puppet.debug "Current Hosts are: #{current_hosts.inspect}"
209237
newhosts = alive_hosts - current_hosts
238+
Puppet.debug "New Hosts are: #{newhosts.inspect}"
210239
newhosts.each do |host|
211240
output = {}
212241
if rs_arbiter == host
@@ -225,39 +254,27 @@ def set_members
225254
end
226255

227256
def mongo_command(command, host, retries=4)
228-
self.class.mongo_command(command,host,retries)
257+
self.class.mongo_command(command, host, retries)
229258
end
230259

231260
def self.mongo_command(command, host=nil, retries=4)
232-
# Allow waiting for mongod to become ready
233-
# Wait for 2 seconds initially and double the delay at each retry
234-
wait = 2
235261
begin
236-
args = Array.new
237-
args << '--quiet'
238-
args << ['--host',host] if host
239-
args << ['--eval',"printjson(#{command})"]
240-
output = mongo(args.flatten)
262+
output = mongo_eval("printjson(#{command})", 'admin', retries, host)
241263
rescue Puppet::ExecutionFailure => e
242-
if e =~ /Error: couldn't connect to server/ and wait <= 2**max_wait
243-
info("Waiting #{wait} seconds for mongod to become available")
244-
sleep wait
245-
wait *= 2
246-
retry
247-
else
248-
raise
249-
end
264+
Puppet.debug "Got an exception: #{e}"
265+
raise
250266
end
251267

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

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

275+
# Parse the JSON output and return
260276
JSON.parse(output)
277+
261278
end
262279

263280
end

0 commit comments

Comments
 (0)