diff options
-rw-r--r-- | chef.gemspec | 1 | ||||
-rw-r--r-- | lib/chef/provider/systemd_unit.rb | 210 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/systemd_unit.rb | 61 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/systemd_unit_spec.rb | 797 | ||||
-rw-r--r-- | spec/unit/resource/systemd_unit_spec.rb | 133 |
7 files changed, 1204 insertions, 0 deletions
diff --git a/chef.gemspec b/chef.gemspec index 59367b00f8..b88c899d5c 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -34,6 +34,7 @@ Gem::Specification.new do |s| s.add_dependency "chef-zero", "~> 4.5" s.add_dependency "plist", "~> 3.2" + s.add_dependency "iniparse", "~> 1.4" # Audit mode requires these, so they are non-developmental dependencies now %w{rspec-core rspec-expectations rspec-mocks}.each { |gem| s.add_dependency gem, "~> 3.4" } diff --git a/lib/chef/provider/systemd_unit.rb b/lib/chef/provider/systemd_unit.rb new file mode 100644 index 0000000000..db71a6c234 --- /dev/null +++ b/lib/chef/provider/systemd_unit.rb @@ -0,0 +1,210 @@ +# +# Author:: Nathan Williams (<nath.e.will@gmail.com>) +# Copyright:: Copyright 2016, Nathan Williams +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/provider" +require "chef/mixin/which" +require "chef/mixin/shell_out" +require "chef/resource/file" + +class Chef + class Provider + class SystemdUnit < Chef::Provider + include Chef::Mixin::Which + include Chef::Mixin::ShellOut + + provides :systemd_unit, os: "linux" + + def load_current_resource + @current_resource = Chef::Resource::SystemdUnit.new(new_resource.name) + + current_resource.content(::File.read(unit_path)) if ::File.exist?(unit_path) + current_resource.user(new_resource.user) + current_resource.enabled(enabled?) + current_resource.active(active?) + current_resource.masked(masked?) + current_resource.static(static?) + current_resource.triggers_reload(new_resource.triggers_reload) + + current_resource + end + + def action_create + if current_resource.content != new_resource.to_ini + converge_by("creating unit: #{new_resource.name}") do + manage_unit_file(:create) + daemon_reload if new_resource.triggers_reload + end + end + end + + def action_delete + if ::File.exist?(unit_path) + converge_by("deleting unit: #{new_resource.name}") do + manage_unit_file(:delete) + daemon_reload if new_resource.triggers_reload + end + end + end + + def action_enable + if current_resource.static + Chef::Log.debug("#{new_resource.name} is a static unit, enabling is a NOP.") + end + + unless current_resource.enabled || current_resource.static + converge_by("enabling unit: #{new_resource.name}") do + systemctl_execute!(:enable, new_resource.name) + end + end + end + + def action_disable + if current_resource.static + Chef::Log.debug("#{new_resource.name} is a static unit, disabling is a NOP.") + end + + if current_resource.enabled && !current_resource.static + converge_by("disabling unit: #{new_resource.name}") do + systemctl_execute!(:disable, new_resource.name) + end + end + end + + def action_mask + unless current_resource.masked + converge_by("masking unit: #{new_resource.name}") do + systemctl_execute!(:mask, new_resource.name) + end + end + end + + def action_unmask + if current_resource.masked + converge_by("unmasking unit: #{new_resource.name}") do + systemctl_execute!(:unmask, new_resource.name) + end + end + end + + def action_start + unless current_resource.active + converge_by("starting unit: #{new_resource.name}") do + systemctl_execute!(:start, new_resource.name) + end + end + end + + def action_stop + if current_resource.active + converge_by("stopping unit: #{new_resource.name}") do + systemctl_execute!(:stop, new_resource.name) + end + end + end + + def action_restart + converge_by("restarting unit: #{new_resource.name}") do + systemctl_execute!(:restart, new_resource.name) + end + end + + def action_reload + if current_resource.active + converge_by("reloading unit: #{new_resource.name}") do + systemctl_execute!(:reload, new_resource.name) + end + else + Chef::Log.debug("#{new_resource.name} is not active, skipping reload.") + end + end + + def active? + systemctl_execute("is-active", new_resource.name).exitstatus == 0 + end + + def enabled? + systemctl_execute("is-enabled", new_resource.name).exitstatus == 0 + end + + def masked? + systemctl_execute(:status, new_resource.name).stdout.include?("masked") + end + + def static? + systemctl_execute("is-enabled", new_resource.name).stdout.include?("static") + end + + private + + def unit_path + if new_resource.user + "/etc/systemd/user/#{new_resource.name}" + else + "/etc/systemd/system/#{new_resource.name}" + end + end + + def manage_unit_file(action = :nothing) + Chef::Resource::File.new(unit_path, run_context).tap do |f| + f.owner "root" + f.group "root" + f.mode "0644" + f.content new_resource.to_ini + end.run_action(action) + end + + def daemon_reload + shell_out_with_systems_locale!("#{systemctl_path} daemon-reload") + end + + def systemctl_execute!(action, unit) + shell_out_with_systems_locale!("#{systemctl_cmd} #{action} #{unit}", systemctl_opts) + end + + def systemctl_execute(action, unit) + shell_out("#{systemctl_cmd} #{action} #{unit}", systemctl_opts) + end + + def systemctl_cmd + @systemctl_cmd ||= "#{systemctl_path} #{systemctl_args}" + end + + def systemctl_path + @systemctl_path ||= which("systemctl") + end + + def systemctl_args + @systemctl_args ||= new_resource.user ? "--user" : "--system" + end + + def systemctl_opts + @systemctl_opts ||= + if new_resource.user + { + "user" => new_resource.user, + "environment" => { + "DBUS_SESSION_BUS_ADDRESS" => "unix:path=/run/user/#{node['etc']['passwd'][new_resource.user]['uid']}/bus", + }, + } + else + {} + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index a8a058158c..14c47df939 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -54,6 +54,7 @@ require "chef/provider/ruby_block" require "chef/provider/script" require "chef/provider/service" require "chef/provider/subversion" +require "chef/provider/systemd_unit" require "chef/provider/template" require "chef/provider/user" require "chef/provider/whyrun_safe_ruby_block" diff --git a/lib/chef/resource/systemd_unit.rb b/lib/chef/resource/systemd_unit.rb new file mode 100644 index 0000000000..525e9ab35e --- /dev/null +++ b/lib/chef/resource/systemd_unit.rb @@ -0,0 +1,61 @@ +# +# Author:: Nathan Williams (<nath.e.will@gmail.com>) +# Copyright:: Copyright 2016, Nathan Williams +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" +require "iniparse" + +class Chef + class Resource + class SystemdUnit < Chef::Resource + resource_name :systemd_unit + + default_action :nothing + allowed_actions :create, :delete, + :enable, :disable, + :mask, :unmask, + :start, :stop, + :restart, :reload + + property :enabled, [TrueClass, FalseClass] + property :active, [TrueClass, FalseClass] + property :masked, [TrueClass, FalseClass] + property :static, [TrueClass, FalseClass] + property :user, String, desired_state: false + property :content, [String, Hash] + property :triggers_reload, [TrueClass, FalseClass], + default: true, desired_state: false + + def to_ini + case content + when Hash + IniParse.gen do |doc| + content.each_pair do |sect, opts| + doc.section(sect) do |section| + opts.each_pair do |opt, val| + section.option(opt, val) + end + end + end + end.to_s + else + content.to_s + end + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index d8cec8c51d..af9c918f55 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -75,6 +75,7 @@ require "chef/resource/ruby_block" require "chef/resource/scm" require "chef/resource/script" require "chef/resource/service" +require "chef/resource/systemd_unit" require "chef/resource/windows_service" require "chef/resource/subversion" require "chef/resource/smartos_package" diff --git a/spec/unit/provider/systemd_unit_spec.rb b/spec/unit/provider/systemd_unit_spec.rb new file mode 100644 index 0000000000..e71885f32d --- /dev/null +++ b/spec/unit/provider/systemd_unit_spec.rb @@ -0,0 +1,797 @@ +# +# Author:: Nathan Williams (<nath.e.will@gmail.com>) +# Copyright:: Copyright (c), Nathan Williams +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" + +describe Chef::Provider::SystemdUnit do + let(:node) do + Chef::Node.new.tap do |n| + n.default["etc"] = {} + n.default["etc"]["passwd"] = { + "joe" => { + "uid" => 1_000, + }, + } + end + end + + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:unit_name) { "sysstat-collect.timer" } + let(:user_name) { "joe" } + let(:current_resource) { Chef::Resource::SystemdUnit.new(unit_name) } + let(:new_resource) { Chef::Resource::SystemdUnit.new(unit_name) } + let(:provider) { Chef::Provider::SystemdUnit.new(new_resource, run_context) } + let(:unit_path_system) { "/etc/systemd/system/sysstat-collect.timer" } + let(:unit_path_user) { "/etc/systemd/user/sysstat-collect.timer" } + let(:unit_content_string) { "[Unit]\nDescription=Run system activity accounting tool every 10 minutes\n\n[Timer]\nOnCalendar=*:00/10\n\n[Install]\nWantedBy=sysstat.service" } + + let(:unit_content_hash) do + { + "Unit" => { + "Description" => "Run system activity accounting tool every 10 minutes", + }, + "Timer" => { + "OnCalendar" => "*:00/10", + }, + "Install" => { + "WantedBy" => "sysstat.service", + }, + } + end + + let(:user_cmd_opts) do + { + "user" => "joe", + "environment" => { + "DBUS_SESSION_BUS_ADDRESS" => "unix:path=/run/user/1000/bus", + }, + } + end + + let(:shell_out_success) do + double("shell_out_with_systems_locale", :exitstatus => 0, :error? => false) + end + + let(:shell_out_failure) do + double("shell_out_with_systems_locale", :exitstatus => 1, :error? => true) + end + + let(:shell_out_masked) do + double("shell_out_with_systems_locale", :exit_status => 0, :error? => false, :stdout => "masked") + end + + let(:shell_out_static) do + double("shell_out_with_systems_locale", :exit_status => 0, :error? => false, :stdout => "static") + end + + before(:each) do + allow(Chef::Resource::SystemdUnit).to receive(:new) + .with(unit_name) + .and_return(current_resource) + end + + describe "load_current_resource" do + before(:each) do + allow(provider).to receive(:active?).and_return(false) + allow(provider).to receive(:enabled?).and_return(false) + allow(provider).to receive(:masked?).and_return(false) + allow(provider).to receive(:static?).and_return(false) + end + + it "should create a current resource with the name of the new resource" do + expect(Chef::Resource::SystemdUnit).to receive(:new) + .with(unit_name) + .and_return(current_resource) + provider.load_current_resource + end + + it "should check if the unit is active" do + expect(provider).to receive(:active?) + provider.load_current_resource + end + + it "sets the active property to true if the unit is active" do + allow(provider).to receive(:active?).and_return(true) + provider.load_current_resource + expect(current_resource.active).to be true + end + + it "sets the active property to false if the unit is not active" do + allow(provider).to receive(:active?).and_return(false) + provider.load_current_resource + expect(current_resource.active).to be false + end + + it "should check if the unit is enabled" do + expect(provider).to receive(:enabled?) + provider.load_current_resource + end + + it "sets the enabled property to true if the unit is enabled" do + allow(provider).to receive(:enabled?).and_return(true) + provider.load_current_resource + expect(current_resource.enabled).to be true + end + + it "sets the enabled property to false if the unit is not enabled" do + allow(provider).to receive(:enabled?).and_return(false) + provider.load_current_resource + expect(current_resource.enabled).to be false + end + + it "should check if the unit is masked" do + expect(provider).to receive(:masked?) + provider.load_current_resource + end + + it "sets the masked property to true if the unit is masked" do + allow(provider).to receive(:masked?).and_return(true) + provider.load_current_resource + expect(current_resource.masked).to be true + end + + it "sets the masked property to false if the unit is masked" do + allow(provider).to receive(:masked?).and_return(false) + provider.load_current_resource + expect(current_resource.masked).to be false + end + + it "should check if the unit is static" do + expect(provider).to receive(:static?) + provider.load_current_resource + end + + it "sets the static property to true if the unit is static" do + allow(provider).to receive(:static?).and_return(true) + provider.load_current_resource + expect(current_resource.static).to be true + end + + it "sets the static property to false if the unit is not static" do + allow(provider).to receive(:static?).and_return(false) + provider.load_current_resource + expect(current_resource.static).to be false + end + + it "loads the system unit content if the file exists and user is not set" do + allow(File).to receive(:exist?) + .with(unit_path_system) + .and_return(true) + allow(File).to receive(:read) + .with(unit_path_system) + .and_return(unit_content_string) + + expect(File).to receive(:exist?) + .with(unit_path_system) + expect(File).to receive(:read) + .with(unit_path_system) + provider.load_current_resource + expect(current_resource.content).to eq(unit_content_string) + end + + it "does not load the system unit content if the unit file is not present and the user is not set" do + allow(File).to receive(:exist?) + .with(unit_path_system) + .and_return(false) + expect(File).to_not receive(:read) + .with(unit_path_system) + provider.load_current_resource + expect(current_resource.content).to eq(nil) + end + + it "loads the user unit content if the file exists and user is set" do + new_resource.user("joe") + allow(File).to receive(:exist?) + .with(unit_path_user) + .and_return(true) + allow(File).to receive(:read) + .with(unit_path_user) + .and_return(unit_content_string) + expect(File).to receive(:exist?) + .with(unit_path_user) + expect(File).to receive(:read) + .with(unit_path_user) + provider.load_current_resource + expect(current_resource.content).to eq(unit_content_string) + end + + it "does not load the user unit if the file does not exist and user is set" do + new_resource.user("joe") + allow(File).to receive(:exist?) + .with(unit_path_user) + .and_return(false) + expect(File).to_not receive(:read) + .with(unit_path_user) + provider.load_current_resource + expect(current_resource.content).to eq(nil) + end + end + + %w{/bin/systemctl /usr/bin/systemctl}.each do |systemctl_path| + describe "when systemctl path is #{systemctl_path}" do + before(:each) do + provider.current_resource = current_resource + allow(provider).to receive(:which) + .with("systemctl") + .and_return(systemctl_path) + end + + describe "creates/deletes the unit" do + it "creates the unit file when it does not exist" do + allow(provider).to receive(:manage_unit_file) + .with(:create) + .and_return(true) + allow(provider).to receive(:daemon_reload) + .and_return(true) + expect(provider).to receive(:manage_unit_file).with(:create) + provider.action_create + end + + it "creates the file when the unit content is different" do + allow(provider).to receive(:manage_unit_file) + .with(:create) + .and_return(true) + allow(provider).to receive(:daemon_reload) + .and_return(true) + expect(provider).to receive(:manage_unit_file).with(:create) + provider.action_create + end + + it "does not create the unit file when the content is the same" do + current_resource.content(unit_content_string) + allow(provider).to receive(:manage_unit_file).with(:create) + allow(provider).to receive(:daemon_reload) + .and_return(true) + expect(provider).to_not receive(:manage_unit_file) + provider.action_create + end + + it "triggers a daemon-reload when creating a unit with triggers_reload" do + allow(provider).to receive(:manage_unit_file).with(:create) + expect(new_resource.triggers_reload).to eq true + allow(provider).to receive(:shell_out_with_systems_locale!) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} daemon-reload") + provider.action_create + end + + it "triggers a daemon-reload when deleting a unit with triggers_reload" do + allow(File).to receive(:exist?) + .with(unit_path_system) + .and_return(true) + allow(provider).to receive(:manage_unit_file).with(:delete) + expect(new_resource.triggers_reload).to eq true + allow(provider).to receive(:shell_out_with_systems_locale!) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} daemon-reload") + provider.action_delete + end + + it "does not trigger a daemon-reload when creating a unit without triggers_reload" do + new_resource.triggers_reload(false) + allow(provider).to receive(:manage_unit_file).with(:create) + allow(provider).to receive(:shell_out_with_systems_locale!) + expect(provider).to_not receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} daemon-reload") + provider.action_create + end + + it "does not trigger a daemon-reload when deleting a unit without triggers_reload" do + new_resource.triggers_reload(false) + allow(File).to receive(:exist?) + .with(unit_path_system) + .and_return(true) + allow(provider).to receive(:manage_unit_file).with(:delete) + allow(provider).to receive(:shell_out_with_systems_locale!) + expect(provider).to_not receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} daemon-reload") + provider.action_delete + end + + context "when a user is specified" do + it "deletes the file when it exists" do + new_resource.user("joe") + allow(File).to receive(:exist?) + .with(unit_path_user) + .and_return(true) + allow(provider).to receive(:manage_unit_file) + .with(:delete) + .and_return(true) + allow(provider).to receive(:daemon_reload) + expect(provider).to receive(:manage_unit_file).with(:delete) + provider.action_delete + end + + it "does not delete the file when it is absent" do + new_resource.user("joe") + allow(File).to receive(:exist?) + .with(unit_path_user) + .and_return(false) + allow(provider).to receive(:manage_unit_file).with(:delete) + expect(provider).to_not receive(:manage_unit_file) + provider.action_delete + end + end + + context "when no user is specified" do + it "deletes the file when it exists" do + allow(File).to receive(:exist?) + .with(unit_path_system) + .and_return(true) + allow(provider).to receive(:manage_unit_file) + .with(:delete) + allow(provider).to receive(:daemon_reload) + expect(provider).to receive(:manage_unit_file).with(:delete) + provider.action_delete + end + + it "does not delete the file when it is absent" do + allow(File).to receive(:exist?) + .with(unit_path_system) + .and_return(false) + allow(provider).to receive(:manage_unit_file).with(:delete) + allow(provider).to receive(:daemon_reload) + expect(provider).to_not receive(:manage_unit_file) + provider.action_delete + end + end + end + + describe "enables/disables the unit" do + context "when a user is specified" do + it "enables the unit when it is disabled" do + current_resource.user(user_name) + current_resource.enabled(false) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user enable #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_enable + end + + it "does not enable the unit when it is enabled" do + current_resource.user(user_name) + current_resource.enabled(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_enable + end + + it "does not enable the unit when it is static" do + current_resource.user(user_name) + current_resource.static(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_enable + end + + it "disables the unit when it is enabled" do + current_resource.user(user_name) + current_resource.enabled(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user disable #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_disable + end + + it "does not disable the unit when it is disabled" do + current_resource.user(user_name) + current_resource.enabled(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_disable + end + + it "does not disable the unit when it is static" do + current_resource.user(user_name) + current_resource.static(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_disable + end + end + + context "when no user is specified" do + it "enables the unit when it is disabled" do + current_resource.enabled(false) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system enable #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_enable + end + + it "does not enable the unit when it is enabled" do + current_resource.enabled(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_enable + end + + it "does not enable the unit when it is static" do + current_resource.static(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_enable + end + + it "disables the unit when it is enabled" do + current_resource.enabled(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system disable #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_disable + end + + it "does not disable the unit when it is disabled" do + current_resource.enabled(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_disable + end + + it "does not disable the unit when it is static" do + current_resource.user(user_name) + current_resource.static(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_disable + end + end + end + + describe "masks/unmasks the unit" do + context "when a user is specified" do + it "masks the unit when it is unmasked" do + current_resource.user(user_name) + current_resource.masked(false) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user mask #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_mask + end + + it "does not mask the unit when it is masked" do + current_resource.user(user_name) + current_resource.masked(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_mask + end + + it "unmasks the unit when it is masked" do + current_resource.user(user_name) + current_resource.masked(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user unmask #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_unmask + end + + it "does not unmask the unit when it is unmasked" do + current_resource.user(user_name) + current_resource.masked(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_unmask + end + end + + context "when no user is specified" do + it "masks the unit when it is unmasked" do + current_resource.masked(false) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system mask #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_mask + end + + it "does not mask the unit when it is masked" do + current_resource.masked(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_mask + end + + it "unmasks the unit when it is masked" do + current_resource.masked(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system unmask #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_unmask + end + + it "does not unmask the unit when it is unmasked" do + current_resource.masked(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_unmask + end + end + end + + describe "starts/stops the unit" do + context "when a user is specified" do + it "starts the unit when it is inactive" do + current_resource.user(user_name) + current_resource.active(false) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user start #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_start + end + + it "does not start the unit when it is active" do + current_resource.user(user_name) + current_resource.active(true) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_start + end + + it "stops the unit when it is active" do + current_resource.user(user_name) + current_resource.active(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user stop #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_stop + end + + it "does not stop the unit when it is inactive" do + current_resource.user(user_name) + current_resource.active(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_stop + end + end + + context "when no user is specified" do + it "starts the unit when it is inactive" do + current_resource.active(false) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system start #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_start + end + + it "does not start the unit when it is active" do + current_resource.active(true) + expect(provider).to_not receive(:shell_out_with_systems_locale!) + provider.action_start + end + + it "stops the unit when it is active" do + current_resource.active(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system stop #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_stop + end + + it "does not stop the unit when it is inactive" do + current_resource.active(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_stop + end + end + end + + describe "restarts/reloads the unit" do + context "when a user is specified" do + it "restarts the unit" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user restart #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_restart + end + + it "reloads the unit if active" do + current_resource.user(user_name) + current_resource.active(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --user reload #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + provider.action_reload + end + + it "does not reload if the unit is inactive" do + current_resource.user(user_name) + current_resource.active(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_reload + end + end + + context "when no user is specified" do + it "restarts the unit" do + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system restart #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_restart + end + + it "reloads the unit if active" do + current_resource.active(true) + expect(provider).to receive(:shell_out_with_systems_locale!) + .with("#{systemctl_path} --system reload #{unit_name}", {}) + .and_return(shell_out_success) + provider.action_reload + end + + it "does not reload the unit if inactive" do + current_resource.active(false) + expect(provider).not_to receive(:shell_out_with_systems_locale!) + provider.action_reload + end + end + end + + describe "#active?" do + before(:each) do + provider.current_resource = current_resource + allow(provider).to receive(:which).with("systemctl").and_return("#{systemctl_path}") + end + + context "when a user is specified" do + it "returns true when unit is active" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user is-active #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + expect(provider.active?).to be true + end + + it "returns false when unit is inactive" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user is-active #{unit_name}", user_cmd_opts) + .and_return(shell_out_failure) + expect(provider.active?).to be false + end + end + + context "when no user is specified" do + it "returns true when unit is active" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system is-active #{unit_name}", {}) + .and_return(shell_out_success) + expect(provider.active?).to be true + end + + it "returns false when unit is not active" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system is-active #{unit_name}", {}) + .and_return(shell_out_failure) + expect(provider.active?).to be false + end + end + end + + describe "#enabled?" do + before(:each) do + provider.current_resource = current_resource + allow(provider).to receive(:which).with("systemctl").and_return("#{systemctl_path}") + end + + context "when a user is specified" do + it "returns true when unit is enabled" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user is-enabled #{unit_name}", user_cmd_opts) + .and_return(shell_out_success) + expect(provider.enabled?).to be true + end + + it "returns false when unit is not enabled" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user is-enabled #{unit_name}", user_cmd_opts) + .and_return(shell_out_failure) + expect(provider.enabled?).to be false + end + end + + context "when no user is specified" do + it "returns true when unit is enabled" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system is-enabled #{unit_name}", {}) + .and_return(shell_out_success) + expect(provider.enabled?).to be true + end + + it "returns false when unit is not enabled" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system is-enabled #{unit_name}", {}) + .and_return(shell_out_failure) + expect(provider.enabled?).to be false + end + end + end + + describe "#masked?" do + before(:each) do + provider.current_resource = current_resource + allow(provider).to receive(:which).with("systemctl").and_return("#{systemctl_path}") + end + + context "when a user is specified" do + it "returns true when the unit is masked" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user status #{unit_name}", user_cmd_opts) + .and_return(shell_out_masked) + expect(provider.masked?).to be true + end + + it "returns false when the unit is not masked" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user status #{unit_name}", user_cmd_opts) + .and_return(shell_out_static) + expect(provider.masked?).to be false + end + end + + context "when no user is specified" do + it "returns true when the unit is masked" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system status #{unit_name}", {}) + .and_return(shell_out_masked) + expect(provider.masked?).to be true + end + + it "returns false when the unit is not masked" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system status #{unit_name}", {}) + .and_return(shell_out_static) + expect(provider.masked?).to be false + end + end + end + + describe "#static?" do + before(:each) do + provider.current_resource = current_resource + allow(provider).to receive(:which).with("systemctl").and_return("#{systemctl_path}") + end + + context "when a user is specified" do + it "returns true when the unit is static" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user is-enabled #{unit_name}", user_cmd_opts) + .and_return(shell_out_static) + expect(provider.static?).to be true + end + + it "returns false when the unit is not static" do + current_resource.user(user_name) + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --user is-enabled #{unit_name}", user_cmd_opts) + .and_return(shell_out_masked) + expect(provider.static?).to be false + end + end + + context "when no user is specified" do + it "returns true when the unit is static" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system is-enabled #{unit_name}", {}) + .and_return(shell_out_static) + expect(provider.static?).to be true + end + + it "returns false when the unit is not static" do + expect(provider).to receive(:shell_out) + .with("#{systemctl_path} --system is-enabled #{unit_name}", {}) + .and_return(shell_out_masked) + expect(provider.static?).to be false + end + end + end + end + end +end diff --git a/spec/unit/resource/systemd_unit_spec.rb b/spec/unit/resource/systemd_unit_spec.rb new file mode 100644 index 0000000000..7e46872525 --- /dev/null +++ b/spec/unit/resource/systemd_unit_spec.rb @@ -0,0 +1,133 @@ +# +# Author:: Nathan Williams (<nath.e.will@gmail.com>) +# Copyright:: Copyright 2016, Nathan Williams +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" + +describe Chef::Resource::SystemdUnit do + before(:each) do + @resource = Chef::Resource::SystemdUnit.new("sysstat-collect.timer") + end + + let(:unit_content_string) { "[Unit]\nDescription = Run system activity accounting tool every 10 minutes\n\n[Timer]\nOnCalendar = *:00/10\n\n[Install]\nWantedBy = sysstat.service\n" } + + let(:unit_content_hash) do + { + "Unit" => { + "Description" => "Run system activity accounting tool every 10 minutes", + }, + "Timer" => { + "OnCalendar" => "*:00/10", + }, + "Install" => { + "WantedBy" => "sysstat.service", + }, + } + end + + it "creates a new Chef::Resource::SystemdUnit" do + expect(@resource).to be_a_kind_of(Chef::Resource) + expect(@resource).to be_a_kind_of(Chef::Resource::SystemdUnit) + end + + it "should have a name" do + expect(@resource.name).to eql("sysstat-collect.timer") + end + + it "has a default action of nothing" do + expect(@resource.action).to eql([:nothing]) + end + + it "supports appropriate unit actions" do + expect { @resource.action :create }.not_to raise_error + expect { @resource.action :delete }.not_to raise_error + expect { @resource.action :enable }.not_to raise_error + expect { @resource.action :disable }.not_to raise_error + expect { @resource.action :mask }.not_to raise_error + expect { @resource.action :unmask }.not_to raise_error + expect { @resource.action :start }.not_to raise_error + expect { @resource.action :stop }.not_to raise_error + expect { @resource.action :restart }.not_to raise_error + expect { @resource.action :reload }.not_to raise_error + expect { @resource.action :wrong }.to raise_error(ArgumentError) + end + + it "accepts boolean state properties" do + expect { @resource.active false }.not_to raise_error + expect { @resource.active true }.not_to raise_error + expect { @resource.active "yes" }.to raise_error(ArgumentError) + + expect { @resource.enabled true }.not_to raise_error + expect { @resource.enabled false }.not_to raise_error + expect { @resource.enabled "no" }.to raise_error(ArgumentError) + + expect { @resource.masked true }.not_to raise_error + expect { @resource.masked false }.not_to raise_error + expect { @resource.masked :nope }.to raise_error(ArgumentError) + + expect { @resource.static true }.not_to raise_error + expect { @resource.static false }.not_to raise_error + expect { @resource.static "yep" }.to raise_error(ArgumentError) + end + + it "accepts the content property" do + expect { @resource.content nil }.not_to raise_error + expect { @resource.content "test" }.not_to raise_error + expect { @resource.content({}) }.not_to raise_error + expect { @resource.content 5 }.to raise_error(ArgumentError) + end + + it "accepts the user property" do + expect { @resource.user nil }.not_to raise_error + expect { @resource.user "deploy" }.not_to raise_error + expect { @resource.user 5 }.to raise_error(ArgumentError) + end + + it "accepts the triggers_reload property" do + expect { @resource.triggers_reload true }.not_to raise_error + expect { @resource.triggers_reload false }.not_to raise_error + expect { @resource.triggers_reload "no" }.to raise_error(ArgumentError) + end + + it "reports its state" do + @resource.active true + @resource.enabled true + @resource.masked false + @resource.static false + @resource.content "test" + state = @resource.state + expect(state[:active]).to eq(true) + expect(state[:enabled]).to eq(true) + expect(state[:masked]).to eq(false) + expect(state[:static]).to eq(false) + expect(state[:content]).to eq("test") + end + + it "returns the unit name as its identity" do + expect(@resource.identity).to eq("sysstat-collect.timer") + end + + it "serializes to ini with a string-formatted content property" do + @resource.content(unit_content_string) + expect(@resource.to_ini).to eq unit_content_string + end + + it "serializes to ini with a hash-formatted content property" do + @resource.content(unit_content_hash) + expect(@resource.to_ini).to eq unit_content_string + end +end |