diff --git a/acceptance/tests/resource/service/ticket_14297_handle_upstart.rb b/acceptance/tests/resource/service/ticket_14297_handle_upstart.rb new file mode 100644 index 000000000..edc7edd53 --- /dev/null +++ b/acceptance/tests/resource/service/ticket_14297_handle_upstart.rb @@ -0,0 +1,48 @@ +test_name 'Upstart Testing' + +# only run these on ubuntu vms +confine :to, :platform => 'ubuntu' + +# pick any ubuntu agent +agent = agents.first + +def check_service_for(pkg, type, agent) + if pkg == "apache2" + if type == "stop" + on agent, "service #{pkg} status", :acceptable_exit_codes => [1,2,3] + else + on agent, "service #{pkg} status", :acceptable_exit_codes => [0] + end + else + on agent, "service #{pkg} status | grep #{type} -q" + end +end + +begin +# in Precise these packages provide a mix of upstart with no linked init +# script (tty2), upstart linked to an init script (rsyslog), and no upstart +# script - only an init script (apache2) + %w(tty2 rsyslog apache2).each do |pkg| + on agent, puppet_resource("package #{pkg} ensure=present") + + step "Ensure #{pkg} has started" + on agent, "service #{pkg} start", :acceptable_exit_codes => [0,1] + + step "Check that status for running #{pkg}" + check_service_for(pkg, "start", agent) + + step "Stop #{pkg} with `puppet resource'" + on agent, puppet_resource("service #{pkg} ensure=stopped") + + step "Check that status for stopped #{pkg}" + check_service_for(pkg, "stop", agent) + + step "Start #{pkg} with `puppet resource'" + on agent, puppet_resource("service #{pkg} ensure=running") + + step "Check that status for started #{pkg}" + check_service_for(pkg, "start", agent) + end +ensure + on agent, puppet_resource("package apache2 ensure=absent") +end diff --git a/lib/puppet/provider/service/upstart.rb b/lib/puppet/provider/service/upstart.rb index 7892bec61..a767d7cc6 100755 --- a/lib/puppet/provider/service/upstart.rb +++ b/lib/puppet/provider/service/upstart.rb @@ -1,87 +1,331 @@ +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, which have replaced `initd` services on Ubuntu. For `upstart` documentation, see . " # confine to :ubuntu for now because I haven't tested on other platforms confine :operatingsystem => :ubuntu #[:ubuntu, :fedora, :debian] - + 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 + has_feature :enableable def self.instances 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]}" else line.split.first end instances << new(:name => name) } } instances end + def self.defpath + ["/etc/init", "/etc/init.d"] + end + + def upstart_version + @@upstart_version ||= SemVer.new(initctl(" --version").match(/initctl \(upstart (\d\.\d[\.\d]?)\)/)[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| + fqname = File.join(path,name+suffix) + if File.exists?(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 = File.open(initscript).read + if upstart_version < "0.6.7" + enabled_pre_0_6_7?(script_contents) + elsif upstart_version < "0.9.0" + enabled_pre_0_9_0?(script_contents) + elsif upstart_version >= "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 = File.open(initscript).read + if upstart_version < "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 = File.open(initscript).read + if upstart_version < "0.6.7" + disable_pre_0_6_7(script_text) + elsif upstart_version < "0.9.0" + disable_pre_0_9_0(script_text) + elsif upstart_version >= "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 if @resource[:status] is_upstart?(@resource[:status]) ? upstart_status(@resource[:status]) : normal_status elsif is_upstart? upstart_status else super end end - + def normal_status ucommand(:status, false) ($?.exitstatus == 0) ? :running : :stopped end - + def upstart_status(exec = @resource[:name]) output = status_exec(@resource[:name].split) if (! $?.nil?) && (output =~ /start\//) return :running else return :stopped end end - + def is_upstart?(script = initscript) - File.symlink?(script) && File.readlink(script) == "/lib/init/upstart-job" + return true if (File.symlink?(script) && File.readlink(script) == "/lib/init/upstart-job") + return true if (File.file?(script) && (not script.include?("init.d"))) + return false + end + +private + + 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 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 do |line| + if line.match(START_ON) + enabled = :true + elsif line.match(MANUAL) + enabled = :false + end + end + over_text.each 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 File.exists?(overscript) + File.open(overscript).read + 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.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.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.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 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 27983f6e1..55cb4150d 100755 --- a/spec/unit/provider/service/upstart_spec.rb +++ b/spec/unit/provider/service/upstart_spec.rb @@ -1,90 +1,499 @@ #!/usr/bin/env rspec require 'spec_helper' provider_class = Puppet::Type.type(:service).provider(:upstart) describe provider_class do + let(:manual) { "\nmanual" } + let(:start_on_default_runlevels) { "\nstart on runlevel [2,3,4,5]" } + + 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 + describe "#instances" do it "should be able to find all instances" do processes = ["rc stop/waiting", "ssh start/running, process 712"].join("\n") provider_class.stubs(:execpipe).yields(processes) provider_class.instances.map {|provider| provider.name}.should =~ ["rc","ssh"] end it "should attach the interface name for network interfaces" do processes = ["network-interface (eth0)"].join("\n") provider_class.stubs(:execpipe).yields(processes) provider_class.instances.first.name.should == "network-interface INTERFACE=eth0" end end describe "#status" do it "should allow the user to override the status command" do resource = Puppet::Type.type(:service).new(:name => "foo", :provider => :upstart, :status => "/bin/foo") provider = provider_class.new(resource) # Because we stub execution, we also need to stub the result of it, or a # previously failing command execution will cause this test to do the # wrong thing. provider.expects(:ucommand) $?.stubs(:exitstatus).returns(0) provider.status.should == :running end 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 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 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 - + describe "when init script" do before(:each) do provider.stubs(:is_upstart?).returns(false) end ["start", "stop", "status"].each do |command| it "should return the #{command}cmd of its parent provider" do provider.expects(:search).with('foo').returns("/etc/init.d/foo") provider.send("#{command}cmd".to_sym).should == ["/etc/init.d/foo", command.to_sym] end 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