diff --git a/docs/resources/virtualization.md.erb b/docs/resources/virtualization.md.erb new file mode 100644 index 0000000000..c804aa12f0 --- /dev/null +++ b/docs/resources/virtualization.md.erb @@ -0,0 +1,71 @@ +--- +title: About the virtualization Resource +--- + +# virtualization + +Use the `virtualization` InSpec audit resource to test the virtualization platform on which the system is running. + +## Syntax + +An `virtualization` resource block declares the virtualization platform that should be tested: + + describe virtualization do + its('system') { should MATCHER 'value' } + end + +where + +* `('system')` is the name of the system information of the virtualization platform (e.g. docker, lxc, vbox, kvm, etc) +* `MATCHER` is a valid matcher for this resource +* `'value'` is the value to be tested + +## Matchers + +This InSpec audit resource has the following matchers: + +### be + +<%= partial "/shared/matcher_be" %> + +### cmp + +<%= partial "/shared/matcher_cmp" %> + +### eq + +<%= partial "/shared/matcher_eq" %> + +### include + +<%= partial "/shared/matcher_include" %> + +### match + +<%= partial "/shared/matcher_match" %> + +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test for Docker + + describe virtualization do + its('system') { should eq 'docker' } + end + +### Test for VirtualBox + + describe virtualization do + its('system') { should eq 'vbox' } + its('role') { should eq 'guest' } + end + +### Detect the virtualization platform + + if virtualization.system == 'vbox' + describe package('name') do + it { should be_installed } + end + end + diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index 7dd16f54a8..5347c91d99 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -137,6 +137,7 @@ def self.validate_resource_dsl_version!(version) require 'resources/sys_info' require 'resources/users' require 'resources/vbscript' +require 'resources/virtualization' require 'resources/windows_feature' require 'resources/windows_task' require 'resources/xinetd' diff --git a/lib/resources/virtualization.rb b/lib/resources/virtualization.rb new file mode 100644 index 0000000000..e4da689ae8 --- /dev/null +++ b/lib/resources/virtualization.rb @@ -0,0 +1,252 @@ +# encoding: utf-8 +# author: Takaaki Furukawa + +require 'hashie/mash' + +module Inspec::Resources + class Virtualization < Inspec.resource(1) # rubocop:disable Metrics/ClassLength + name 'virtualization' + desc 'Use the virtualization InSpec audit resource to test the virtualization platform on which the system is running' + example " + describe virtualization do + its('system') { should eq 'docker' } + end + + describe virtualization do + its('role') { should eq 'guest' } + end + + control 'test' do + describe file('/var/tmp/foo') do + it { should be_file } + end + only_if { virtualization.system == 'docker' } + end + " + + def initialize + unless inspec.os.linux? + skip_resource 'The `virtualization` resource is not supported on your OS yet.' + else + collect_data_linux + end + end + + # add helper methods for easy access of properties + # allows users to use virtualization.role, virtualization.system + %w{role system}.each do |property| + define_method(property.to_sym) do + @virtualization_data[property.to_sym] + end + end + + def params + collect_data_linux + end + + def to_s + 'Virtualization Detection' + end + + private + + def lxc_version_exists? + inspec.command('lxc-version').exist? + end + + def docker_exists? + inspec.command('docker').exist? + end + + def nova_exists? + inspec.command('nova').exist? + end + + # Detect Xen + # /proc/xen is an empty dir for EL6 + Linode Guests + Paravirt EC2 instances + # Notes: + # - cpuid of guests, if we could get it, would also be a clue + # - may be able to determine if under paravirt from /dev/xen/evtchn (See OHAI-253) + # - Additional edge cases likely should not change the above assumptions + # but rather be additive - btm + def detect_xen + return false unless inspec.file('/proc/xen').exist? + @virtualization_data[:system] = 'xen' + @virtualization_data[:role] = 'guest' + + # This file should exist on most Xen systems, normally empty for guests + if inspec.file('/proc/xen/capabilities').exist? && + inspec.file('/proc/xen/capabilities').content =~ /control_d/i # rubocop:disable Style/MultilineOperationIndentation + @virtualization_data[:role] = 'host' + end + true + end + + # Detect Virtualbox from kernel module + def detect_virtualbox + return false unless inspec.file('/proc/modules').exist? + modules = inspec.file('/proc/modules').content + if modules =~ /^vboxdrv/ + Inspec::Log.debug('Plugin Virtualization: /proc/modules contains vboxdrv. Detecting as vbox host') + @virtualization_data[:system] = 'vbox' + @virtualization_data[:role] = 'host' + elsif modules =~ /^vboxguest/ + Inspec::Log.debug('Plugin Virtualization: /proc/modules contains vboxguest. Detecting as vbox guest') + @virtualization_data[:system] = 'vbox' + @virtualization_data[:role] = 'guest' + else + return false + end + true + end + + # if nova binary is present we're on an openstack host + def detect_openstack + return false unless nova_exists? + @virtualization_data[:system] = 'openstack' + @virtualization_data[:role] = 'host' + true + end + + # Detect paravirt KVM/QEMU from cpuinfo, report as KVM + def detect_kvm_from_cpuinfo + return false unless inspec.file('/proc/cpuinfo').content =~ /QEMU Virtual CPU|Common KVM processor|Common 32-bit KVM processor/ + @virtualization_data[:system] = 'kvm' + @virtualization_data[:role] = 'guest' + true + end + + # Detect KVM systems via /sys + # guests will have the hypervisor cpu feature that hosts don't have + def detect_kvm_from_sys + return false unless inspec.file('/sys/devices/virtual/misc/kvm').exist? + if inspec.file('/proc/cpuinfo').content =~ /hypervisor/ + @virtualization_data[:system] = 'kvm' + @virtualization_data[:role] = 'guest' + else + @virtualization_data[:system] = 'kvm' + @virtualization_data[:role] = 'host' + end + true + end + + # Detect OpenVZ / Virtuozzo. + # http://wiki.openvz.org/BC_proc_entries + def detect_openvz + if inspec.file('/proc/bc/0').exist? + @virtualization_data[:system] = 'openvz' + @virtualization_data[:role] = 'host' + elsif inspec.file('/proc/vz').exist? + @virtualization_data[:system] = 'openvz' + @virtualization_data[:role] = 'guest' + else + return false + end + true + end + + # Detect Parallels virtual machine from pci devices + def detect_parallels + return false unless inspec.file('/proc/bus/pci/devices').content =~ /1ab84000/ + @virtualization_data[:system] = 'parallels' + @virtualization_data[:role] = 'guest' + true + end + + # Detect Linux-VServer + def detect_linux_vserver + return false unless inspec.file('/proc/self/status').exist? + proc_self_status = inspec.file('/proc/self/status').content + vxid = proc_self_status.match(/^(s_context|VxID):\s*(\d+)$/) + return false unless vxid && vxid[2] + @virtualization_data[:system] = 'linux-vserver' + if vxid[2] == '0' + @virtualization_data[:role] = 'host' + else + @virtualization_data[:role] = 'guest' + end + true + end + + # Detect LXC/Docker + # + # /proc/self/cgroup will look like this inside a docker container: + # ::/lxc/ + # + # /proc/self/cgroup could have a name including alpha/digit/dashes + # ::/lxc/ + # + # /proc/self/cgroup could have a non-lxc cgroup name indicating other uses + # of cgroups. This is probably not LXC/Docker. + # ::/Charlie + # + # A host which supports cgroups, and has capacity to host lxc containers, + # will show the subsystems and root (/) namespace. + # ::/ + # + # Full notes, https://tickets.opscode.com/browse/OHAI-551 + # Kernel docs, https://www.kernel.org/doc/Documentation/cgroups + def detect_lxc_docker + return false unless inspec.file('/proc/self/cgroup').exist? + cgroup_content = inspec.file('/proc/self/cgroup').content + if cgroup_content =~ %r{^\d+:[^:]+:/(lxc|docker)/.+$} || + cgroup_content =~ %r{^\d+:[^:]+:/[^/]+/(lxc|docker)-.+$} # rubocop:disable Style/MultilineOperationIndentation + @virtualization_data[:system] = $1 # rubocop:disable Style/PerlBackrefs + @virtualization_data[:role] = 'guest' + elsif lxc_version_exists? && cgroup_content =~ %r{\d:[^:]+:/$} + # lxc-version shouldn't be installed by default + # Even so, it is likely we are on an LXC capable host that is not being used as such + # So we're cautious here to not overwrite other existing values (OHAI-573) + unless @virtualization_data[:system] && @virtualization_data[:role] + @virtualization_data[:system] = 'lxc' + @virtualization_data[:role] = 'host' + end + else + return false + end + true + end + + def detect_docker + return false unless inspec.file('/.dockerenv').exist? || inspec.file('/.dockerinit').exist? + @virtualization_data[:system] = 'docker' + @virtualization_data[:role] = 'guest' + true + end + + # Detect LXD + # See https://github.com/lxc/lxd/blob/master/doc/dev-lxd.md + def detect_lxd + if inspec.file('/dev/lxd/sock').exist? + @virtualization_data[:system] = 'lxd' + @virtualization_data[:role] = 'guest' + elsif inspec.file('/var/lib/lxd/devlxd').exist? + @virtualization_data[:system] = 'lxd' + @virtualization_data[:role] = 'host' + else + return false + end + true + end + + def collect_data_linux # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity + # cache data in an instance var to avoid doing multiple detections for a single test + @virtualization_data ||= Hashie::Mash.new + return unless @virtualization_data.empty? + + # each detect method will return true if it matched and was successfully + # able to populate @virtualization_data with stuff. + return if detect_xen + return if detect_virtualbox + return if detect_openstack + return if detect_kvm_from_cpuinfo + return if detect_kvm_from_sys + return if detect_openvz + return if detect_parallels + return if detect_linux_vserver + return if detect_lxc_docker + return if detect_docker + return if detect_lxd + end + end +end diff --git a/test/integration/default/virtualization_spec.rb b/test/integration/default/virtualization_spec.rb new file mode 100644 index 0000000000..24a6ed7f93 --- /dev/null +++ b/test/integration/default/virtualization_spec.rb @@ -0,0 +1,7 @@ +# encoding: utf-8 +if ENV['DOCKER'] && os.linux? + describe virtualization do + its('system') { should eq 'docker' } + its('role') { should eq 'guest' } + end +end