diff --git a/lib/puppet/provider/service/upstart.rb b/lib/puppet/provider/service/upstart.rb index 9395a90a3..548341a89 100644 --- a/lib/puppet/provider/service/upstart.rb +++ b/lib/puppet/provider/service/upstart.rb @@ -1,357 +1,361 @@ require 'semver' Puppet::Type.type(:service).provide :upstart, :parent => :debian do START_ON = /^\s*start\s+on/ COMMENTED_START_ON = /^\s*#+\s*start\s+on/ MANUAL = /^\s*manual\s*$/ desc "Ubuntu service management with `upstart`. This provider manages `upstart` jobs on Ubuntu. For `upstart` documentation, see . " confine :any => [ Facter.value(:operatingsystem) == 'Ubuntu', (Facter.value(:osfamily) == 'RedHat' and Facter.value(:operatingsystemrelease) =~ /^6\./), Facter.value(:operatingsystem) == 'Amazon', Facter.value(:operatingsystem) == 'LinuxMint', ] defaultfor :operatingsystem => :ubuntu commands :start => "/sbin/start", :stop => "/sbin/stop", :restart => "/sbin/restart", :status_exec => "/sbin/status", :initctl => "/sbin/initctl" # upstart developer haven't implemented initctl enable/disable yet: # http://www.linuxplanet.com/linuxplanet/tutorials/7033/2/ has_feature :enableable def self.instances self.get_services(self.excludes) # Take exclude list from init provider end def self.excludes excludes = super if Facter.value(:osfamily) == 'RedHat' # Puppet cannot deal with services that have instances, so we have to # ignore these services using instances on redhat based systems. excludes += %w[serial tty] end excludes end def self.get_services(exclude=[]) instances = [] execpipe("#{command(:initctl)} list") { |process| process.each_line { |line| # needs special handling of services such as network-interface: # initctl list: # network-interface (lo) start/running # network-interface (eth0) start/running # network-interface-security start/running name = \ if matcher = line.match(/^(network-interface)\s\(([^\)]+)\)/) "#{matcher[1]} INTERFACE=#{matcher[2]}" elsif matcher = line.match(/^(network-interface-security)\s\(([^\)]+)\)/) "#{matcher[1]} JOB=#{matcher[2]}" else line.split.first end instances << new(:name => name) } } instances.reject { |instance| exclude.include?(instance.name) } end def self.defpath ["/etc/init", "/etc/init.d"] end def upstart_version @upstart_version ||= initctl("--version").match(/initctl \(upstart ([^\)]*)\)/)[1] end # Where is our override script? def overscript @overscript ||= initscript.gsub(/\.conf$/,".override") end def search(name) # Search prefers .conf as that is what upstart uses [".conf", "", ".sh"].each do |suffix| paths.each do |path| service_name = name.match(/^(\S+)/)[1] fqname = File.join(path, service_name + suffix) if Puppet::FileSystem.exist?(fqname) return fqname end self.debug("Could not find #{name}#{suffix} in #{path}") end end raise Puppet::Error, "Could not find init script or upstart conf file for '#{name}'" end def enabled? return super if not is_upstart? script_contents = read_script_from(initscript) if version_is_pre_0_6_7 enabled_pre_0_6_7?(script_contents) elsif version_is_pre_0_9_0 enabled_pre_0_9_0?(script_contents) elsif version_is_post_0_9_0 enabled_post_0_9_0?(script_contents, read_override_file) end end def enable return super if not is_upstart? script_text = read_script_from(initscript) if version_is_pre_0_9_0 enable_pre_0_9_0(script_text) else enable_post_0_9_0(script_text, read_override_file) end end def disable return super if not is_upstart? script_text = read_script_from(initscript) if version_is_pre_0_6_7 disable_pre_0_6_7(script_text) elsif version_is_pre_0_9_0 disable_pre_0_9_0(script_text) elsif version_is_post_0_9_0 disable_post_0_9_0(read_override_file) end end def startcmd is_upstart? ? [command(:start), @resource[:name]] : super end def stopcmd is_upstart? ? [command(:stop), @resource[:name]] : super end def restartcmd is_upstart? ? (@resource[:hasrestart] == :true) && [command(:restart), @resource[:name]] : super end def statuscmd is_upstart? ? nil : super #this is because upstart is broken with its return codes end def status - return super if not is_upstart? + if (@resource[:hasstatus] == :false) || + @resource[:status] || + ! is_upstart? + return super + end output = status_exec(@resource[:name].split) if output =~ /start\// return :running else return :stopped end end private def is_upstart?(script = initscript) Puppet::FileSystem.exist?(script) && script.match(/\/etc\/init\/\S+\.conf/) end def version_is_pre_0_6_7 Puppet::Util::Package.versioncmp(upstart_version, "0.6.7") == -1 end def version_is_pre_0_9_0 Puppet::Util::Package.versioncmp(upstart_version, "0.9.0") == -1 end def version_is_post_0_9_0 Puppet::Util::Package.versioncmp(upstart_version, "0.9.0") >= 0 end def enabled_pre_0_6_7?(script_text) # Upstart version < 0.6.7 means no manual stanza. if script_text.match(START_ON) return :true else return :false end end def enabled_pre_0_9_0?(script_text) # Upstart version < 0.9.0 means no override files # So we check to see if an uncommented start on or manual stanza is the last one in the file # The last one in the file wins. enabled = :false script_text.each_line do |line| if line.match(START_ON) enabled = :true elsif line.match(MANUAL) enabled = :false end end enabled end def enabled_post_0_9_0?(script_text, over_text) # This version has manual stanzas and override files # So we check to see if an uncommented start on or manual stanza is the last one in the # conf file and any override files. The last one in the file wins. enabled = :false script_text.each_line do |line| if line.match(START_ON) enabled = :true elsif line.match(MANUAL) enabled = :false end end over_text.each_line do |line| if line.match(START_ON) enabled = :true elsif line.match(MANUAL) enabled = :false end end if over_text enabled end def enable_pre_0_9_0(text) # We also need to remove any manual stanzas to ensure that it is enabled text = remove_manual_from(text) if enabled_pre_0_9_0?(text) == :false enabled_script = if text.match(COMMENTED_START_ON) uncomment_start_block_in(text) else add_default_start_to(text) end else enabled_script = text end write_script_to(initscript, enabled_script) end def enable_post_0_9_0(script_text, over_text) over_text = remove_manual_from(over_text) if enabled_post_0_9_0?(script_text, over_text) == :false if script_text.match(START_ON) over_text << extract_start_on_block_from(script_text) else over_text << "\nstart on runlevel [2,3,4,5]" end end write_script_to(overscript, over_text) end def disable_pre_0_6_7(script_text) disabled_script = comment_start_block_in(script_text) write_script_to(initscript, disabled_script) end def disable_pre_0_9_0(script_text) write_script_to(initscript, ensure_disabled_with_manual(script_text)) end def disable_post_0_9_0(over_text) write_script_to(overscript, ensure_disabled_with_manual(over_text)) end def read_override_file if Puppet::FileSystem.exist?(overscript) read_script_from(overscript) else "" end end def uncomment(line) line.gsub(/^(\s*)#+/, '\1') end def remove_trailing_comments_from_commented_line_of(line) line.gsub(/^(\s*#+\s*[^#]*).*/, '\1') end def remove_trailing_comments_from(line) line.gsub(/^(\s*[^#]*).*/, '\1') end def unbalanced_parens_on(line) line.count('(') - line.count(')') end def remove_manual_from(text) text.gsub(MANUAL, "") end def comment_start_block_in(text) parens = 0 text.lines.map do |line| if line.match(START_ON) || parens > 0 # If there are more opening parens than closing parens, we need to comment out a multiline 'start on' stanza parens += unbalanced_parens_on(remove_trailing_comments_from(line)) "#" + line else line end end.join('') end def uncomment_start_block_in(text) parens = 0 text.lines.map do |line| if line.match(COMMENTED_START_ON) || parens > 0 parens += unbalanced_parens_on(remove_trailing_comments_from_commented_line_of(line)) uncomment(line) else line end end.join('') end def extract_start_on_block_from(text) parens = 0 text.lines.map do |line| if line.match(START_ON) || parens > 0 parens += unbalanced_parens_on(remove_trailing_comments_from(line)) line end end.join('') end def add_default_start_to(text) text + "\nstart on runlevel [2,3,4,5]" end def ensure_disabled_with_manual(text) remove_manual_from(text) + "\nmanual" end def read_script_from(filename) File.open(filename) do |file| file.read end end def write_script_to(file, text) Puppet::Util.replace_file(file, 0644) do |file| file.write(text) end end end diff --git a/spec/unit/provider/service/upstart_spec.rb b/spec/unit/provider/service/upstart_spec.rb index d31511626..ba55cb846 100755 --- a/spec/unit/provider/service/upstart_spec.rb +++ b/spec/unit/provider/service/upstart_spec.rb @@ -1,533 +1,590 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:service).provider(:upstart) do let(:manual) { "\nmanual" } let(:start_on_default_runlevels) { "\nstart on runlevel [2,3,4,5]" } let(:provider_class) { Puppet::Type.type(:service).provider(:upstart) } def given_contents_of(file, content) File.open(file, 'w') do |file| file.write(content) end end def then_contents_of(file) File.open(file).read end def lists_processes_as(output) Puppet::Util::Execution.stubs(:execpipe).with("/sbin/initctl list").yields(output) provider_class.stubs(:which).with("/sbin/initctl").returns("/sbin/initctl") end it "should be the default provider on Ubuntu" do Facter.expects(:value).with(:operatingsystem).returns("Ubuntu") described_class.default?.should be_true end describe "excluding services" do it "ignores tty and serial on Redhat systems" do Facter.stubs(:value).with(:osfamily).returns('RedHat') expect(described_class.excludes).to include 'serial' expect(described_class.excludes).to include 'tty' end end describe "#instances" do it "should be able to find all instances" do lists_processes_as("rc stop/waiting\nssh start/running, process 712") provider_class.instances.map {|provider| provider.name}.should =~ ["rc","ssh"] end it "should attach the interface name for network interfaces" do lists_processes_as("network-interface (eth0)") provider_class.instances.first.name.should == "network-interface INTERFACE=eth0" end it "should attach the job name for network interface security" do processes = "network-interface-security (network-interface/eth0)" provider_class.stubs(:execpipe).yields(processes) provider_class.instances.first.name.should == "network-interface-security JOB=network-interface/eth0" end it "should not find excluded services" do processes = "wait-for-state stop/waiting" processes += "\nportmap-wait start/running" processes += "\nidmapd-mounting stop/waiting" processes += "\nstartpar-bridge start/running" processes += "\ncryptdisks-udev stop/waiting" processes += "\nstatd-mounting stop/waiting" processes += "\ngssd-mounting stop/waiting" provider_class.stubs(:execpipe).yields(processes) provider_class.instances.should be_empty end end describe "#search" do it "searches through paths to find a matching conf file" do File.stubs(:directory?).returns(true) Puppet::FileSystem.stubs(:exist?).returns(false) Puppet::FileSystem.expects(:exist?).with("/etc/init/foo-bar.conf").returns(true) resource = Puppet::Type.type(:service).new(:name => "foo-bar", :provider => :upstart) provider = provider_class.new(resource) provider.initscript.should == "/etc/init/foo-bar.conf" end it "searches for just the name of a compound named service" do File.stubs(:directory?).returns(true) Puppet::FileSystem.stubs(:exist?).returns(false) Puppet::FileSystem.expects(:exist?).with("/etc/init/network-interface.conf").returns(true) resource = Puppet::Type.type(:service).new(:name => "network-interface INTERFACE=lo", :provider => :upstart) provider = provider_class.new(resource) provider.initscript.should == "/etc/init/network-interface.conf" end end describe "#status" do it "should use the default status command if none is specified" do resource = Puppet::Type.type(:service).new(:name => "foo", :provider => :upstart) provider = provider_class.new(resource) provider.stubs(:is_upstart?).returns(true) provider.expects(:status_exec).with(["foo"]).returns("foo start/running, process 1000") Process::Status.any_instance.stubs(:exitstatus).returns(0) provider.status.should == :running end + describe "when a special status command is specifed" do + it "should use the provided status command" do + resource = Puppet::Type.type(:service).new(:name => 'foo', :provider => :upstart, :status => '/bin/foo') + provider = provider_class.new(resource) + provider.stubs(:is_upstart?).returns(true) + + provider.expects(:status_exec).with(['foo']).never + provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true) + Process::Status.any_instance.stubs(:exitstatus).returns(0) + provider.status + end + + it "should return :stopped when the provided status command return non-zero" do + resource = Puppet::Type.type(:service).new(:name => 'foo', :provider => :upstart, :status => '/bin/foo') + provider = provider_class.new(resource) + provider.stubs(:is_upstart?).returns(true) + + provider.expects(:status_exec).with(['foo']).never + provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true) + $CHILD_STATUS.stubs(:exitstatus).returns 1 + provider.status.should == :stopped + end + + it "should return :running when the provided status command return zero" do + resource = Puppet::Type.type(:service).new(:name => 'foo', :provider => :upstart, :status => '/bin/foo') + provider = provider_class.new(resource) + provider.stubs(:is_upstart?).returns(true) + + provider.expects(:status_exec).with(['foo']).never + provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true) + $CHILD_STATUS.stubs(:exitstatus).returns 0 + provider.status.should == :running + end + end + + describe "when :hasstatus is set to false" do + it "should return :stopped if the pid can not be found" do + resource = Puppet::Type.type(:service).new(:name => 'foo', :hasstatus => false, :provider => :upstart) + provider = provider_class.new(resource) + provider.stubs(:is_upstart?).returns(true) + + provider.expects(:status_exec).with(['foo']).never + provider.expects(:getpid).returns nil + provider.status.should == :stopped + end + + it "should return :running if the pid can be found" do + resource = Puppet::Type.type(:service).new(:name => 'foo', :hasstatus => false, :provider => :upstart) + provider = provider_class.new(resource) + provider.stubs(:is_upstart?).returns(true) + + provider.expects(:status_exec).with(['foo']).never + provider.expects(:getpid).returns 2706 + provider.status.should == :running + end + end + it "should properly handle services with 'start' in their name" do resource = Puppet::Type.type(:service).new(:name => "foostartbar", :provider => :upstart) provider = provider_class.new(resource) provider.stubs(:is_upstart?).returns(true) provider.expects(:status_exec).with(["foostartbar"]).returns("foostartbar stop/waiting") Process::Status.any_instance.stubs(:exitstatus).returns(0) provider.status.should == :stopped end end describe "inheritance" do let :resource do resource = Puppet::Type.type(:service).new(:name => "foo", :provider => :upstart) end let :provider do provider = provider_class.new(resource) end describe "when upstart job" do before(:each) do provider.stubs(:is_upstart?).returns(true) end ["start", "stop"].each do |command| it "should return the #{command}cmd of its parent provider" do provider.send("#{command}cmd".to_sym).should == [provider.command(command.to_sym), resource.name] end end it "should return nil for the statuscmd" do provider.statuscmd.should be_nil end end end describe "should be enableable" do let :resource do Puppet::Type.type(:service).new(:name => "foo", :provider => :upstart) end let :provider do provider_class.new(resource) end let :init_script do PuppetSpec::Files.tmpfile("foo.conf") end let :over_script do PuppetSpec::Files.tmpfile("foo.override") end let :disabled_content do "\t # \t start on\nother file stuff" end let :multiline_disabled do "# \t start on other file stuff (\n" + "# more stuff ( # )))))inline comment\n" + "# finishing up )\n" + "# and done )\n" + "this line shouldn't be touched\n" end let :multiline_disabled_bad do "# \t start on other file stuff (\n" + "# more stuff ( # )))))inline comment\n" + "# finishing up )\n" + "# and done )\n" + "# this is a comment i want to be a comment\n" + "this line shouldn't be touched\n" end let :multiline_enabled_bad do " \t start on other file stuff (\n" + " more stuff ( # )))))inline comment\n" + " finishing up )\n" + " and done )\n" + "# this is a comment i want to be a comment\n" + "this line shouldn't be touched\n" end let :multiline_enabled do " \t start on other file stuff (\n" + " more stuff ( # )))))inline comment\n" + " finishing up )\n" + " and done )\n" + "this line shouldn't be touched\n" end let :multiline_enabled_standalone do " \t start on other file stuff (\n" + " more stuff ( # )))))inline comment\n" + " finishing up )\n" + " and done )\n" end let :enabled_content do "\t \t start on\nother file stuff" end let :content do "just some text" end describe "Upstart version < 0.6.7" do before(:each) do provider.stubs(:is_upstart?).returns(true) provider.stubs(:upstart_version).returns("0.6.5") provider.stubs(:search).returns(init_script) end [:enabled?,:enable,:disable].each do |enableable| it "should respond to #{enableable}" do provider.should respond_to(enableable) end end describe "when enabling" do it "should open and uncomment the '#start on' line" do given_contents_of(init_script, disabled_content) provider.enable then_contents_of(init_script).should == enabled_content end it "should add a 'start on' line if none exists" do given_contents_of(init_script, "this is a file") provider.enable then_contents_of(init_script).should == "this is a file" + start_on_default_runlevels end it "should handle multiline 'start on' stanzas" do given_contents_of(init_script, multiline_disabled) provider.enable then_contents_of(init_script).should == multiline_enabled end it "should leave not 'start on' comments alone" do given_contents_of(init_script, multiline_disabled_bad) provider.enable then_contents_of(init_script).should == multiline_enabled_bad end end describe "when disabling" do it "should open and comment the 'start on' line" do given_contents_of(init_script, enabled_content) provider.disable then_contents_of(init_script).should == "#" + enabled_content end it "should handle multiline 'start on' stanzas" do given_contents_of(init_script, multiline_enabled) provider.disable then_contents_of(init_script).should == multiline_disabled end end describe "when checking whether it is enabled" do it "should consider 'start on ...' to be enabled" do given_contents_of(init_script, enabled_content) provider.enabled?.should == :true end it "should consider '#start on ...' to be disabled" do given_contents_of(init_script, disabled_content) provider.enabled?.should == :false end it "should consider no start on line to be disabled" do given_contents_of(init_script, content) provider.enabled?.should == :false end end end describe "Upstart version < 0.9.0" do before(:each) do provider.stubs(:is_upstart?).returns(true) provider.stubs(:upstart_version).returns("0.7.0") provider.stubs(:search).returns(init_script) end [:enabled?,:enable,:disable].each do |enableable| it "should respond to #{enableable}" do provider.should respond_to(enableable) end end describe "when enabling" do it "should open and uncomment the '#start on' line" do given_contents_of(init_script, disabled_content) provider.enable then_contents_of(init_script).should == enabled_content end it "should add a 'start on' line if none exists" do given_contents_of(init_script, "this is a file") provider.enable then_contents_of(init_script).should == "this is a file" + start_on_default_runlevels end it "should handle multiline 'start on' stanzas" do given_contents_of(init_script, multiline_disabled) provider.enable then_contents_of(init_script).should == multiline_enabled end it "should remove manual stanzas" do given_contents_of(init_script, multiline_enabled + manual) provider.enable then_contents_of(init_script).should == multiline_enabled end it "should leave not 'start on' comments alone" do given_contents_of(init_script, multiline_disabled_bad) provider.enable then_contents_of(init_script).should == multiline_enabled_bad end end describe "when disabling" do it "should add a manual stanza" do given_contents_of(init_script, enabled_content) provider.disable then_contents_of(init_script).should == enabled_content + manual end it "should remove manual stanzas before adding new ones" do given_contents_of(init_script, multiline_enabled + manual + "\n" + multiline_enabled) provider.disable then_contents_of(init_script).should == multiline_enabled + "\n" + multiline_enabled + manual end it "should handle multiline 'start on' stanzas" do given_contents_of(init_script, multiline_enabled) provider.disable then_contents_of(init_script).should == multiline_enabled + manual end end describe "when checking whether it is enabled" do describe "with no manual stanza" do it "should consider 'start on ...' to be enabled" do given_contents_of(init_script, enabled_content) provider.enabled?.should == :true end it "should consider '#start on ...' to be disabled" do given_contents_of(init_script, disabled_content) provider.enabled?.should == :false end it "should consider no start on line to be disabled" do given_contents_of(init_script, content) provider.enabled?.should == :false end end describe "with manual stanza" do it "should consider 'start on ...' to be disabled if there is a trailing manual stanza" do given_contents_of(init_script, enabled_content + manual + "\nother stuff") provider.enabled?.should == :false end it "should consider two start on lines with a manual in the middle to be enabled" do given_contents_of(init_script, enabled_content + manual + "\n" + enabled_content) provider.enabled?.should == :true end end end end describe "Upstart version > 0.9.0" do before(:each) do provider.stubs(:is_upstart?).returns(true) provider.stubs(:upstart_version).returns("0.9.5") provider.stubs(:search).returns(init_script) provider.stubs(:overscript).returns(over_script) end [:enabled?,:enable,:disable].each do |enableable| it "should respond to #{enableable}" do provider.should respond_to(enableable) end end describe "when enabling" do it "should add a 'start on' line if none exists" do given_contents_of(init_script, "this is a file") provider.enable then_contents_of(init_script).should == "this is a file" then_contents_of(over_script).should == start_on_default_runlevels end it "should handle multiline 'start on' stanzas" do given_contents_of(init_script, multiline_disabled) provider.enable then_contents_of(init_script).should == multiline_disabled then_contents_of(over_script).should == start_on_default_runlevels end it "should remove any manual stanzas from the override file" do given_contents_of(over_script, manual) given_contents_of(init_script, enabled_content) provider.enable then_contents_of(init_script).should == enabled_content then_contents_of(over_script).should == "" end it "should copy existing start on from conf file if conf file is disabled" do given_contents_of(init_script, multiline_enabled_standalone + manual) provider.enable then_contents_of(init_script).should == multiline_enabled_standalone + manual then_contents_of(over_script).should == multiline_enabled_standalone end it "should leave not 'start on' comments alone" do given_contents_of(init_script, multiline_disabled_bad) given_contents_of(over_script, "") provider.enable then_contents_of(init_script).should == multiline_disabled_bad then_contents_of(over_script).should == start_on_default_runlevels end end describe "when disabling" do it "should add a manual stanza to the override file" do given_contents_of(init_script, enabled_content) provider.disable then_contents_of(init_script).should == enabled_content then_contents_of(over_script).should == manual end it "should handle multiline 'start on' stanzas" do given_contents_of(init_script, multiline_enabled) provider.disable then_contents_of(init_script).should == multiline_enabled then_contents_of(over_script).should == manual end end describe "when checking whether it is enabled" do describe "with no override file" do it "should consider 'start on ...' to be enabled" do given_contents_of(init_script, enabled_content) provider.enabled?.should == :true end it "should consider '#start on ...' to be disabled" do given_contents_of(init_script, disabled_content) provider.enabled?.should == :false end it "should consider no start on line to be disabled" do given_contents_of(init_script, content) provider.enabled?.should == :false end end describe "with override file" do it "should consider 'start on ...' to be disabled if there is manual in override file" do given_contents_of(init_script, enabled_content) given_contents_of(over_script, manual + "\nother stuff") provider.enabled?.should == :false end it "should consider '#start on ...' to be enabled if there is a start on in the override file" do given_contents_of(init_script, disabled_content) given_contents_of(over_script, "start on stuff") provider.enabled?.should == :true end end end end end end