Merge pull request #341 from chef/chris-rock/mount

implement `mount` resource
This commit is contained in:
Dominik Richter 2016-01-04 03:41:45 +01:00
commit 5df44fef9a
22 changed files with 372 additions and 14 deletions

View file

@ -26,6 +26,7 @@ The following InSpec audit resources are available:
* `kernel_parameter`_ * `kernel_parameter`_
* `limits_conf`_ * `limits_conf`_
* `login_defs`_ * `login_defs`_
* `mount`_
* `mysql_conf`_ * `mysql_conf`_
* `mysql_session`_ * `mysql_session`_
* `npm`_ * `npm`_
@ -2116,6 +2117,84 @@ The following examples show how to use this InSpec audit resource.
its('PASS_MAX_DAYS') { should eq '90' } its('PASS_MAX_DAYS') { should eq '90' }
end end
mount
=====================================================
Use the ``mount`` |inspec resource| to test the mountpoints on |linux| systems.
**Stability: Experimental**
Syntax
-----------------------------------------------------
An ``mount`` |inspec resource| block declares the synchronization settings that should be tested:
.. code-block:: ruby
describe mount('path') do
it { should MATCHER 'value' }
end
where
* ``('path')`` is the path to the mounted directory
* ``MATCHER`` is a valid matcher for this |inspec resource|
* ``'value'`` is the value to be tested
Matchers
-----------------------------------------------------
This |inspec resource| has the following matchers:
be_mounted
+++++++++++++++++++++++++++++++++++++++++++++++++++++
The ``be_mounted`` matcher tests if the file is accessible from the file system:
.. code-block:: ruby
it { should be_mounted }
device
+++++++++++++++++++++++++++++++++++++++++++++++++++++
The ``device`` matcher tests the device from the fstab table:
.. code-block:: ruby
its('device') { should eq '/dev/mapper/VolGroup-lv_root' }
type
+++++++++++++++++++++++++++++++++++++++++++++++++++++
The ``type`` matcher tests the filesystem type:
.. code-block:: ruby
its('type') { should eq 'ext4' }
options
+++++++++++++++++++++++++++++++++++++++++++++++++++++
The ``options`` matcher tests the mount options for the filesystem from the fstab table:
.. code-block:: ruby
its('options') { should eq ['rw', 'mode=620'] }
Examples
-----------------------------------------------------
The following examples show how to use this InSpec audit resource.
**Test a the mount point on '/'**
.. code-block:: ruby
describe mount('/') do
it { should be_mounted }
its('device') { should eq '/dev/mapper/VolGroup-lv_root' }
its('type') { should eq 'ext4' }
its('options') { should eq ['rw', 'mode=620'] }
end
mysql_conf mysql_conf
===================================================== =====================================================
Use the ``mysql_conf`` |inspec resource| to test the contents of the configuration file for |mysql|, typically located at ``/etc/mysql/my.cnf`` or ``/etc/my.cnf``. Use the ``mysql_conf`` |inspec resource| to test the contents of the configuration file for |mysql|, typically located at ``/etc/mysql/my.cnf`` or ``/etc/my.cnf``.

View file

@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib'] spec.require_paths = ['lib']
spec.add_dependency 'r-train', '~> 0.9' spec.add_dependency 'r-train', '~> 0.9', '>= 0.9.3'
spec.add_dependency 'thor', '~> 0.19' spec.add_dependency 'thor', '~> 0.19'
spec.add_dependency 'json', '~> 1.8' spec.add_dependency 'json', '~> 1.8'
spec.add_dependency 'rainbow', '~> 2' spec.add_dependency 'rainbow', '~> 2'

View file

@ -44,6 +44,7 @@ require 'resources/kernel_module'
require 'resources/kernel_parameter' require 'resources/kernel_parameter'
require 'resources/limits_conf' require 'resources/limits_conf'
require 'resources/login_def' require 'resources/login_def'
require 'resources/mount'
require 'resources/mysql' require 'resources/mysql'
require 'resources/mysql_conf' require 'resources/mysql_conf'
require 'resources/mysql_session' require 'resources/mysql_session'

View file

@ -271,3 +271,33 @@ RSpec::Matchers.define :cmp do |expected|
"\nexpected: value != #{expected}\n got: #{actual}\n\n(compared using `cmp` matcher)\n" "\nexpected: value != #{expected}\n got: #{actual}\n\n(compared using `cmp` matcher)\n"
end end
end end
# user resource matcher for serverspec compatibility
# This matcher will be deprecated in future
RSpec::Matchers.define :be_mounted do
match do |path|
if !@options.nil?
path.mounted?(@options, @identical)
else
path.mounted?
end
end
chain :with do |attr|
@options = attr
@identical = false
end
chain :only_with do |attr|
@options = attr
@identical = true
end
failure_message do |path|
if !@options.nil?
"\n#{path} is not mounted with the options\n expected: #{@options}\n got: #{path.mount_options}\n"
else
"\n#{path} is not mounted\n"
end
end
end

View file

@ -26,7 +26,7 @@ require 'utils/parser'
class EtcGroup < Inspec.resource(1) class EtcGroup < Inspec.resource(1)
include Converter include Converter
include ContentParser include CommentParser
name 'etc_group' name 'etc_group'
desc 'Use the etc_group InSpec audit resource to test groups that are defined on Linux and UNIX platforms. The /etc/group file stores details about each group---group name, password, group identifier, along with a comma-separate list of users that belong to the group.' desc 'Use the etc_group InSpec audit resource to test groups that are defined on Linux and UNIX platforms. The /etc/group file stores details about each group---group name, password, group identifier, along with a comma-separate list of users that belong to the group.'

View file

@ -5,7 +5,7 @@
# license: All rights reserved # license: All rights reserved
module Inspec::Resources module Inspec::Resources
class File < Inspec.resource(1) class File < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
name 'file' name 'file'
desc 'Use the file InSpec audit resource to test all system file types, including files, directories, symbolic links, named pipes, sockets, character devices, block devices, and doors.' desc 'Use the file InSpec audit resource to test all system file types, including files, directories, symbolic links, named pipes, sockets, character devices, block devices, and doors.'
example " example "
@ -18,8 +18,9 @@ module Inspec::Resources
its('mode') { should eq 0644 } its('mode') { should eq 0644 }
end end
" "
include MountParser
attr_reader :file, :path attr_reader :file, :path, :mount_options
def initialize(path) def initialize(path)
@path = path @path = path
@file = inspec.backend.file(@path) @file = inspec.backend.file(@path)
@ -28,7 +29,7 @@ module Inspec::Resources
%w{ %w{
type exist? file? block_device? character_device? socket? directory? type exist? file? block_device? character_device? socket? directory?
symlink? pipe? mode mode? owner owned_by? group grouped_into? link_target symlink? pipe? mode mode? owner owned_by? group grouped_into? link_target
link_path linked_to? content mtime size selinux_label mounted? immutable? link_path linked_to? content mtime size selinux_label immutable?
product_version file_version version? md5sum sha256sum product_version file_version version? md5sum sha256sum
}.each do |m| }.each do |m|
define_method m.to_sym do |*args| define_method m.to_sym do |*args|
@ -58,6 +59,30 @@ module Inspec::Resources
file_permission_granted?('x', by_usergroup, by_specific_user) file_permission_granted?('x', by_usergroup, by_specific_user)
end end
def mounted?(expected_options = nil, identical = false)
mounted = file.mounted
# return if no additional parameters have been provided
return file.mounted? if expected_options.nil?
# deprecation warning, this functionality will be removed in future version
warn "[DEPRECATION] `be_mounted.with and be_mounted.only_with` are deprecated. Please use `mount('#{path}')` instead."
# we cannot read mount data on non-Linux systems
return nil if !inspec.os.linux?
# parse content if we are on linux
@mount_options ||= parse_mount_options(mounted.stdout, true)
if identical
# check if the options should be identical
@mount_options == expected_options
else
# otherwise compare the selected values
@mount_options.contains(expected_options)
end
end
def to_s def to_s
"File #{path}" "File #{path}"
end end

57
lib/resources/mount.rb Normal file
View file

@ -0,0 +1,57 @@
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
require 'utils/simpleconfig'
class Mount < Inspec.resource(1)
name 'mount'
desc 'Use the mount InSpec audit resource to test if mount points.'
example "
describe mount('/') do
it { should be_mounted }
its(:count) { should eq 1 }
its('device') { should eq '/dev/mapper/VolGroup-lv_root' }
its('type') { should eq 'ext4' }
its('options') { should eq ['rw', 'mode=620'] }
end
"
include MountParser
attr_reader :file
def initialize(path)
@path = path
return skip_resource 'The `mount` resource is not supported on your OS yet.' if !inspec.os.linux?
@file = inspec.backend.file(@path)
end
def mounted?
file.mounted?
end
def count
mounted = file.mounted
return nil if mounted.nil? || mounted.stdout.nil?
mounted.stdout.lines.count
end
def method_missing(name)
return nil if !file.mounted?
mounted = file.mounted
return nil if mounted.nil? || mounted.stdout.nil?
line = mounted.stdout
# if we got multiple lines, only use the last entry
line = mounted.stdout.lines.to_a.last if mounted.stdout.lines.count > 1
# parse content if we are on linux
@mount_options ||= parse_mount_options(line)
@mount_options[name]
end
def to_s
"Mount #{@path}"
end
end

View file

@ -37,7 +37,7 @@ class Passwd < Inspec.resource(1)
end end
" "
include ContentParser include PasswdParser
attr_reader :uid attr_reader :uid
attr_reader :parsed attr_reader :parsed

View file

@ -230,7 +230,8 @@ class UnixUser < UserInfo
end end
class LinuxUser < UnixUser class LinuxUser < UnixUser
include ContentParser include PasswdParser
include CommentParser
def meta_info(username) def meta_info(username)
cmd = inspec.command("getent passwd #{username}") cmd = inspec.command("getent passwd #{username}")
@ -295,7 +296,7 @@ end
# - chpass(1) A flexible tool for changing user database information. # - chpass(1) A flexible tool for changing user database information.
# - passwd(1) The command-line tool to change user passwords. # - passwd(1) The command-line tool to change user passwords.
class FreeBSDUser < UnixUser class FreeBSDUser < UnixUser
include ContentParser include PasswdParser
def meta_info(username) def meta_info(username)
cmd = inspec.command("pw usershow #{username} -7") cmd = inspec.command("pw usershow #{username} -7")

View file

@ -1,13 +1,41 @@
# encoding: utf-8 # encoding: utf-8
# Inspired by: http://stackoverflow.com/a/9381776
# author: Dominik Richter # author: Dominik Richter
# author: Christoph Hartmann # author: Christoph Hartmann
class ::Hash class ::Hash
# Inspired by: http://stackoverflow.com/a/9381776
def deep_merge(second) def deep_merge(second)
merger = proc { |_key, v1, v2| merger = proc { |_key, v1, v2|
v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2 v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2
} }
merge(second, &merger) merge(second, &merger)
end end
# converts a deep hash into a flat hash
# hash = {
# 'a' => 1,
# 'b' => {'c' => 2},
# }
# hash.smash # => {"a"=>1, "b-c"=>2}
def smash(prefix = nil)
inject({}) do |acc, (key, value)|
index = prefix.to_s + key.to_s
if value.is_a?(Hash)
acc.merge(value.smash(index + '-'))
else
acc.merge(index => value)
end
end
end
# deep check if all values are contained
def contains(contains)
hash = smash
contains = contains.smash
contains.each do |key, val|
return false if hash[key] != val
end
true
end
end end

View file

@ -2,7 +2,7 @@
# author: Christoph Hartmann # author: Christoph Hartmann
# author: Dominik Richter # author: Dominik Richter
module ContentParser module PasswdParser
# Parse /etc/passwd files. # Parse /etc/passwd files.
# #
# @param [String] content the raw content of /etc/passwd # @param [String] content the raw content of /etc/passwd
@ -29,7 +29,9 @@ module ContentParser
'shell' => x.at(6), 'shell' => x.at(6),
} }
end end
end
module CommentParser
# Parse a line with a command. For example: `a = b # comment`. # Parse a line with a command. For example: `a = b # comment`.
# Retrieves the actual content. # Retrieves the actual content.
# #
@ -59,3 +61,34 @@ module ContentParser
[line, idx_nl] [line, idx_nl]
end end
end end
module MountParser
# this parses the output of mount command (only tested on linux)
# this method expects only one line of the mount output
def parse_mount_options(mount_line, compatibility = false)
mount = mount_line.scan(/\S+/)
# parse device and type
mount_options = { device: mount[0], type: mount[4] }
if compatibility == false
# parse options as array
mount_options[:options] = mount[5].gsub(/\(|\)/, '').split(',')
else
# parse options as serverspec uses it, tbis is deprecated
mount_options[:options] = {}
mount[5].gsub(/\(|\)/, '').split(',').each do |option|
name, val = option.split('=')
if val.nil?
val = true
else
# parse numbers
val = val.to_i if val.match(/^\d+$/)
end
mount_options[:options][name.to_sym] = val
end
end
mount_options
end
end

View file

@ -7,7 +7,7 @@
require 'utils/parser' require 'utils/parser'
class SimpleConfig class SimpleConfig
include ContentParser include CommentParser
attr_reader :params, :groups attr_reader :params, :groups
def initialize(raw_data, opts = {}) def initialize(raw_data, opts = {})

View file

@ -196,7 +196,9 @@ class MockLoader
# apache_conf # apache_conf
'find /etc/apache2/ports.conf -maxdepth 1 -type f' => cmd.call('find-apache2-ports-conf'), 'find /etc/apache2/ports.conf -maxdepth 1 -type f' => cmd.call('find-apache2-ports-conf'),
'find /etc/apache2/conf-enabled/*.conf -maxdepth 1 -type f' => cmd.call('find-apache2-conf-enabled'), 'find /etc/apache2/conf-enabled/*.conf -maxdepth 1 -type f' => cmd.call('find-apache2-conf-enabled'),
# mount
"mount | grep -- ' on /'" => cmd.call("mount"),
"mount | grep -- ' on /mnt/iso-disk'" => cmd.call("mount-multiple"),
} }
@backend @backend

View file

@ -6,6 +6,7 @@
include_recipe('os_prepare::apt') include_recipe('os_prepare::apt')
include_recipe('os_prepare::file') include_recipe('os_prepare::file')
include_recipe('os_prepare::mount')
include_recipe('os_prepare::json_yaml_csv_ini') include_recipe('os_prepare::json_yaml_csv_ini')
include_recipe('os_prepare::package') include_recipe('os_prepare::package')
include_recipe('os_prepare::registry_key') include_recipe('os_prepare::registry_key')

View file

@ -0,0 +1,29 @@
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
#
# file mount tests
# download alpine linux for file mount tests
remote_file '/root/alpine-3.3.0-x86_64.iso' do
source 'http://wiki.alpinelinux.org/cgi-bin/dl.cgi/v3.3/releases/x86_64/alpine-3.3.0-x86_64.iso'
owner 'root'
group 'root'
mode '0755'
action :create
end
# create mount directory
directory '/mnt/iso-disk' do
owner 'root'
group 'root'
mode '0755'
action :create
end
# mount -o loop /root/alpine-3.3.0-x86_64.iso /mnt/iso-disk
mount '/mnt/iso-disk' do
device '/root/alpine-3.3.0-x86_64.iso'
options 'loop'
action [:mount, :enable]
end

View file

@ -108,4 +108,33 @@ if os.unix?
its('type') { should eq :directory } its('type') { should eq :directory }
end end
# for server spec compatibility
# Do not use `.with` or `.only_with`, this syntax is deprecated and will be removed
# in InSpec version 1
describe file('/mnt/iso-disk') do
it { should be_mounted }
it { should be_mounted.with( :type => 'iso9660' ) }
it { should be_mounted.with( :type => 'iso9660', :options => { :ro => true } ) }
it { should be_mounted.with( :type => 'iso9660', :device => '/root/alpine-3.3.0-x86_64.iso' ) }
it { should_not be_mounted.with( :type => 'ext4' ) }
it { should_not be_mounted.with( :type => 'xfs' ) }
end
# compare with exact match
# also see mount_spec.rb
describe file('/mnt/iso-disk') do
it { should be_mounted.only_with( {
:device=>"/root/alpine-3.3.0-x86_64.iso",
:type=>"iso9660",
:options=>{
:ro=>true}
})
}
end
elsif os.windows?
describe file('C:\\Windows') do
it { should exist }
it { should be_directory }
end
end end

View file

@ -0,0 +1,10 @@
# encoding: utf-8
# instead of `.with` or `.only_with` we recommend to use the `mount` resource
describe mount '/mnt/iso-disk' do
it { should be_mounted }
its('count') { should eq 1 }
its('device') { should eq '/root/alpine-3.3.0-x86_64.iso' }
its('type') { should eq 'iso9660' }
its('options') { should eq ['ro'] }
end

1
test/unit/mock/cmd/mount Normal file
View file

@ -0,0 +1 @@
/dev/xvda1 on / type ext4 (rw,discard)

View file

@ -0,0 +1,2 @@
/root/alpine-3.3.0-x86_64.iso on /mnt/iso-disk type iso9660 (ro)
/root/alpine-3.3.0-x86_64_2.iso on /mnt/iso-disk type iso9660 (ro)

View file

@ -1,3 +1,7 @@
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
require 'helper' require 'helper'
require 'inspec/resource' require 'inspec/resource'

View file

@ -0,0 +1,26 @@
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
require 'helper'
require 'inspec/resource'
describe Inspec::Resources::File do
let(:root_resource) { load_resource('mount', '/') }
it 'parses the mount data properly' do
root_resource.send(:device).must_equal('/dev/xvda1')
root_resource.send(:type).must_equal('ext4')
root_resource.send(:options).must_equal(['rw','discard'])
root_resource.send(:count).must_equal(1)
end
let(:iso_resource) { load_resource('mount', '/mnt/iso-disk') }
it 'parses the mount data properly' do
iso_resource.send(:device).must_equal('/root/alpine-3.3.0-x86_64_2.iso')
iso_resource.send(:type).must_equal('iso9660')
iso_resource.send(:options).must_equal(['ro'])
iso_resource.send(:count).must_equal(2)
end
end

View file

@ -2,8 +2,8 @@
# author: Dominik Richter # author: Dominik Richter
# author: Christoph Hartmann # author: Christoph Hartmann
describe ContentParser do describe PasswdParser do
let (:parser) { Class.new() { include ContentParser }.new } let (:parser) { Class.new() { include PasswdParser }.new }
describe '#parse_passwd' do describe '#parse_passwd' do
it 'parses nil content' do it 'parses nil content' do