inspec/lib/resources/service.rb
Stephan Renatus f63a8ad1d5 upstart_service: add version fallback, fix regexp
before this regexp change, a service called "running" (hello integration
tests) would always be "running" ;)
2016-02-05 13:49:18 +01:00

663 lines
19 KiB
Ruby

# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
# author: Stephan Renatus
# license: All rights reserved
# Usage:
# describe service('dhcp') do
# it { should be_enabled }
# it { should be_installed }
# it { should be_running }
# 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
"
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 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
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 ||= '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 ||= '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
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 ||= Gem::Version.new(inspec.command("#{service_ctl} --version")
.stdout.match(/\(upstart ([^\)]+)\)/)[1])
end
end
class SysV < ServiceManager
def initialize(service_name, service_ctl = nil)
@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*')
enabled_services = enabled_services_cmd.stdout.split("\n").select { |line|
/(^.*#{service_name}.*)/.match(line)
}
enabled = !enabled_services.empty?
# 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,
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'
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 ||= '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 ||= '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 ||= '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