summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThom May <thom@may.lt>2016-05-01 11:06:55 +0100
committerThom May <thom@may.lt>2016-05-01 11:06:55 +0100
commitfd78fabc832dd801c3658f57dab9197e8f7ec655 (patch)
tree58809c8e69d84dbad37840bd4037ab1107612065
parentb94e2ef41fb68edc69e7a8067ea4b472e4b3aca9 (diff)
parent1b0da9c71c766e82705b1548125965d959d8840c (diff)
downloadchef-fd78fabc832dd801c3658f57dab9197e8f7ec655.tar.gz
Merge pull request #4700 from nathwill/features/systemd-unit
add systemd_unit resource
-rw-r--r--chef.gemspec1
-rw-r--r--lib/chef/provider/systemd_unit.rb210
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/systemd_unit.rb61
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/provider/systemd_unit_spec.rb797
-rw-r--r--spec/unit/resource/systemd_unit_spec.rb133
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