From 57b1ee5ab26789578c321a4f6464705af0dcca76 Mon Sep 17 00:00:00 2001 From: Grant Ridder Date: Wed, 12 Apr 2017 01:55:26 -0700 Subject: [PATCH] Rewrite Part 2 (#140) * Update user_management recipe testing * Add support for mongo gem 2.x for user add * Cleanup resource cloning * Suppress chef_gem compile_time warnings * Remove Makefile * Remove python dep since it is no longer used * Add support for mongo gem 2.x for user delete * Add actions as option in users array [@jrstarke](https://github.com/jrstarke) https://github.com/sous-chefs/mongodb/pull/91 * Restrict user kitchen testing to one OS --- .kitchen.yml | 64 +++++- .travis.yml | 1 + Berksfile | 2 + Makefile | 20 -- metadata.rb | 1 - providers/user.rb | 187 +++++++++++++++++- recipes/install.rb | 6 +- recipes/mongo_gem.rb | 1 + recipes/user_management.rb | 2 + .../cookbooks/mongodb_spec/metadata.rb | 3 + .../mongodb_spec/recipes/user_delete.rb | 6 + .../inspec/user_management_spec.rb | 7 +- .../inspec/user_management_spec.rb | 29 +++ .../inspec/user_management_spec.rb | 29 +++ 14 files changed, 325 insertions(+), 33 deletions(-) delete mode 100644 Makefile create mode 100644 test/fixtures/cookbooks/mongodb_spec/metadata.rb create mode 100644 test/fixtures/cookbooks/mongodb_spec/recipes/user_delete.rb create mode 100644 test/integration/user_management_v2/inspec/user_management_spec.rb create mode 100644 test/integration/user_management_v2_delete/inspec/user_management_spec.rb diff --git a/.kitchen.yml b/.kitchen.yml index b1f2898..2bdf855 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -1,9 +1,13 @@ --- driver: name: vagrant - chef_version: latest + chef_version: 12.19.36 + # chef_version: latest linked_clone: true +provisioner: + require_chef_omnibus: 12.19.36 + verifier: name: inspec @@ -88,10 +92,58 @@ suites: - "recipe[sc-mongodb::user_management]" attributes: mongodb: - install_method: mongodb-org - # Needed to read the correct config file - # since mongo 2.6 - default_init_name: mongod - dbconfig_file: mongodb.conf config: auth: true + users: + - username: kitchen + password: blah123 + roles: + - read + database: admin + includes: + # Only need to test this on one OS since this is + # purely to test mongo ruby driver code + - centos-7.3 + +- name: user_management_v2 + run_list: + - recipe[sc-mongodb::default] + - recipe[sc-mongodb::user_management] + attributes: + mongodb: + config: + auth: true + ruby_gems: + mongo: ~> 2.0 + users: + - username: kitchen + password: blah123 + roles: + - read + database: admin + includes: + # Only need to test this on one OS since this is + # purely to test mongo ruby driver code + - centos-7.3 + +- name: user_management_v2_delete + run_list: + - recipe[sc-mongodb::default] + - recipe[sc-mongodb::user_management] + - recipe[mongodb_spec::user_delete] + attributes: + mongodb: + config: + auth: true + ruby_gems: + mongo: ~> 2.0 + users: + - username: kitchen + password: blah123 + roles: + - read + database: admin + includes: + # Only need to test this on one OS since this is + # purely to test mongo ruby driver code + - centos-7.3 diff --git a/.travis.yml b/.travis.yml index 392d83f..3d37c4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ install: echo "skip bundle install" branches: only: - master + - rewrite_v1 # Ensure we make ChefDK's Ruby the default before_script: diff --git a/Berksfile b/Berksfile index 34fea21..c19f35e 100644 --- a/Berksfile +++ b/Berksfile @@ -1,3 +1,5 @@ source 'https://supermarket.chef.io' metadata + +cookbook 'mongodb_spec', path: 'test/fixtures/cookbooks/mongodb_spec' diff --git a/Makefile b/Makefile deleted file mode 100644 index 4bce139..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ - -COOKBOOK=sc-mongodb -BRANCH=master - -BUILD_DIR=../build -DIST_PREFIX=$(BUILD_DIR)/$(COOKBOOK) - -all: metadata.json - -clean: - -rm metadata.json - -metadata.json: - -rm $@ - knife cookbook metadata -o .. $(COOKBOOK) - -dist: clean metadata.json - mkdir -p $(BUILD_DIR) - version=`python -c "import json;c = json.load(open('metadata.json')); print c.get('version', 'UNKNOWN')"`; \ - tar --exclude-vcs --exclude=Makefile -cvzf $(DIST_PREFIX)-$$version.tar.gz ../$(COOKBOOK) diff --git a/metadata.rb b/metadata.rb index fc771f6..b2c622c 100644 --- a/metadata.rb +++ b/metadata.rb @@ -16,7 +16,6 @@ depends 'apt', '>= 1.8.2' depends 'yum', '>= 3.0' -depends 'python' depends 'build-essential', '>= 5.0.0' %w( diff --git a/providers/user.rb b/providers/user.rb index 4f78cd6..75bb1bc 100644 --- a/providers/user.rb +++ b/providers/user.rb @@ -4,6 +4,10 @@ def user_exists?(username, connection) connection['admin']['system.users'].find(user: username).count > 0 end +def user_exists_v2?(username, connection) + connection['system.users'].find(user: username).count > 0 +end + def add_user(username, password, database, roles = []) require 'rubygems' require 'mongo' @@ -78,6 +82,97 @@ def add_user(username, password, database, roles = []) end end +def add_user_v2(username, password, database, roles = []) + # 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') + 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 (@new_resource.connection['config']['auth'] == true) || (@new_resource.connection['mongos_create_admin'] == true) + begin + connection = retrieve_db_v2( + @new_resource.connection['authentication']['username'], + @new_resource.connection['authentication']['password'] + ) + rescue Mongo::Auth::Unauthorized => e + # invalid creds + Chef::Log.warn("Unable to authenticate as admin user. If this is a fresh install, ignore warning: #{e}") + connection = retrieve_db_v2 + rescue Mongo::Error::NoServerAvailable => e + # Replicaset not initialized + Chef::Log.warn("Server appears to be part of an uninitialized or initializing replicaset: #{e}") + Chef::Log.warn('Retrying 1 time') + sleep(@new_resource.connection['mongod_create_user']['delay']) + begin + connection = retrieve_db_v2 + rescue Mongo::Error::NoServerAvailable => e + Chef::Application.fatal!("Unable to connect to mongo: #{e}") + end + end + end + + admin = connection.use('admin') + db = connection.use(database) + + begin + if user_exists_v2?(username, connection) + Chef::Log.warn("#{username} already exists on #{database}") + else + # Create the user + db.database.users.create( + username, + password: password, + roles: roles + ) + Chef::Log.info("Created user #{username} on #{database}") + end + rescue Mongo::Error::OperationFailure => e + # User probably already exists + Chef::Application.fatal!("Unable to add user on initial try: #{e}") + rescue Mongo::Error::NoServerAvailable => 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 + rs_info = admin.command(replSetGetStatus: 1) + rs_info_self = rs_info.documents[0]['members'].select { |a| a['self'] }.first + has_info_message = rs_info_self.key?('infoMessage') + + if rs_info_self['state'] == 1 + # This node is a primary node, try to add the user + db.database.users.create( + username, + password: password, + roles: roles + ) + Chef::Log.info("Created or updated user #{username} on #{database} of primary replicaset node") + break + elsif rs_info_self['state'] == 2 && has_info_message + # 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 rs_info_self['state'] == 2 && !has_info_message + # 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 + end + + i += 1 + sleep(@new_resource.connection['mongod_create_user']['delay']) + end + else + Chef::Application.fatal!("Unable to add user: #{e}") + end + end +end + # Drop a user from the database specified def delete_user(username, database) require 'rubygems' @@ -104,6 +199,40 @@ def delete_user(username, database) end end +def delete_user_v2(username, database) + if (@new_resource.connection['config']['auth'] == true) || (@new_resource.connection['mongos_create_admin'] == true) + begin + connection = retrieve_db_v2( + @new_resource.connection['authentication']['username'], + @new_resource.connection['authentication']['password'] + ) + rescue Mongo::Auth::Unauthorized => e + # invalid creds + Chef::Application.fatal!("Unable to authenticate as admin user: #{e}") + connection = retrieve_db_v2 + rescue Mongo::Error::NoServerAvailable => e + # Replicaset not initialized + Chef::Log.warn("Server appears to be part of an uninitialized or initializing replicaset: #{e}") + Chef::Log.warn('Retrying 1 time') + sleep(@new_resource.connection['mongod_create_user']['delay']) + begin + connection = retrieve_db_v2 + rescue Mongo::Error::NoServerAvailable => e + Chef::Application.fatal!("Unable to connect to mongo: #{e}") + end + end + end + + db = connection.use(database) + + if user_exists_v2?(username, connection) + db.database.users.remove(username) + Chef::Log.info("Deleted user #{username} on #{database}") + else + Chef::Log.warn("Unable to delete non-existent user #{username} on #{database}") + end +end + # Get the MongoClient connection def retrieve_db(attempt = 0) require 'rubygems' @@ -125,14 +254,66 @@ def retrieve_db(attempt = 0) end end +def retrieve_db_v2(username = nil, password = nil, attempt = 0) + require 'mongo' + + host = @new_resource.connection['host'] || 'localhost' + port = @new_resource.connection['port'] || 27017 + + begin + Chef::Log.info("Connecting to #{host}:#{port} with #{username}") + client = Mongo::Client.new( + ["#{host}:#{port}"], + user: username, + password: password, + connect_timeout: 5, + socket_timeout: 5, + max_read_retries: 5, + server_selection_timeout: 3 + ) + + # Query the server for all database names to verify server connection + client.database_names + rescue Mongo::Error::NoServerAvailable, Mongo::Error::OperationFailure => e + if attempt < @new_resource.connection['user_management']['connection']['retries'] + Chef::Log.warn("Unable to connect to MongoDB instance: #{e}, retrying in #{@new_resource.connection['user_management']['connection']['delay']} second(s)...") + sleep(@new_resource.connection['user_management']['connection']['delay']) + retrieve_db_v2(username, password, attempt + 1) + end + end + + client +end + action :add do - add_user(new_resource.username, new_resource.password, new_resource.database, new_resource.roles) + require 'mongo' + if defined?(Mongo::VERSION) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.0.0') + # The gem displays a lot of debug messages by default so set to INFO + Mongo::Logger.logger.level = ::Logger::INFO + add_user_v2(new_resource.username, new_resource.password, new_resource.database, new_resource.roles) + else # mongo gem version 1.x + add_user(new_resource.username, new_resource.password, new_resource.database, new_resource.roles) + end end action :delete do - delete_user(new_resource.username, new_resource.database) + require 'mongo' + if defined?(Mongo::VERSION) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.0.0') + # The gem displays a lot of debug messages by default so set to INFO + Mongo::Logger.logger.level = ::Logger::INFO + delete_user_v2(new_resource.username, new_resource.database) + else # mongo gem version 1.x + delete_user(new_resource.username, new_resource.database) + end end action :modify do - add_user(new_resource.username, new_resource.password, new_resource.database, new_resource.roles) + require 'mongo' + if defined?(Mongo::VERSION) && Gem::Version.new(Mongo::VERSION) >= Gem::Version.new('2.0.0') + # The gem displays a lot of debug messages by default so set to INFO + Mongo::Logger.logger.level = ::Logger::INFO + # TODO: implement modify for 2.x gem + else # mongo gem version 1.x + add_user(new_resource.username, new_resource.password, new_resource.database, new_resource.roles) + end end diff --git a/recipes/install.rb b/recipes/install.rb index 85c0448..a2b2f6b 100644 --- a/recipes/install.rb +++ b/recipes/install.rb @@ -40,7 +40,8 @@ end # just-in-case config file drop -template node['mongodb']['dbconfig_file'][config_type] do +template "#{node['mongodb']['dbconfig_file'][config_type]} install" do + path node['mongodb']['dbconfig_file'][config_type] cookbook node['mongodb']['template_cookbook'] source node['mongodb']['dbconfig_file']['template'] group node['mongodb']['root_group'] @@ -68,7 +69,8 @@ action :nothing end -template init_file do +template "#{init_file} install" do + path init_file cookbook node['mongodb']['template_cookbook'] source node['mongodb']['init_script_template'] group node['mongodb']['root_group'] diff --git a/recipes/mongo_gem.rb b/recipes/mongo_gem.rb index 3bebb96..a8d1d4c 100644 --- a/recipes/mongo_gem.rb +++ b/recipes/mongo_gem.rb @@ -42,5 +42,6 @@ node['mongodb']['ruby_gems'].each do |gem, version| chef_gem gem do version version + compile_time false end end diff --git a/recipes/user_management.rb b/recipes/user_management.rb index 357434c..49c676d 100644 --- a/recipes/user_management.rb +++ b/recipes/user_management.rb @@ -44,6 +44,8 @@ action :nothing subscribes :add, 'ruby_block[config_replicaset]', :delayed subscribes :add, 'ruby_block[config_sharding]', :delayed + else + action user['action'] || :add end end end diff --git a/test/fixtures/cookbooks/mongodb_spec/metadata.rb b/test/fixtures/cookbooks/mongodb_spec/metadata.rb new file mode 100644 index 0000000..5ad18a8 --- /dev/null +++ b/test/fixtures/cookbooks/mongodb_spec/metadata.rb @@ -0,0 +1,3 @@ +name 'mongodb_spec' + +depends 'sc-mongodb' diff --git a/test/fixtures/cookbooks/mongodb_spec/recipes/user_delete.rb b/test/fixtures/cookbooks/mongodb_spec/recipes/user_delete.rb new file mode 100644 index 0000000..aa95758 --- /dev/null +++ b/test/fixtures/cookbooks/mongodb_spec/recipes/user_delete.rb @@ -0,0 +1,6 @@ +mongodb_user '"kitchen" user delete' do + username 'kitchen' + database 'admin' + connection node['mongodb'] + action :delete +end diff --git a/test/integration/user_management/inspec/user_management_spec.rb b/test/integration/user_management/inspec/user_management_spec.rb index b53bb74..bbd3922 100644 --- a/test/integration/user_management/inspec/user_management_spec.rb +++ b/test/integration/user_management/inspec/user_management_spec.rb @@ -8,7 +8,7 @@ # [ $? -eq 0 ] # } -describe service('mongodb') do +describe service('mongod') do it { should be_installed } it { should be_enabled } it { should be_running } @@ -22,3 +22,8 @@ describe bash('mongo --eval "db.stats().ok"') do its('exit_status') { should_not eq 1 } end + +# kitchen read user created +describe bash(%(mongo admin -u admin -p admin --eval "db.system.users.find({'_id' : 'admin.kitchen', 'user' : 'kitchen', 'db' : 'admin', 'roles' : [ { 'role' : 'read', 'db' : 'admin' } ]})" | grep _id)) do + its('exit_status') { should eq 0 } +end diff --git a/test/integration/user_management_v2/inspec/user_management_spec.rb b/test/integration/user_management_v2/inspec/user_management_spec.rb new file mode 100644 index 0000000..bbd3922 --- /dev/null +++ b/test/integration/user_management_v2/inspec/user_management_spec.rb @@ -0,0 +1,29 @@ +# @test "requires authentication" { +# mongo --eval "db.stats().ok" +# ! [ $? -eq 1 ] +# } +# +# @test "admin user created" { +# mongo admin -u admin -p admin --eval "db.stats().ok" +# [ $? -eq 0 ] +# } + +describe service('mongod') do + it { should be_installed } + it { should be_enabled } + it { should be_running } +end + +# admin user created +describe bash('mongo admin -u admin -p admin --eval "db.stats().ok"') do + its('exit_status') { should eq 0 } +end + +describe bash('mongo --eval "db.stats().ok"') do + its('exit_status') { should_not eq 1 } +end + +# kitchen read user created +describe bash(%(mongo admin -u admin -p admin --eval "db.system.users.find({'_id' : 'admin.kitchen', 'user' : 'kitchen', 'db' : 'admin', 'roles' : [ { 'role' : 'read', 'db' : 'admin' } ]})" | grep _id)) do + its('exit_status') { should eq 0 } +end diff --git a/test/integration/user_management_v2_delete/inspec/user_management_spec.rb b/test/integration/user_management_v2_delete/inspec/user_management_spec.rb new file mode 100644 index 0000000..a1266b3 --- /dev/null +++ b/test/integration/user_management_v2_delete/inspec/user_management_spec.rb @@ -0,0 +1,29 @@ +# @test "requires authentication" { +# mongo --eval "db.stats().ok" +# ! [ $? -eq 1 ] +# } +# +# @test "admin user created" { +# mongo admin -u admin -p admin --eval "db.stats().ok" +# [ $? -eq 0 ] +# } + +describe service('mongod') do + it { should be_installed } + it { should be_enabled } + it { should be_running } +end + +# admin user created +describe bash('mongo admin -u admin -p admin --eval "db.stats().ok"') do + its('exit_status') { should eq 0 } +end + +describe bash('mongo --eval "db.stats().ok"') do + its('exit_status') { should_not eq 1 } +end + +# kitchen read user created but then deleted +describe bash(%(mongo admin -u admin -p admin --eval "db.system.users.find({'_id' : 'admin.kitchen', 'user' : 'kitchen', 'db' : 'admin', 'roles' : [ { 'role' : 'read', 'db' : 'admin' } ]})" | grep _id)) do + its('exit_status') { should eq 1 } +end