inspec/lib/resources/service.rb
Adam Leff 577688a3a0 Placing all resources in the Inspec::Resources namespace
Many of the resources are named as a top-level class with a fairly generic class name, such as "OS". This causes an issue specifically with kitchen-google which depends on a gem which depends on the "os" gem which itself defines an OS class with a different superclass. This prevents users from using TK, Google Compute, and Inspec without this fix.

Some mocked commands had their digest changed as well due to the new indentation, specifically in the User and RegistryKey classes.

I strongly recommend viewing this diff with `git diff --ignore-space-change`
to see the *real* changes. :)
2016-03-08 13:40:16 -05:00

733 lines
22 KiB
Ruby

# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
# author: Stephan Renatus
# license: All rights reserved
module Inspec::Resources
class Runlevels < Hash
attr_accessor :owner
def self.from_hash(owner, hash = {}, filter = nil)
res = Runlevels.new(owner)
filter = filter.first if filter.is_a?(Array) && filter.length <= 1
ks = case filter
when nil
hash.keys
when Regexp
hash.keys.find_all { |x| x.to_s =~ filter }
when Array
f = filter.map(&:to_s)
hash.keys.find_all { |x| f.include?(x.to_s) }
when Numeric
hash.keys.include?(filter) ? [filter] : []
else
hash.keys.find_all { |x| x == filter }
end
ks.each { |k| res[k] = hash[k] }
res
end
def initialize(owner, default = false)
@owner = owner
super(default)
end
def filter(f)
Runlevels.from_hash(owner, self, f)
end
# Check if all runlevels are enabled
#
# @return [boolean] true if all runlevels are enabled
def enabled?
values.all?
end
# Check if all runlevels are disabled
#
# @return [boolean] true if all runlevels are disabled
def disabled?
!values.any?
end
def to_s
"#{owner} runlevels #{keys.join(', ')}"
end
end
# We detect the init system for each operating system, based on the operating
# system.
#
# Fedora 15 : systemd
# RedHat 7 : systemd
# Ubuntu 15.04 : systemd
# Ubuntu < 15.04 : upstart
#
# TODO: extend the logic to detect the running init system, independently of OS
class Service < Inspec.resource(1)
name 'service'
desc 'Use the service InSpec audit resource to test if the named service is installed, running and/or enabled.'
example "
describe service('service_name') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
describe service('service_name').runlevels(3, 5) do
it { should be_enabled }
end
"
attr_reader :service_ctl
def initialize(service_name, service_ctl = nil)
@service_name = service_name
@service_mgmt = nil
@service_ctl ||= service_ctl
@cache = nil
@service_mgmt = select_service_mgmt
return skip_resource 'The `service` resource is not supported on your OS yet.' if @service_mgmt.nil?
end
def select_service_mgmt # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
os = inspec.os
family = os[:family]
# Ubuntu
# @see: https://wiki.ubuntu.com/SystemdForUpstartUsers
# Ubuntu 15.04 : Systemd
# Systemd runs with PID 1 as /sbin/init.
# Upstart runs with PID 1 as /sbin/upstart.
# Ubuntu < 15.04 : Upstart
# Upstart runs with PID 1 as /sbin/init.
# Systemd runs with PID 1 as /lib/systemd/systemd.
if %w{ubuntu}.include?(family)
version = inspec.os[:release].to_f
if version < 15.04
Upstart.new(inspec, service_ctl)
else
Systemd.new(inspec, service_ctl)
end
elsif %w{debian}.include?(family)
version = inspec.os[:release].to_i
if version > 7
Systemd.new(inspec, service_ctl)
else
SysV.new(inspec, service_ctl || '/usr/sbin/service')
end
elsif %w{redhat fedora centos}.include?(family)
version = inspec.os[:release].to_i
if (%w{ redhat centos }.include?(family) && version >= 7) || (family == 'fedora' && version >= 15)
Systemd.new(inspec, service_ctl)
else
SysV.new(inspec, service_ctl || '/sbin/service')
end
elsif %w{wrlinux}.include?(family)
SysV.new(inspec, service_ctl)
elsif %w{darwin}.include?(family)
LaunchCtl.new(inspec, service_ctl)
elsif os.windows?
WindowsSrv.new(inspec)
elsif %w{freebsd}.include?(family)
BSDInit.new(inspec, service_ctl)
elsif %w{arch opensuse}.include?(family)
Systemd.new(inspec, service_ctl)
elsif %w{aix}.include?(family)
SrcMstr.new(inspec)
elsif os.solaris?
Svcs.new(inspec)
end
end
def info
return nil if @service_mgmt.nil?
@cache ||= @service_mgmt.info(@service_name)
end
# verifies if the service is enabled
def enabled?(_level = nil)
return false if info.nil?
info[:enabled]
end
# verifies the service is registered
def installed?(_name = nil, _version = nil)
return false if info.nil?
info[:installed]
end
# verifies the service is currently running
def running?(_under = nil)
return false if info.nil?
info[:running]
end
# get all runlevels that are available and their configuration
def runlevels(*args)
return Runlevels.new(self) if info.nil? or info[:runlevels].nil?
Runlevels.from_hash(self, info[:runlevels], args)
end
def to_s
"Service #{@service_name}"
end
end
class ServiceManager
attr_reader :inspec, :service_ctl
def initialize(inspec, service_ctl = nil)
@inspec = inspec
@service_ctl ||= service_ctl
end
end
# @see: http://www.freedesktop.org/software/systemd/man/systemctl.html
# @see: http://www.freedesktop.org/software/systemd/man/systemd-system.conf.html
class Systemd < ServiceManager
def initialize(inspec, service_ctl = nil)
@service_ctl = service_ctl || 'systemctl'
super
end
def info(service_name)
cmd = inspec.command("#{service_ctl} show --all #{service_name}")
return nil if cmd.exit_status.to_i != 0
# parse data
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/,
multiple_values: false,
).params
# LoadState values eg. loaded, not-found
installed = params['LoadState'] == 'loaded'
# test via 'systemctl is-active service'
# SubState values running
running = params['SubState'] == 'running'
# test via systemctl --quiet is-enabled
# ActiveState values eg.g inactive, active
enabled = params['UnitFileState'] == 'enabled'
{
name: params['Id'],
description: params['Description'],
installed: installed,
running: running,
enabled: enabled,
type: 'systemd',
}
end
end
# AIX services
class SrcMstr < ServiceManager
attr_reader :name
def info(service_name)
@name = service_name
running = status?
return nil if running.nil?
{
name: service_name,
description: nil,
installed: true,
running: running,
enabled: enabled?,
type: 'srcmstr',
}
end
private
def status?
status_cmd = inspec.command("lssrc -s #{@name}")
return nil if status_cmd.exit_status.to_i != 0
status_cmd.stdout.split(/\n/).last.chomp =~ /active$/ ? true : false
end
def enabled?
enabled_rc_tcpip? || enabled_inittab?
end
# #rubocop:disable Style/TrailingComma
def enabled_rc_tcpip?
inspec.command(
"grep -v ^# /etc/rc.tcpip | grep 'start ' | grep -Eq '(/{0,1}| )#{name} '",
).exit_status == 0
end
def enabled_inittab?
inspec.command("lsitab #{name}").exit_status == 0
end
end
# @see: http://upstart.ubuntu.com
class Upstart < ServiceManager
def initialize(service_name, service_ctl = nil)
@service_ctl = service_ctl || 'initctl'
super
end
def info(service_name)
# get the status of upstart service
status = inspec.command("#{service_ctl} status #{service_name}")
# fallback for systemv services, those are not handled via `initctl`
return SysV.new(inspec).info(service_name) if status.exit_status.to_i != 0
# @see: http://upstart.ubuntu.com/cookbook/#job-states
# grep for running to indicate the service is there
running = !status.stdout[%r{start/running}].nil?
{
name: service_name,
description: nil,
installed: true,
running: running,
enabled: info_enabled(status, service_name),
type: 'upstart',
}
end
private
def info_enabled(status, service_name)
# check if a service is enabled
# http://upstart.ubuntu.com/cookbook/#determine-if-a-job-is-disabled
# $ initctl show-config $job | grep -q "^ start on" && echo enabled || echo disabled
# Ubuntu 10.04 show-config is not supported
# @see http://manpages.ubuntu.com/manpages/maverick/man8/initctl.8.html
support_for_show_config = Gem::Version.new('1.3')
if version >= support_for_show_config
config = inspec.command("#{service_ctl} show-config #{service_name}").stdout
else # use config file as fallback
config = inspec.file("/etc/init/#{service_name}.conf").content
end
# disregard if the config does not exist
return nil if config.nil?
enabled = !config[/^\s*start on/].nil?
# implement fallback for Ubuntu 10.04
if inspec.os[:family] == 'ubuntu' &&
inspec.os[:release].to_f >= 10.04 &&
inspec.os[:release].to_f < 12.04 &&
status.exit_status == 0
enabled = true
end
enabled
end
def version
@version ||= (
out = inspec.command("#{service_ctl} --version").stdout
Gem::Version.new(out[/\(upstart ([^\)]+)\)/, 1])
)
end
end
class SysV < ServiceManager
RUNLEVELS = { 0=>false, 1=>false, 2=>false, 3=>false, 4=>false, 5=>false, 6=>false }.freeze
def initialize(service_name, service_ctl = nil)
@service_ctl = service_ctl || 'service'
super
end
def info(service_name)
# check if service is installed
# read all available services via ls /etc/init.d/
srvlist = inspec.command('ls -1 /etc/init.d/')
return nil if srvlist.exit_status != 0
# check if the service is in list
service = srvlist.stdout.split("\n").select { |srv| srv == service_name }
# abort if we could not find any service
return nil if service.empty?
# read all enabled services from runlevel
# on rhel via: 'chkconfig --list', is not installed by default
# bash: for i in `find /etc/rc*.d -name S*`; do basename $i | sed -r 's/^S[0-9]+//'; done | sort | uniq
enabled_services_cmd = inspec.command('find /etc/rc*.d -name S*')
service_line = %r{rc(?<runlevel>[0-6])\.d/S[^/]*?#{Regexp.escape service_name}$}
all_services = enabled_services_cmd.stdout.split("\n").map { |line|
service_line.match(line)
}.compact
enabled = !all_services.empty?
# Determine a list of runlevels which this service is activated for
runlevels = RUNLEVELS.dup
all_services.each { |x| runlevels[x[:runlevel].to_i] = true }
# check if service is really running
# service throws an exit code if the service is not installed or
# not enabled
cmd = inspec.command("#{service_ctl} #{service_name} status")
running = cmd.exit_status == 0
{
name: service_name,
description: nil,
installed: true,
running: running,
enabled: enabled,
runlevels: runlevels,
type: 'sysv',
}
end
end
# @see: https://www.freebsd.org/doc/en/articles/linux-users/startup.html
# @see: https://www.freebsd.org/cgi/man.cgi?query=rc.conf&sektion=5
class BSDInit < ServiceManager
def initialize(service_name, service_ctl = nil)
@service_ctl = service_ctl || 'service'
super
end
def info(service_name)
# check if service is enabled
# services are enabled in /etc/rc.conf and /etc/defaults/rc.conf
# via #{service_name}_enable="YES"
# service SERVICE status returns the following result if not activated:
# Cannot 'status' sshd. Set sshd_enable to YES in /etc/rc.conf or use 'onestatus' instead of 'status'.
# gather all enabled services
cmd = inspec.command("#{service_ctl} -e")
return nil if cmd.exit_status != 0
# search for the service
srv = /(^.*#{service_name}$)/.match(cmd.stdout)
return nil if srv.nil? || srv[0].nil?
enabled = true
# check if the service is running
# if the service is not available or not running, we always get an error code
cmd = inspec.command("#{service_ctl} #{service_name} onestatus")
running = cmd.exit_status == 0
{
name: service_name,
description: nil,
installed: true,
running: running,
enabled: enabled,
type: 'bsd-init',
}
end
end
class Runit < ServiceManager
def initialize(service_name, service_ctl = nil)
@service_ctl = service_ctl || 'sv'
super
end
# rubocop:disable Style/DoubleNegation
def info(service_name)
# get the status of runit service
cmd = inspec.command("#{service_ctl} status #{service_name}")
# return nil unless cmd.exit_status == 0 # NOTE(sr) why do we do this?
installed = cmd.exit_status == 0
running = installed && !!(cmd.stdout =~ /^run:/)
enabled = installed && (running || !!(cmd.stdout =~ /normally up/) || !!(cmd.stdout =~ /want up/))
{
name: service_name,
description: nil,
installed: installed,
running: running,
enabled: enabled,
type: 'runit',
}
end
end
# MacOS / Darwin
# new launctl on macos 10.10
class LaunchCtl < ServiceManager
def initialize(service_name, service_ctl = nil)
@service_ctl = service_ctl || 'launchctl'
super
end
def info(service_name)
# get the status of upstart service
cmd = inspec.command("#{service_ctl} list")
return nil if cmd.exit_status != 0
# search for the service
srv = /(^.*#{service_name}.*)/.match(cmd.stdout)
return nil if srv.nil? || srv[0].nil?
# extract values from service
parsed_srv = /^(?<pid>[0-9-]+)\t(?<exit>[0-9]+)\t(?<name>\S*)$/.match(srv[0])
enabled = !parsed_srv['name'].nil? # it's in the list
# check if the service is running
pid = parsed_srv['pid']
running = pid != '-'
# extract service label
srv = parsed_srv['name'] || service_name
{
name: srv,
description: nil,
installed: true,
running: running,
enabled: enabled,
type: 'darwin',
}
end
end
# Determine the service state from Windows
# Uses Powershell to retrieve the information
class WindowsSrv < ServiceManager
# Determine service details
# PS: Get-Service -Name 'dhcp'| Select-Object -Property Name, DisplayName, Status | ConvertTo-Json
# {
# "Name": "dhcp",
# "DisplayName": "DHCP Client",
# "Status": 4
# }
#
# Until StartMode is not added to Get-Service, we need to do a workaround
# @see: https://connect.microsoft.com/PowerShell/feedback/details/424948/i-would-like-to-see-the-property-starttype-added-to-get-services
# Use the following powershell to determine the start mode
# PS: Get-WmiObject -Class Win32_Service | Where-Object {$_.Name -eq $name -or $_.DisplayName -eq $name} | Select-Object -Prop
# erty Name, StartMode, State, Status | ConvertTo-Json
# {
# "Name": "Dhcp",
# "StartMode": "Auto",
# "State": "Running",
# "Status": "OK"
# }
#
# Windows Services have the following status code:
# @see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms685996(v=vs.85).aspx
# - 1: Stopped
# - 2: Starting
# - 3: Stopping
# - 4: Running
# - 5: Continue Pending
# - 6: Pause Pending
# - 7: Paused
def info(service_name)
cmd = inspec.command("New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name Service -Value (Get-Service -Name #{service_name}| Select-Object -Property Name, DisplayName, Status) -PassThru | Add-Member -MemberType NoteProperty -Name WMI -Value (Get-WmiObject -Class Win32_Service | Where-Object {$_.Name -eq '#{service_name}' -or $_.DisplayName -eq '#{service_name}'} | Select-Object -Property StartMode) -PassThru | ConvertTo-Json")
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0
# try to parse json
begin
service = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
# check that we got a response
return nil if service.nil? || service['Service'].nil?
{
name: service['Service']['Name'],
description: service['Service']['DisplayName'],
installed: true,
running: service_running?(service),
enabled: service_enabled?(service),
type: 'windows',
}
end
private
# detect if service is enabled
def service_enabled?(service)
!service['WMI'].nil? &&
!service['WMI']['StartMode'].nil? &&
service['WMI']['StartMode'] == 'Auto'
end
# detect if service is running
def service_running?(service)
!service['Service']['Status'].nil? && service['Service']['Status'] == 4
end
end
# Solaris services
class Svcs < ServiceManager
def initialize(service_name, service_ctl = nil)
@service_ctl = service_ctl || 'svcs'
super
end
def info(service_name)
# get the status of runit service
cmd = inspec.command("#{service_ctl} -l #{service_name}")
return nil if cmd.exit_status != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^(\w+)\s*(.*)$/,
multiple_values: false,
).params
installed = cmd.exit_status == 0
running = installed && (params['state'] == 'online')
enabled = installed && (params['enabled'] == 'true')
{
name: service_name,
description: params['name'],
installed: installed,
running: running,
enabled: enabled,
type: 'svcs',
}
end
end
# specific resources for specific service managers
class SystemdService < Service
name 'systemd_service'
desc 'Use the systemd_service InSpec audit resource to test if the named service (controlled by systemd) is installed, running and/or enabled.'
example "
# to override service mgmt auto-detection
describe systemd_service('service_name') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
# to set a non-standard systemctl path
describe systemd_service('service_name', '/path/to/systemctl') do
it { should be_running }
end
"
def select_service_mgmt
Systemd.new(inspec, service_ctl)
end
end
class UpstartService < Service
name 'upstart_service'
desc 'Use the upstart_service InSpec audit resource to test if the named service (controlled by upstart) is installed, running and/or enabled.'
example "
# to override service mgmt auto-detection
describe upstart_service('service_name') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
# to set a non-standard initctl path
describe upstart_service('service_name', '/path/to/initctl') do
it { should be_running }
end
"
def select_service_mgmt
Upstart.new(inspec, service_ctl)
end
end
class SysVService < Service
name 'sysv_service'
desc 'Use the sysv_service InSpec audit resource to test if the named service (controlled by SysV) is installed, running and/or enabled.'
example "
# to override service mgmt auto-detection
describe sysv_service('service_name') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
# to set a non-standard service path
describe sysv_service('service_name', '/path/to/service') do
it { should be_running }
end
"
def select_service_mgmt
SysV.new(inspec, service_ctl)
end
end
class BSDService < Service
name 'bsd_service'
desc 'Use the bsd_service InSpec audit resource to test if the named service (controlled by BSD init) is installed, running and/or enabled.'
example "
# to override service mgmt auto-detection
describe bsd_service('service_name') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
# to set a non-standard service path
describe bsd_service('service_name', '/path/to/service') do
it { should be_running }
end
"
def select_service_mgmt
BSDInit.new(inspec, service_ctl)
end
end
class LaunchdService < Service
name 'launchd_service'
desc 'Use the launchd_service InSpec audit resource to test if the named service (controlled by launchd) is installed, running and/or enabled.'
example "
# to override service mgmt auto-detection
describe launchd_service('service_name') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
# to set a non-standard launchctl path
describe launchd_service('service_name', '/path/to/launchctl') do
it { should be_running }
end
"
def select_service_mgmt
LaunchCtl.new(inspec, service_ctl)
end
end
class RunitService < Service
name 'runit_service'
desc 'Use the runit_service InSpec audit resource to test if the named service (controlled by runit) is installed, running and/or enabled.'
example "
# to override service mgmt auto-detection
describe runit_service('service_name') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
# to set a non-standard sv path
describe runit_service('service_name', '/path/to/sv') do
it { should be_running }
end
"
def select_service_mgmt
Runit.new(inspec, service_ctl)
end
end
end