Vendor the external cookbooks

Knife-Zero doesn't include Berkshelf support, so vendoring everything in
the repo is convenient again
This commit is contained in:
Greg Karékinian
2019-10-13 18:32:56 +02:00
parent aa66743166
commit 049d5dd006
1245 changed files with 100630 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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.
#
module PoiseService
autoload :Error, 'poise_service/error'
autoload :Resources, 'poise_service/resources'
autoload :ServiceMixin, 'poise_service/service_mixin'
autoload :ServiceProviders, 'poise_service/service_providers'
autoload :Utils, 'poise_service/utils'
autoload :VERSION, 'poise_service/version'
end

View File

@@ -0,0 +1,18 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'poise_service/resources'
require 'poise_service/service_providers'

View File

@@ -0,0 +1,20 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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.
#
module PoiseService
class Error < ::Exception
end
end

View File

@@ -0,0 +1,27 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'poise_service/resources/poise_service'
require 'poise_service/resources/poise_service_user'
module PoiseService
# Chef resources and providers for poise-service.
#
# @since 1.0.0
module Resources
end
end

View File

@@ -0,0 +1,165 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'etc'
require 'chef/mash'
require 'chef/resource'
require 'poise'
require 'poise_service/error'
module PoiseService
module Resources
# (see PoiseService::Resource)
module PoiseService
# `poise_service` resource. Provides a unified service interface with a
# dependency injection framework.
#
# @since 1.0.0
# @provides poise_service
# @action enable
# @action disable
# @action start
# @action stop
# @action restart
# @action reload
# @example
# poise_service 'myapp' do
# command 'myapp --serve'
# user 'myuser'
# directory '/home/myapp'
# end
class Resource < Chef::Resource
include Poise(inversion: true)
provides(:poise_service)
actions(:enable, :disable, :start, :stop, :restart, :reload)
# @!attribute service_name
# Name of the service to the underlying init system. Defaults to the name
# of the resource.
# @return [String]
attribute(:service_name, kind_of: String, name_attribute: true)
# @!attribute command
# Command to run inside the service. This command must remain in the
# foreground and not daemoinize itself.
# @return [String]
attribute(:command, kind_of: String, required: true)
# @!attribute user
# User to run the service as. See {UserResource} for an easy way to
# create service users. Defaults to root.
# @return [String]
attribute(:user, kind_of: String, default: 'root')
# @!attribute directory
# Working directory for the service. Defaults to the home directory of
# the configured user or / if not found.
# @return [String]
attribute(:directory, kind_of: String, default: lazy { default_directory })
# @!attribute environment
# Environment variables for the service.
# @return [Hash]
attribute(:environment, kind_of: Hash, default: lazy { Mash.new })
# @!attribute stop_signal
# Signal to use to stop the service. Some systems will fall back to
# KILL if this signal fails to stop the process. Defaults to TERM.
# @return [String, Symbol, Integer]
attribute(:stop_signal, kind_of: [String, Symbol, Integer], default: 'TERM')
# @!attribute reload_signal
# Signal to use to reload the service. Defaults to HUP.
# @return [String, Symbol, Integer]
attribute(:reload_signal, kind_of: [String, Symbol, Integer], default: 'HUP')
# @!attribute restart_on_update
# If true, the service will be restarted if the service definition or
# configuration changes. If 'immediately', the notification will happen
# in immediate mode.
# @return [Boolean, String]
attribute(:restart_on_update, equal_to: [true, false, 'immediately', :immediately], default: true)
# Resource DSL callback.
#
# @api private
def after_created
# Set signals to clean values.
stop_signal(clean_signal(stop_signal))
reload_signal(clean_signal(reload_signal))
end
# Return the PID of the main process for this service or nil if the service
# isn't running or the PID cannot be found.
#
# @return [Integer, nil]
# @example
# execute "kill -WINCH #{resources('poise_test[myapp]').pid}"
def pid
# :pid isn't a real action, but this should still work.
provider_for_action(:pid).pid
end
private
# Try to find the home diretory for the configured user. This will fail if
# nsswitch.conf was changed during this run such as with LDAP. Defaults to
# the system root directory.
#
# @see #directory
# @return [String]
def default_directory
# Default fallback.
sysroot = case node['platform_family']
when 'windows'
ENV.fetch('SystemRoot', 'C:\\')
else
'/'
end
# For root we always want the system root path.
return sysroot if user == 'root'
# Force a reload in case any users were created earlier in the run.
Etc.endpwent
# ArgumentError means we can't find the user, possibly nsswitch caching?
home = begin
Dir.home(user)
rescue ArgumentError
sysroot
end
# If the home doesn't exist or is empty, use sysroot.
home = sysroot if home.empty? || !::File.directory?(home)
home
end
# Clean up a signal string/integer. Ints are mapped to the signal name,
# and strings are reformatted to upper case and without the SIG.
#
# @see #stop_signal
# @param signal [String, Symbol, Integer] Signal value to clean.
# @return [String]
def clean_signal(signal)
if signal.is_a?(Integer)
raise Error.new("Unknown signal #{signal}") unless (0..31).include?(signal)
Signal.signame(signal)
else
short_sig = signal.to_s.upcase
short_sig = short_sig[3..-1] if short_sig.start_with?('SIG')
raise Error.new("Unknown signal #{signal}") unless Signal.list.include?(short_sig)
short_sig
end
end
# Providers can be found under service_providers/.
end
end
end
end

View File

@@ -0,0 +1,240 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'chef/provider'
require 'poise'
module PoiseService
module Resources
# (see PoiseServiceTest::Resource)
module PoiseServiceTest
# A `poise_service_test` resource for integration testing service providers.
# This is used in Test-Kitchen tests to ensure all providers behave
# similarly.
#
# @since 1.0.0
# @provides poise_service_test
# @action run
# @example
# poise_service_test 'upstart' do
# service_provider :upstart
# base_port 5000
# end
class Resource < Chef::Resource
include Poise
provides(:poise_service_test)
actions(:run)
# @!attribute service_provider
# Service provider to set for the test group.
# @return [Symbol]
attribute(:service_provider, kind_of: Symbol)
# @!attribute service_options
# Service options to set for the test group.
# @return [Hash, nil]
attribute(:service_options, kind_of: [Hash, NilClass])
# @!attribute base_port
# Port number to start from for the test group.
# @return [Integer]
attribute(:base_port, kind_of: Integer)
end
# Provider for `poise_service_test`.
#
# @see Resource
# @provides poise_service_test
class Provider < Chef::Provider
include Poise
provides(:poise_service_test)
SERVICE_SCRIPT = <<-EOH
require 'webrick'
require 'json'
require 'etc'
FILE_DATA = ''
server = WEBrick::HTTPServer.new(Port: ARGV[0].to_i)
server.mount_proc '/' do |req, res|
res.body = {
directory: Dir.getwd,
user: Etc.getpwuid(Process.uid).name,
euser: Etc.getpwuid(Process.euid).name,
group: Etc.getgrgid(Process.gid).name,
egroup: Etc.getgrgid(Process.egid).name,
environment: ENV.to_hash,
file_data: FILE_DATA,
pid: Process.pid,
}.to_json
end
EOH
# `run` action for `poise_service_test`. Create all test services.
#
# @return [void]
def action_run
notifying_block do
create_script
create_noterm_script
create_user
create_tests
end
end
private
def create_script
file '/usr/bin/poise_test' do
owner 'root'
group 'root'
mode '755'
content <<-EOH
#!/opt/chef/embedded/bin/ruby
#{SERVICE_SCRIPT}
def load_file
FILE_DATA.replace(IO.read(ARGV[1]))
end
if ARGV[1]
load_file
trap('HUP') do
load_file
end
end
server.start
EOH
end
end
def create_noterm_script
file '/usr/bin/poise_test_noterm' do
owner 'root'
group 'root'
mode '755'
content <<-EOH
#!/opt/chef/embedded/bin/ruby
trap('HUP', 'IGNORE')
trap('TERM', 'IGNORE')
#{SERVICE_SCRIPT}
while true
begin
server.start
rescue Exception
rescue StandardError
end
end
EOH
end
end
def create_user
poise_service_user 'poise' do
home '/tmp'
end
end
def create_tests
poise_service "poise_test_#{new_resource.name}" do
if new_resource.service_provider
provider new_resource.service_provider
options new_resource.service_provider, new_resource.service_options if new_resource.service_options
end
command "/usr/bin/poise_test #{new_resource.base_port}"
end
poise_service "poise_test_#{new_resource.name}_params" do
if new_resource.service_provider
provider new_resource.service_provider
options new_resource.service_provider, new_resource.service_options if new_resource.service_options
end
command "/usr/bin/poise_test #{new_resource.base_port + 1}"
environment POISE_ENV: new_resource.name
user 'poise'
end
poise_service "poise_test_#{new_resource.name}_noterm" do
if new_resource.service_provider
provider new_resource.service_provider
options new_resource.service_provider, new_resource.service_options if new_resource.service_options
end
action [:enable, :disable]
command "/usr/bin/poise_test_noterm #{new_resource.base_port + 2}"
stop_signal 'kill'
end
{'restart' => 3, 'reload' => 4}.each do |action, port|
# Stop it before writing the file so we always start with first.
poise_service "poise_test_#{new_resource.name}_#{action} stop" do
if new_resource.service_provider
provider new_resource.service_provider
options new_resource.service_provider, new_resource.service_options if new_resource.service_options
end
action(:disable)
service_name "poise_test_#{new_resource.name}_#{action}"
end
# Write the content to the read on service launch.
file "/etc/poise_test_#{new_resource.name}_#{action}" do
content 'first'
end
# Launch the service, reading in first.
poise_service "poise_test_#{new_resource.name}_#{action}" do
if new_resource.service_provider
provider new_resource.service_provider
options new_resource.service_provider, new_resource.service_options if new_resource.service_options
end
command "/usr/bin/poise_test #{new_resource.base_port + port} /etc/poise_test_#{new_resource.name}_#{action}"
end
# Rewrite the file to second, restart/reload to trigger an update.
file "/etc/poise_test_#{new_resource.name}_#{action} again" do
path "/etc/poise_test_#{new_resource.name}_#{action}"
content 'second'
notifies action.to_sym, "poise_service[poise_test_#{new_resource.name}_#{action}]"
end
end
# Test the #pid accessor.
ruby_block "/tmp/poise_test_#{new_resource.name}_pid" do
block do
pid = resources("poise_service[poise_test_#{new_resource.name}]").pid
IO.write("/tmp/poise_test_#{new_resource.name}_pid", pid.to_s)
end
end
# Test changing the service definition itself.
poise_service "poise_test_#{new_resource.name}_change" do
if new_resource.service_provider
provider new_resource.service_provider
options new_resource.service_provider, new_resource.service_options if new_resource.service_options
end
command "/usr/bin/poise_test #{new_resource.base_port + 5}"
end
poise_service "poise_test_#{new_resource.name}_change_second" do
service_name "poise_test_#{new_resource.name}_change"
if new_resource.service_provider
provider new_resource.service_provider
options new_resource.service_provider, new_resource.service_options if new_resource.service_options
end
command "/usr/bin/poise_test #{new_resource.base_port + 6}"
end
end
end
end
end
end

View File

@@ -0,0 +1,186 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'chef/provider'
require 'poise'
module PoiseService
module Resources
# (see PoiseServiceUser::Resource)
# @since 1.0.0
module PoiseServiceUser
# Shells to look for in order.
# @api private
DEFAULT_SHELLS = %w{/bin/nologin /usr/bin/nologin /bin/false}
# A `poise_service_user` resource to create service users/groups.
#
# @since 1.0.0
# @provides poise_service_user
# @action create
# @action remove
# @example
# poise_service_user 'myapp' do
# home '/var/tmp'
# group 'nogroup'
# end
class Resource < Chef::Resource
include Poise
provides(:poise_service_user)
actions(:create, :remove)
# @!attribute user
# Name of the user to create. Defaults to the name of the resource.
# @return [String]
attribute(:user, kind_of: String, name_attribute: true)
# @!attribute group
# Name of the group to create. Defaults to the name of the user,
# except on Windows where it defaults to false. Set to false to
# disable group creation.
# @return [String, false]
attribute(:group, kind_of: [String, FalseClass], default: lazy { default_group })
# @!attribute uid
# UID of the user to create. Optional, if not set the UID will be
# allocated automatically.
# @return [Integer]
attribute(:uid, kind_of: Integer)
# @!attribute gid
# GID of the group to create. Optional, if not set the GID will be
# allocated automatically.
# @return [Integer]
attribute(:gid, kind_of: Integer)
# @!attribute shell
# Login shell for the user. Optional, if not set the shell will be
# determined automatically.
# @return [String]
attribute(:shell, kind_of: String, default: lazy { default_shell })
# @!attribute home
# Home directory of the user. This directory will not be created if it
# does not exist. Optional.
# @return [String]
attribute(:home, kind_of: String)
private
# Find a default shell for service users. Tries to use nologin, but fall
# back on false.
#
# @api private
# @return [String]
def default_shell
DEFAULT_SHELLS.find {|s| ::File.exist?(s) } || DEFAULT_SHELLS.last
end
# Find the default group name. Returns false on Windows because service
# groups aren't needed there. Otherwise use the name of the service user.
#
# @api private
# @return [String, false]
def default_group
if node.platform_family?('windows')
false
else
user
end
end
end
# Provider for `poise_service_user`.
#
# @since 1.0.0
# @see Resource
# @provides poise_service_user
class Provider < Chef::Provider
include Poise
provides(:poise_service_user)
# `create` action for `poise_service_user`. Ensure the user and group (if
# enabled) exist.
#
# @return [void]
def action_create
notifying_block do
create_group if new_resource.group
create_user
end
end
# `remove` action for `poise_service_user`. Ensure the user and group (if
# enabled) are destroyed.
#
# @return [void]
def action_remove
notifying_block do
remove_user
remove_group if new_resource.group
end
end
private
# Create the system group.
#
# @api private
# @return [void]
def create_group
group new_resource.group do
gid new_resource.gid
# Solaris doesn't support the idea of system groups.
system true unless node.platform_family?('solaris2')
end
end
# Create the system user.
#
# @api private
# @return [void]
def create_user
user new_resource.user do
comment "Service user for #{new_resource.name}"
gid new_resource.group if new_resource.group
home new_resource.home
shell new_resource.shell
# Solaris doesn't support the idea of system users.
system true unless node.platform_family?('solaris2')
uid new_resource.uid
end
end
# Remove the system group.
#
# @api private
# @return [void]
def remove_group
create_group.tap do |r|
r.action(:remove)
end
end
# Remove the system user.
#
# @api private
# @return [void]
def remove_user
create_user.tap do |r|
r.action(:remove)
end
end
end
end
end
end

View File

@@ -0,0 +1,193 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'poise'
require 'poise_service/resources/poise_service'
module PoiseService
# Mixin for application services. This is any resource that will be part of
# an application deployment and involves running a persistent service.
#
# @since 1.0.0
# @example
# module MyApp
# class Resource < Chef::Resource
# include Poise
# provides(:my_app)
# include PoiseService::ServiceMixin
# end
#
# class Provider < Chef::Provider
# include Poise
# provides(:my_app)
# include PoiseService::ServiceMixin
#
# def action_enable
# notifying_block do
# template '/etc/myapp.conf' do
# # ...
# end
# end
# super
# end
#
# def service_options(r)
# r.command('myapp --serve')
# end
# end
# end
module ServiceMixin
include Poise::Utils::ResourceProviderMixin
# Mixin for service wrapper resources.
#
# @see ServiceMixin
module Resource
include Poise::Resource
module ClassMethods
# @api private
def included(klass)
super
klass.extend(ClassMethods)
klass.class_exec do
actions(:enable, :disable, :start, :stop, :restart, :reload)
attribute(:service_name, kind_of: String, name_attribute: true)
end
end
end
extend ClassMethods
end
# Mixin for service wrapper providers.
#
# @see ServiceMixin
module Provider
include Poise::Provider
# Default enable action for service wrappers.
#
# @return [void]
def action_enable
notify_if_service do
service_resource.run_action(:enable)
end
end
# Default disable action for service wrappers.
#
# @return [void]
def action_disable
notify_if_service do
service_resource.run_action(:disable)
end
end
# Default start action for service wrappers.
#
# @return [void]
def action_start
notify_if_service do
service_resource.run_action(:start)
end
end
# Default stop action for service wrappers.
#
# @return [void]
def action_stop
notify_if_service do
service_resource.run_action(:stop)
end
end
# Default restart action for service wrappers.
#
# @return [void]
def action_restart
notify_if_service do
service_resource.run_action(:restart)
end
end
# Default reload action for service wrappers.
#
# @return [void]
def action_reload
notify_if_service do
service_resource.run_action(:reload)
end
end
# @todo Add reload once poise-service supports it.
private
# Set the current resource as notified if the provided block updates the
# service resource.
#
# @api public
# @param block [Proc] Block to run.
# @return [void]
# @example
# notify_if_service do
# service_resource.run_action(:enable)
# end
def notify_if_service(&block)
service_resource.updated_by_last_action(false)
block.call if block
new_resource.updated_by_last_action(true) if service_resource.updated_by_last_action?
end
# Service resource for this service wrapper. This returns a
# poise_service resource that will not be added to the resource
# collection. Override {#service_options} to set service resource
# parameters.
#
# @api public
# @return [Chef::Resource]
# @example
# service_resource.run_action(:restart)
def service_resource
@service_resource ||= PoiseService::Resources::PoiseService::Resource.new(new_resource.name, run_context).tap do |r|
# Set some defaults.
r.declared_type = :poise_service
r.enclosing_provider = self
r.source_line = new_resource.source_line
r.service_name(new_resource.service_name)
# Call the subclass hook for more specific settings.
service_options(r)
end
end
# Abstract hook to set parameters on {#service_resource} when it is
# created. This is required to set at least `resource.command`.
#
# @api public
# @param resource [Chef::Resource] Resource instance to set parameters on.
# @return [void]
# @example
# def service_options(resource)
# resource.command('myapp --serve')
# end
def service_options(resource)
end
end
end
end

View File

@@ -0,0 +1,38 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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/platform/provider_priority_map'
require 'poise_service/service_providers/dummy'
require 'poise_service/service_providers/inittab'
require 'poise_service/service_providers/systemd'
require 'poise_service/service_providers/sysvinit'
require 'poise_service/service_providers/upstart'
module PoiseService
# Inversion providers for the poise_service resource.
#
# @since 1.0.0
module ServiceProviders
# Set up priority maps
Chef::Platform::ProviderPriorityMap.instance.priority(:poise_service, [
PoiseService::ServiceProviders::Systemd,
PoiseService::ServiceProviders::Upstart,
PoiseService::ServiceProviders::Sysvinit,
])
end
end

View File

@@ -0,0 +1,196 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'poise'
module PoiseService
module ServiceProviders
class Base < Chef::Provider
include Poise(inversion: :poise_service)
# Extend the default lookup behavior to check for service_name too.
#
# @api private
def self.resolve_inversion_provider(node, resource)
attrs = resolve_inversion_attribute(node)
(attrs[resource.service_name] && attrs[resource.service_name]['provider']) || super
end
# Extend the default options to check for service_name too.
#
# @api private
def self.inversion_options(node, resource)
super.tap do |opts|
attrs = resolve_inversion_attribute(node)
opts.update(attrs[resource.service_name]) if attrs[resource.service_name]
run_state = Mash.new(node.run_state.fetch('poise_inversion', {}).fetch(inversion_resource, {}))[resource.service_name] || {}
opts.update(run_state['*']) if run_state['*']
opts.update(run_state[provides]) if run_state[provides]
end
end
# Cache the service hints to improve performance. This is called from the
# provides_auto? on most service providers and hits the filesystem a lot.
#
# @return [Array<Symbol>]
def self.service_resource_hints
@@service_resource_hints ||= Chef::Platform::ServiceHelpers.service_resource_providers
end
def action_enable
include_recipe(*Array(recipes)) if recipes
notifying_block do
create_service
end
enable_service
action_start
end
def action_disable
action_stop
disable_service
notifying_block do
destroy_service
end
end
def action_start
return if options['never_start']
notify_if_service do
service_resource.run_action(:start)
end
end
def action_stop
return if options['never_stop']
notify_if_service do
service_resource.run_action(:stop)
end
end
def action_restart
return if options['never_restart']
notify_if_service do
service_resource.run_action(:restart)
end
end
def action_reload
return if options['never_reload']
notify_if_service do
service_resource.run_action(:reload)
end
end
def pid
raise NotImplementedError
end
private
# Recipes to include for this provider to work. Subclasses can override.
#
# @return [String, Array]
def recipes
end
# Subclass hook to create the required files et al for the service.
def create_service
raise NotImplementedError
end
# Subclass hook to remove the required files et al for the service.
def destroy_service
raise NotImplementedError
end
def enable_service
notify_if_service do
service_resource.run_action(:enable)
end
end
def disable_service
notify_if_service do
service_resource.run_action(:disable)
end
end
def notify_if_service(&block)
service_resource.updated_by_last_action(false)
block.call
new_resource.updated_by_last_action(true) if service_resource.updated_by_last_action?
end
# Subclass hook to create the resource used to delegate start, stop, and
# restart actions.
def service_resource
@service_resource ||= Chef::Resource::Service.new(new_resource.service_name, run_context).tap do |r|
r.declared_type = :service
r.enclosing_provider = self
r.source_line = new_resource.source_line
r.supports(status: true, restart: true, reload: true)
end
end
def service_template(path, default_source, &block)
# Sigh scoping.
template path do
owner 'root'
group node['root_group']
mode '644'
if options['template']
# If we have a template override, allow specifying a cookbook via
# "cookbook:template".
parts = options['template'].split(/:/, 2)
if parts.length == 2
source parts[1]
cookbook parts[0]
else
source parts.first
cookbook new_resource.cookbook_name.to_s
end
else
source default_source
cookbook self.poise_defined_in_cookbook
end
variables(
command: options['command'] || new_resource.command,
directory: options['directory'] || new_resource.directory,
environment: options['environment'] || new_resource.environment,
name: new_resource.service_name,
new_resource: new_resource,
options: options,
reload_signal: options['reload_signal'] || new_resource.reload_signal,
stop_signal: options['stop_signal'] || new_resource.stop_signal,
user: options['user'] || new_resource.user,
)
# Don't trigger a restart if the template doesn't already exist, this
# prevents restarting on the run that first creates the service.
restart_on_update = options.fetch('restart_on_update', new_resource.restart_on_update)
if restart_on_update && ::File.exist?(path)
mode = restart_on_update.to_s == 'immediately' ? :immediately : :delayed
notifies :restart, new_resource, mode
end
instance_exec(&block) if block
end
end
end
end
end

View File

@@ -0,0 +1,195 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'etc'
require 'shellwords'
require 'poise_service/service_providers/base'
module PoiseService
module ServiceProviders
class Dummy < Base
provides(:dummy)
# @api private
def self.default_inversion_options(node, resource)
super.merge({
# Time to wait between stop and start.
restart_delay: 1,
})
end
def action_start
return if options['never_start']
return if pid
Chef::Log.debug("[#{new_resource}] Starting #{new_resource.command}")
# Clear the pid file if it exists.
::File.unlink(pid_file) if ::File.exist?(pid_file)
if Process.fork
# Parent, wait for the final child to write the pid file.
now = Time.now
until ::File.exist?(pid_file)
sleep(1)
# After 30 seconds, show output at a higher level to avoid too much
# confusing on failed process launches.
if Time.now - now <= 30
Chef::Log.debug("[#{new_resource}] Waiting for PID file")
else
Chef::Log.warning("[#{new_resource}] Waiting for PID file at #{pid_file} to be created")
end
end
else
# :nocov:
begin
Chef::Log.debug("[#{new_resource}] Forked")
# First child, daemonize and go to town. This handles multi-fork,
# setsid, and shutting down stdin/out/err.
Process.daemon(true)
Chef::Log.debug("[#{new_resource}] Daemonized")
# Daemonized, set up process environment.
Dir.chdir(new_resource.directory)
Chef::Log.debug("[#{new_resource}] Directory changed to #{new_resource.directory}")
ENV['HOME'] = Dir.home(new_resource.user)
new_resource.environment.each do |key, val|
ENV[key.to_s] = val.to_s
end
Chef::Log.debug("[#{new_resource}] Process environment configured")
# Make sure to open the output file and write the pid file before we
# drop privs.
output = ::File.open(output_file, 'ab')
IO.write(pid_file, Process.pid)
Chef::Log.debug("[#{new_resource}] PID #{Process.pid} written to #{pid_file}")
ent = Etc.getpwnam(new_resource.user)
if Process.euid != ent.uid || Process.egid != ent.gid
Process.initgroups(ent.name, ent.gid)
Process::GID.change_privilege(ent.gid) if Process.egid != ent.gid
Process::UID.change_privilege(ent.uid) if Process.euid != ent.uid
Chef::Log.debug("[#{new_resource}] Changed privs to #{new_resource.user} (#{ent.uid}:#{ent.gid})")
end
# Log the command. Happens before ouput redirect or this ends up in the file.
Chef::Log.debug("[#{new_resource}] Execing #{new_resource.command}")
# Set up output logging.
Chef::Log.debug("[#{new_resource}] Logging output to #{output_file}")
$stdout.reopen(output)
$stdout.sync = true
$stderr.reopen(output)
$stderr.sync = true
$stdout.write("#{Time.now} Starting #{new_resource.command}")
# Split the command so we don't get an extra sh -c.
Kernel.exec(*Shellwords.split(new_resource.command))
# Just in case, bail out.
$stdout.reopen(STDOUT)
$stderr.reopen(STDERR)
Chef::Log.debug("[#{new_resource}] Exec failed, bailing out.")
exit!
rescue Exception => e
# Welp, we tried.
$stdout.reopen(STDOUT)
$stderr.reopen(STDERR)
Chef::Log.error("[#{new_resource}] Error during process spawn: #{e}")
exit!
end
# :nocov:
end
Chef::Log.debug("[#{new_resource}] Started.")
end
def action_stop
return if options['never_stop']
return unless pid
Chef::Log.debug("[#{new_resource}] Stopping with #{new_resource.stop_signal}. Current PID is #{pid.inspect}.")
Process.kill(new_resource.stop_signal, pid)
::File.unlink(pid_file)
end
def action_restart
return if options['never_restart']
action_stop
# Give things a moment to stop before we try starting again.
sleep(options['restart_delay'])
action_start
end
def action_reload
return if options['never_reload']
return unless pid
Chef::Log.debug("[#{new_resource}] Reloading with #{new_resource.reload_signal}. Current PID is #{pid.inspect}.")
Process.kill(new_resource.reload_signal, pid)
end
def pid
return nil unless ::File.exist?(pid_file)
pid = IO.read(pid_file).to_i
begin
# Check if the PID is running.
Process.kill(0, pid)
pid
rescue Errno::ESRCH
nil
end
end
private
def service_resource
# Intentionally not implemented.
raise NotImplementedError
end
def enable_service
end
# Write all major service parameters to a file so that if they change, we
# can restart the service. This also makes debuggin a bit easier so you
# can still see what it thinks it was starting without sifting through
# piles of debug output.
def create_service
service_template(run_file, 'dummy.json.erb')
end
def disable_service
end
# Delete the tracking file.
def destroy_service
file run_file do
action :delete
end
file pid_file do
action :delete
end
end
# Path to the run parameters tracking file.
def run_file
"/var/run/#{new_resource.service_name}.json"
end
# Path to the PID file.
def pid_file
"/var/run/#{new_resource.service_name}.pid"
end
# Path to the output file.
def output_file
"/var/run/#{new_resource.service_name}.out"
end
end
end
end

View File

@@ -0,0 +1,150 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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/util/file_edit'
require 'poise_service/service_providers/base'
module PoiseService
module ServiceProviders
class Inittab < Base
provides(:inittab)
def self.provides_auto?(node, resource)
::File.exist?('/etc/inittab')
end
def pid
IO.read(pid_file).to_i if ::File.exist?(pid_file)
end
# Don't try to stop when disabling because we can't.
def action_disable
disable_service
notifying_block do
destroy_service
end
end
def action_start
Chef::Log.debug("[#{new_resource}] Inittab services are always started.")
end
def action_stop
raise NotImplementedError.new("[#{new_resource}] Inittab services cannot be stopped")
end
def action_restart
return if options['never_restart']
# Just kill it and let init restart it.
Process.kill(new_resource.stop_signal, pid) if pid
end
def action_reload
return if options['never_reload']
Process.kill(new_resource.reload_signal, pid) if pid
end
private
def service_resource
# Intentionally not implemented.
raise NotImplementedError
end
def enable_service
end
def disable_service
end
def create_service
# Sigh scoping.
pid_file_ = pid_file
# Inittab only allows 127 characters for the command, so cram stuff in
# a file. Writing to a file is gross, but so is using inittab so ¯\_(ツ)_/¯.
service_template("/sbin/poise_service_#{new_resource.service_name}", 'inittab.sh.erb') do
mode '755'
variables.update(
pid_file: pid_file_,
)
end
# Add to inittab.
edit_inittab do |content|
inittab_line = "#{service_id}:2345:respawn:/sbin/poise_service_#{new_resource.service_name}"
if content =~ /^# #{Regexp.escape(service_tag)}$/
# Existing line, update in place.
content.gsub!(/^(# #{Regexp.escape(service_tag)}\n)(.*)$/, "\\1#{inittab_line}")
else
# Add to the end.
content << "# #{service_tag}\n#{inittab_line}\n"
end
end
end
def destroy_service
# Remove from inittab.
edit_inittab do |content|
content.gsub!(/^# #{Regexp.escape(service_tag)}\n.*?\n$/, '')
end
file "/sbin/poise_service_#{new_resource.service_name}" do
action :delete
end
file pid_file do
action :delete
end
end
# The shortened ID because sysvinit only allows 4 characters.
def service_id
# This is a terrible hash, but it should be good enough.
options['service_id'] || begin
sum = new_resource.service_name.sum(20).to_s(36)
if sum.length < 4
'p' + sum
else
sum
end
end
end
# Tag to put in a comment in inittab for tracking.
def service_tag
"poise_service(#{new_resource.service_name})"
end
def pid_file
options['pid_file'] || "/var/run/#{new_resource.service_name}.pid"
end
def edit_inittab(&block)
inittab = IO.read('/etc/inittab')
original_inittab = inittab.dup
block.call(inittab)
if inittab != original_inittab
file '/etc/inittab' do
content inittab
end
execute 'telinit q'
end
end
end
end
end

View File

@@ -0,0 +1,85 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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/mixin/shell_out'
require 'poise_service/service_providers/base'
module PoiseService
module ServiceProviders
class Systemd < Base
include Chef::Mixin::ShellOut
provides(:systemd)
# @api private
def self.provides_auto?(node, resource)
service_resource_hints.include?(:systemd)
end
# @api private
def self.default_inversion_options(node, resource)
super.merge({
# Automatically reload systemd on changes.
auto_reload: true,
# Service restart mode.
restart_mode: 'on-failure',
})
end
def pid
cmd = shell_out(%w{systemctl status} + [new_resource.service_name])
if !cmd.error? && cmd.stdout.include?('Active: active (running)') && md = cmd.stdout.match(/Main PID: (\d+)/)
md[1].to_i
else
nil
end
end
private
def service_resource
super.tap do |r|
r.provider(Chef::Provider::Service::Systemd)
end
end
def systemctl_daemon_reload
execute 'systemctl daemon-reload' do
action :nothing
user 'root'
end
end
def create_service
reloader = systemctl_daemon_reload
service_template("/etc/systemd/system/#{new_resource.service_name}.service", 'systemd.service.erb') do
notifies :run, reloader, :immediately if options['auto_reload']
variables.update(auto_reload: options['auto_reload'], restart_mode: options['restart_mode'])
end
end
def destroy_service
reloader = systemctl_daemon_reload
file "/etc/systemd/system/#{new_resource.service_name}.service" do
action :delete
notifies :run, reloader, :immediately if options['auto_reload']
end
end
end
end
end

View File

@@ -0,0 +1,97 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'poise_service/service_providers/base'
module PoiseService
module ServiceProviders
class Sysvinit < Base
provides(:sysvinit)
def self.provides_auto?(node, resource)
[:debian, :redhat, :invokercd].any? {|name| service_resource_hints.include?(name) }
end
def pid
IO.read(pid_file).to_i if ::File.exist?(pid_file)
end
private
def service_resource
super.tap do |r|
r.provider(case node['platform_family']
when 'debian'
Chef::Provider::Service::Debian
when 'rhel', 'amazon'
Chef::Provider::Service::Redhat
else
# Better than nothing I guess? Will fail on enable I think.
Chef::Provider::Service::Init
end)
r.init_command(script_path)
# Pending https://github.com/chef/chef/pull/4709.
r.start_command("#{script_path} start")
r.stop_command("#{script_path} stop")
r.status_command("#{script_path} status")
r.restart_command("#{script_path} restart")
r.reload_command("#{script_path} reload")
end
end
def create_service
# Split the command into the binary and its arguments. This is for
# start-stop-daemon since it treats those differently.
parts = new_resource.command.split(/ /, 2)
daemon = ENV['PATH'].split(/:/)
.map {|path| ::File.absolute_path(parts[0], path) }
.find {|path| ::File.exist?(path) } || parts[0]
# Sigh scoping.
pid_file_ = pid_file
# Render the service template
service_template(script_path, 'sysvinit.sh.erb') do
mode '755'
variables.update(
daemon: daemon,
daemon_options: parts[1].to_s,
pid_file: pid_file_,
pid_file_external: options['pid_file_external'].nil? ? !!options['pid_file'] : options['pid_file_external'],
platform_family: node['platform_family'],
)
end
end
def destroy_service
file script_path do
action :delete
end
file pid_file do
action :delete
end
end
def script_path
options['script_path'] || "/etc/init.d/#{new_resource.service_name}"
end
def pid_file
options['pid_file'] || "/var/run/#{new_resource.service_name}.pid"
end
end
end
end

View File

@@ -0,0 +1,136 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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.
#
# Used in the template.
require 'shellwords'
require 'chef/mixin/shell_out'
require 'poise_service/error'
require 'poise_service/service_providers/base'
module PoiseService
module ServiceProviders
class Upstart < Base
include Chef::Mixin::ShellOut
provides(:upstart)
def self.provides_auto?(node, resource)
service_resource_hints.include?(:upstart)
end
# @api private
def self.default_inversion_options(node, resource)
super.merge({
# Time to wait between stop and start.
restart_delay: 1,
})
end
# True restart in Upstart preserves the original config data, we want the
# more obvious behavior like everything else in the world that restart
# would re-read the updated config file. Use stop+start to get this
# behavior. http://manpages.ubuntu.com/manpages/raring/man8/initctl.8.html
def action_restart
return if options['never_restart']
action_stop
# Give things a moment to stop before we try starting again.
sleep(options['restart_delay'])
action_start
end
# Shim out reload if we have a version that predates reload support.
def action_reload
return if options['never_reload']
if !upstart_features[:reload_signal] && new_resource.reload_signal != 'HUP'
if options[:reload_shim]
Process.kill(new_resource.reload_signal, pid)
else
check_reload_signal!
end
else
super
end
end
def pid
cmd = shell_out(%w{initctl status} + [new_resource.service_name])
if !cmd.error? && md = cmd.stdout.match(/process (\d+)/)
md[1].to_i
else
nil
end
end
private
def service_resource
super.tap do |r|
r.provider(Chef::Provider::Service::Upstart)
end
end
def create_service
check_reload_signal!
# Set features so it will be a closure below.
features = upstart_features
service_template("/etc/init/#{new_resource.service_name}.conf", 'upstart.conf.erb') do
variables.update(
upstart_features: features,
)
end
end
def destroy_service
file "/etc/init/#{new_resource.service_name}.conf" do
action :delete
end
end
def upstart_version
cmd = shell_out(%w{initctl --version})
if !cmd.error? && md = cmd.stdout.match(/upstart ([^)]+)\)/)
md[1]
else
'0'
end
end
def upstart_features
@upstart_features ||= begin
upstart_ver = Gem::Version.new(upstart_version)
versions_added = {
kill_signal: '1.3',
reload_signal: '1.10',
setuid: '1.4',
}
versions_added.inject({}) do |memo, (feature, version)|
memo[feature] = Gem::Requirement.create(">= #{version}").satisfied_by?(upstart_ver)
memo
end
end
end
def check_reload_signal!
if !options['reload_shim'] && !upstart_features[:reload_signal] && new_resource.reload_signal != 'HUP'
raise Error.new("Upstart #{upstart_version} only supports HUP for reload, to use the shim please set the 'reload_shim' options for #{new_resource.to_s}")
end
end
end
end
end

View File

@@ -0,0 +1,45 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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 'pathname'
module PoiseService
# Utility methods for PoiseService.
#
# @api public
# @since 1.0.0
module Utils
# Methods are also available as module-level methods as well as a mixin.
extend self
# Common segments to ignore
COMMON_SEGMENTS = %w{var www current etc}.inject({}) {|memo, seg| memo[seg] = true; memo }
# Parse the service name from a path. Look at the last component of the
# path, ignoring some common names.
#
# @param path [String] Path to parse.
# @return [String]
# @example
# attribute(:service_name, kind_of: String, default: lazy { PoiseService::Utils.parse_service_name(path) })
def parse_service_name(path)
parts = Pathname.new(path).each_filename.to_a.reverse!
# Find the last segment not in common segments, fall back to the last segment.
parts.find {|seg| !COMMON_SEGMENTS[seg] } || parts.first
end
end
end

View File

@@ -0,0 +1,20 @@
#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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.
#
module PoiseService
VERSION = '1.5.2'
end