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,92 @@
# Poise-Service Changelog
## v1.5.2
* Set `declared_type` on the mixin-created `poise_service` resource so it works
correctly with ChefSpec.
## v1.5.1
* Fix the `sysvinit` provider on Amazon Linux under Chef 13.
## v1.5.0
* Added `never_start` and `never_stop` provider options to prevent Chef from starting
or stopping a service.
* Automatically reload systemd when removing a service if auto_reload is enabled.
* Improved dummy provider, records process output to `/var/run/service_name.out`
and a `restart_delay` provider option to the dummy provider to wait between
stopping and starting.
## v1.4.2
* Fix the `noterm` test service to work on Ruby 2.3.
## v1.4.1
* Fix `poise_service_user` on Solaris and make it closer to being usable on Windows.
## v1.4.0
* [#31](https://github.com/poise/poise-service/pull/31) Add `shell` property to
`poise_service_user` resource.
## v1.3.1
* [#25](https://github.com/poise/poise-service/pull/25) Cope with a service user
with an invalid home directory.
* Use the correct default cookbook for `service_template` when used with additional plugins.
## v1.3.0
* Allow setting `pid_file_external false` as a provider option for the `sysvinit`
provider to have non-standard path but keep the internal handling.
* Improved quoting for environment variables in the `inittab` provider.
## v1.2.1
* [#23](https://github.com/poise/poise-service/pull/23) Fix service templates on AIX and FreeBSD to use the correct root group.
## v1.2.0
* The `Restart` mode for systemd services can now be controlled via provider
option and defaults to `on-failure` to match other providers.
## v1.1.2
* [#22](https://github.com/poise/poise-service/pull/22) Set all script commands
for the `sysvinit` provider. This should fix compatibility with EL5.
## v1.1.1
* Fix an incorrect value in `poise_service_test`. This is not relevant to
end-users of `poise-service`.
## v1.1.0
* Added `inittab` provider to manage services using old-fashioned `/etc/inittab`.
## v1.0.4
* Set GID correctly in all service providers.
* Allow overriding the path to the generated sysvinit script.
## v1.0.3
* [#10](https://github.com/poise/poise-service/pull/10) Fixes for ensuring services are restarted when their command or user changes.
* [#11](https://github.com/poise/poise-service/pull/11) Revamp the `sysvinit` provider for non-Debian platforms to be more stable.
* [#12](https://github.com/poise/poise-service/pull/12) Improve the `dummy` provider to handle dropping privs correctly.
## v1.0.2
* Fix a potential infinite loop when starting a service with the dummy provider.
* [#2](https://github.com/poise/poise-service/pull/2) Remove usage of root
default files so uploading with Berkshelf works (for now).
## v1.0.1
* Don't use a shared, mutable default value for `#environment`.
## v1.0.0
* Initial release!

View File

@@ -0,0 +1,448 @@
# Poise-Service Cookbook
[![Build Status](https://img.shields.io/travis/poise/poise-service.svg)](https://travis-ci.org/poise/poise-service)
[![Gem Version](https://img.shields.io/gem/v/poise-service.svg)](https://rubygems.org/gems/poise-service)
[![Cookbook Version](https://img.shields.io/cookbook/v/poise-service.svg)](https://supermarket.chef.io/cookbooks/poise-service)
[![Coverage](https://img.shields.io/codecov/c/github/poise/poise-service.svg)](https://codecov.io/github/poise/poise-service)
[![Gemnasium](https://img.shields.io/gemnasium/poise/poise-service.svg)](https://gemnasium.com/poise/poise-service)
[![License](https://img.shields.io/badge/license-Apache_2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
A [Chef](https://www.chef.io/) cookbook to provide a unified interface for
services.
### What is poise-service?
Poise-service is a tool for developers of "library cookbooks" to define a
service without forcing the end-user of the library to adhere to their choice of
service management framework. The `poise_service` resource represents an
abstract service to be run, which can then be customized by node attributes and
the `poise_service_options` resource. This is a technique called [dependency
injection](https://en.wikipedia.org/wiki/Dependency_injection), and allows a
measure of decoupling between the library and application cookbooks.
### Why would I use poise-service?
Poise-service is most useful for authors of library-style cookbooks, for example
the `apache2`, `mysql`, or `application` cookbooks. When using other service
management options with Chef, the author of the library cookbook has to add
specific code for each service management framework they want to support, often
resulting in a cookbook only supporting the favorite framework of the author or
depending on distribution packages for their init scripts. The `poise_service`
resource allows library cookbook authors a way to write generic code for all
service management frameworks while still allowing users of that cookbook to
choose which service management framework best fits their needs.
### How is this different from the built-in service resource?
Chef includes a `service` resource which allows interacting with certain
service management frameworks such as SysV, Upstart, and systemd.
`poise-service` goes further in that it actually generates the configuration
files needed for the requested service management framework, as well as offering
a dependency injection system for application cookbooks to customize which
framework is used.
### What service management frameworks are supported?
* [SysV (aka /etc/init.d)](#sysvinit)
* [Upstart](#upstart)
* [systemd](#systemd)
* [Inittab](#inittab)
* [Runit](https://github.com/poise/poise-service-runit)
* [Monit](https://github.com/poise/poise-monit#service-provider)
* [Solaris](https://github.com/sh9189/poise-service-solaris)
* [AIX](https://github.com/johnbellone/poise-service-aix)
* *Supervisor (coming soon!)*
## Quick Start
To create a service user and a service to run Apache2:
```ruby
poise_service_user 'www-data'
poise_service 'apache2' do
command '/usr/sbin/apache2 -f /etc/apache2/apache2.conf -DFOREGROUND'
stop_signal 'WINCH'
reload_signal 'USR1'
end
```
or for a hypothetical Rails web application:
```ruby
poise_service_user 'myapp'
poise_service 'myapp-web' do
command 'bundle exec unicorn -p 8080'
user 'myapp'
directory '/srv/myapp'
environment RAILS_ENV: 'production'
end
```
## Resources
### `poise_service`
The `poise_service` resource is the abstract definition of a service.
```ruby
poise_service 'myapp' do
command 'myapp --serve'
environment RAILS_ENV: 'production'
end
```
#### Actions
* `:enable` Create, enable and start the service. *(default)*
* `:disable` Stop, disable, and destroy the service.
* `:start` Start the service.
* `:stop` Stop the service.
* `:restart` Stop and then start the service.
* `:reload` Send the configured reload signal to the service.
#### Attributes
* `service_name` Name of the service. *(name attribute)*
* `command` Command to run for the service. This command must stay in the
foreground and not daemonize itself. *(required)*
* `user` User to run the service as. See
[`poise_service_user`](#poise_service_user) for any easy way to create service
users. *(default: root)*
* `directory` Working directory for the service. *(default: home directory for
user, or / if not found)*
* `environment` Environment variables for the service.
* `stop_signal` Signal to use to stop the service. Some systems will fall back
to SIGKILL if this signal fails to stop the process. *(default: TERM)*
* `reload_signal` Signal to use to reload the service. *(default: HUP)*
* `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. *(default: true)*
#### Service Options
The `poise-service` library offers an additional way to pass configuration
information to the final service called "options". Options are key/value pairs
that are passed down to the service provider and can be used to control how it
creates and manages the service. These can be set in the `poise_service`
resource using the `options` method, in node attributes or via the
`poise_service_options` resource. The options from all sources are merged
together in to a single hash.
When setting options in the resource you can either set them for all providers:
```ruby
poise_service 'myapp' do
command 'myapp --serve'
options status_port: 8000
end
```
or for a single provider:
```ruby
poise_service 'myapp' do
command 'myapp --serve'
options :systemd, after_target: 'network'
end
```
Setting via node attributes is generally how an end-user or application cookbook
will set options to customize services in the library cookbooks they are using.
You can set options for all services or for a single service, by service name
or by resource name:
```ruby
# Global, for all services.
override['poise-service']['options']['after_target'] = 'network'
# Single service.
override['poise-service']['myapp']['template'] = 'myapp.erb'
```
The `poise_service_options` resource is also available to set node attributes
for a specific service in a DSL-friendly way:
```ruby
poise_service_options 'myapp' do
template 'myapp.erb'
restart_on_update false
end
```
Unlike resource attributes, service options can be different for each provider.
Not all providers support the same options so make sure to check the
documentation for each provider to see what options are available.
### `poise_service_options`
The `poise_service_options` resource allows setting per-service options in a
DSL-friendly way. See [the Service Options](#service-options) section for more
information about service options overall.
```ruby
poise_service_options 'myapp' do
template 'myapp.erb'
restart_on_update false
end
```
#### Actions
* `:run` Apply the service options. *(default)*
#### Attributes
* `resource` Name of the service. *(name attribute)*
* `for_provider` Provider to set options for.
All other attribute keys will be used as options data.
### `poise_service_user`
The `poise_service_user` resource is an easy way to create service users. It is
not required to use `poise_service`, it is only a helper.
```ruby
poise_service_user 'myapp' do
home '/srv/myapp'
end
```
#### Actions
* `:create` Create the user and group. *(default)*
* `:remove` Remove the user and group.
#### Attributes
* `user` Name of the user. *(name attribute)*
* `group` Name of the group. Set to `false` to disable group creation. *(name attribute)*
* `uid` UID of the user. *(default: automatic)*
* `gid` GID of the group. *(default: automatic)*
* `home` Home directory of the user.
* `shell` Shell of the user. *(default: /bin/nologin if present or /bin/false)*
## Providers
### `sysvinit`
The `sysvinit` provider supports SystemV-style init systems on Debian-family and
RHEL-family platforms. It will create the `/etc/init.d/<service_name>` script
and enable/disable the service using the platform-specific service resource.
```ruby
poise_service 'myapp' do
provider :sysvinit
command 'myapp --serve'
end
```
By default a PID file will be created in `/var/run/service_name.pid`. You can
use the `pid_file` option detailed below to override this and rely on your
process creating a PID file in the given path.
#### Options
* `pid_file` Path to PID file that the service command will create.
* `pid_file_external` If true, assume the service will create the PID file
itself. *(default: true if `pid_file` option is set)*
* `template` Override the default script template. If you want to use a
template in a different cookbook use `'cookbook:template'`.
* `command` Override the service command.
* `directory` Override the service directory.
* `environment` Override the service environment variables.
* `reload_signal` Override the service reload signal.
* `stop_signal` Override the service stop signal.
* `user` Override the service user.
* `never_start` Never try to start the service.
* `never_stop` Never try to stop the service.
* `never_restart` Never try to restart the service.
* `never_reload` Never try to reload the service.
* `script_path` Override the path to the generated service script.
### `upstart`
The `upstart` provider supports [Upstart](http://upstart.ubuntu.com/). It will
create the `/etc/init/service_name.conf` configuration.
```ruby
poise_service 'myapp' do
provider :upstart
command 'myapp --serve'
end
```
As a wide variety of versions of Upstart are in use in various Linux
distributions, the provider does its best to identify which features are
available and provide shims as appropriate. Most of these should be invisible
however Upstart older than 1.10 does not support setting a `reload signal` so
only SIGHUP can be used. You can set a `reload_shim` option to enable an
internal implementaion of reloading to be used for signals other than SIGHUP,
however as this is implemented inside Chef code, running `initctl reload` would
still result in SIGHUP being sent. For this reason, the feature is disabled by
default and will throw an error if a reload signal other than SIGHUP is used.
#### Options
* `reload_shim` Enable the reload signal shim. See above for a warning about
this feature.
* `template` Override the default configuration template. If you want to use a
template in a different cookbook use `'cookbook:template'`.
* `command` Override the service command.
* `directory` Override the service directory.
* `environment` Override the service environment variables.
* `reload_signal` Override the service reload signal.
* `stop_signal` Override the service stop signal.
* `user` Override the service user.
* `never_start` Never try to start the service.
* `never_stop` Never try to stop the service.
* `never_restart` Never try to restart the service.
* `never_reload` Never try to reload the service.
### `systemd`
The `systemd` provider supports [systemd](http://www.freedesktop.org/wiki/Software/systemd/).
It will create the `/etc/systemd/system/service_name.service` configuration.
```ruby
poise_service 'myapp' do
provider :systemd
command 'myapp --serve'
end
```
#### Options
* `template` Override the default configuration template. If you want to use a
template in a different cookbook use `'cookbook:template'`.
* `command` Override the service command.
* `directory` Override the service directory.
* `environment` Override the service environment variables.
* `reload_signal` Override the service reload signal.
* `stop_signal` Override the service stop signal.
* `user` Override the service user.
* `never_start` Never try to start the service.
* `never_stop` Never try to stop the service.
* `never_restart` Never try to restart the service.
* `never_reload` Never try to reload the service.
* `auto_reload` Run `systemctl daemon-reload` after changes to the unit file. *(default: true)*
* `restart_mode` Restart mode for the generated service unit. *(default: on-failure)*
### `inittab`
The `inittab` provider supports managing services via `/etc/inittab` using
[SystemV Init](http://www.nongnu.org/sysvinit/). This can provide basic
process supervision even on very old *nix machines.
```ruby
poise_service 'myapp' do
provider :inittab
command 'myapp --serve'
end
```
**NOTE:** Inittab does not allow stopping services, and they are started as soon
as they are enabled.
#### Options
* `never_start` Never try to start the service.
* `never_stop` Never try to stop the service.
* `never_restart` Never try to restart the service.
* `never_reload` Never try to reload the service.
* `pid_file` Path to PID file that the service command will create.
* `service_id` Unique 1-4 character tag for the service. Defaults to an
auto-generated hash based on the service name. If these collide, bad things
happen. Don't do that.
### `dummy`
The `dummy` provider supports launching services directly from Chef itself.
This is for testing purposes only and is entirely unsuitable for use in
production. This is mostly useful when used alongside kitchen-docker.
```ruby
poise_service 'myapp' do
provider :dummy
command 'myapp --serve'
end
```
The service information is written to `/var/run`. The PID file is `service_name.pid`,
the command output is `service_name.out`, and the service parameters are in
`service_name.json`.
#### Options
* `never_start` Never try to start the service.
* `never_stop` Never try to stop the service.
* `never_restart` Never try to restart the service.
* `never_reload` Never try to reload the service.
* `restart_delay` Number of seconds to wait between stop and start when
restarting. *(default: 1)*
## ServiceMixin
For the common case of a resource (LWRP or plain Ruby) that roughly maps to
"some config files and a service" poise-service provides a mixin module,
`PoiseService::ServiceMixin`. This mixin adds the standard service actions
(`enable`, `disable`, `start`, `stop`, `restart`, and `reload`) with basic
implementations that call those actions on a `poise_service` resource for you.
You customize the service by defining a `service_options` method on your
provider class:
```ruby
def service_options(service)
# service is the PoiseService::Resource object instance.
service.command "/usr/sbin/#{new_resource.name} -f /etc/#{new_resource.name}/conf/httpd.conf -DFOREGROUND"
service.stop_signal 'WINCH'
service.reload_signal 'USR1'
end
```
You will generally want to override the `enable` action to install things
related to the service like packages, users and configuration files:
```ruby
def action_enable
notifying_block do
package 'apache2'
poise_service_user 'www-data'
template "/etc/#{new_resource.name}/conf/httpd.conf" do
# ...
end
end
# This super call will run the normal service enable,
# creating the service and starting it.
super
end
```
See [the poise_service_test_mixin resource](test/cookbooks/poise-service_test/resources/mixin.rb)
and [provider](test/cookbooks/poise-service_test/providers/mixin.rb) for
examples of using `ServiceMixin` in an LWRP.
## Sponsors
Development sponsored by [Bloomberg](http://www.bloomberg.com/company/technology/).
The Poise test server infrastructure is sponsored by [Rackspace](https://rackspace.com/).
## License
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.

View File

@@ -0,0 +1,19 @@
#
# 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.
#
default['poise-service']['provider'] = 'auto'
default['poise-service']['options'] = {}

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

View File

@@ -0,0 +1,19 @@
#
# 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.
#
raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load]
$LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__)
require "poise_service/cheftie"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
<%= {command: @command,
directory: @directory,
environment: @environment,
name: @name,
reload_signal: @reload_signal,
stop_signal: @stop_signal,
user: @user}.to_json %>

View File

@@ -0,0 +1,15 @@
#!/bin/sh
exec /opt/chef/embedded/bin/ruby <<EOH
require 'etc'
IO.write("<%= @pid_file %>", Process.pid)
Dir.chdir("<%= @directory %>")
ent = Etc.getpwnam("<%= @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
end
(ENV["HOME"] = Dir.home("<%= @user %>")) rescue nil
<%= @environment.map {|key, value| "ENV[#{key.to_s.inspect}] = #{value.to_s.inspect}" }.join("; ") %>
exec(*<%= Shellwords.split(@command).inspect %>)
EOH

View File

@@ -0,0 +1,14 @@
[Unit]
Description=<%= @name %>
[Service]
Environment=<%= @environment.map {|key, val| %Q{"#{key}=#{val}"} }.join(' ') %>
ExecStart=<%= @command %>
ExecReload=/bin/kill -<%= @reload_signal %> $MAINPID
KillSignal=<%= @stop_signal %>
User=<%= @user %>
WorkingDirectory=<%= @directory %>
Restart=<%= @restart_mode %>
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,190 @@
#!/bin/sh
# Init script for <%= @name %> generated by poise-service
#
### BEGIN INIT INFO
# Provides: <%= @name %>
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Init script for <%= @name %>
# Description: Init script for <%= @name %>
### END INIT INFO
<%- if @platform_family == 'debian' -%>
. /lib/lsb/init-functions
_start() {
start-stop-daemon --start --quiet --background \
--pidfile "<%= @pid_file %>"<% unless @pid_file_external %> --make-pidfile<% end %> \
--chuid "<%= @user %>" --chdir "<%= @directory %>" \
--exec "<%= @daemon %>" -- <%= @daemon_options %>
}
_stop() {
start-stop-daemon --stop --quiet --pidfile "<%= @pid_file %>" --user "<%= @user %>" --signal "<%= @stop_signal %>"
}
_status() {
status_of_proc -p "<%= @pid_file %>" "<%= @daemon %>" "<%= @name %>"
}
_reload() {
start-stop-daemon --stop --quiet --pidfile "<%= @pid_file %>" --user "<%= @user %>" --signal "<%= @reload_signal %>"
}
<%- else -%>
_start() {
<%# Implementing this using RedHat's bash helpers is too painful. Sorry. %>
<%# See dummy.rb for a more commented version of this code. %>
/opt/chef/embedded/bin/ruby <<EOH
require 'etc'
pid_file = <%= @pid_file.inspect %>
File.unlink(pid_file) if File.exist?(pid_file)
if Process.fork
sleep(1) until File.exist?(pid_file)
else
Process.daemon(true)
Dir.chdir(<%= @directory.inspect %>)
<%- unless @pid_file_external -%>
IO.write(pid_file, Process.pid)
<%- end -%>
ent = Etc.getpwnam(<%= @user.inspect %>)
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
end
Kernel.exec(*<%= Shellwords.split(@command).inspect %>)
exit!
end
EOH
}
_stop() {
if [ -r "<%= @pid_file %>" ]; then
kill -<%= @stop_signal%> "$(cat "<%= @pid_file %>")"
else
return 0
fi
}
_status() {
if [ -r "<%= @pid_file %>" ]; then
kill -0 "$(cat "<%= @pid_file %>")"
else
return 1
fi
}
_reload() {
if [ -r "<%= @pid_file %>" ]; then
kill -<%= @reload_signal%> "$(cat "<%= @pid_file %>")"
else
return 1
fi
}
<%# Some functions to match LSB %>
log_daemon_msg() {
echo -n "$1"
}
log_progress_msg() {
echo -n "$1"
}
log_warning_msg() {
echo -n "$1"
}
log_failure_msg() {
echo -n "$1"
}
log_end_msg() {
if [ "$1" = 0 ]; then
echo " [ OK ]"
else
echo " [FAILED]"
fi
}
<%- end -%>
set -e
start() {
if _start
then
rc=0
sleep 1
if ! kill -0 "$(cat "<%= @pid_file %>")" >/dev/null 2>&1; then
log_failure_msg "<%= @name %> failed to start"
rc=1
fi
else
rc=1
fi
if [ "$rc" -eq 0 ]; then
log_end_msg 0
else
log_end_msg 1
rm -f "<%= @pid_file %>"
fi
}
<%- @environment.each do |key, val| -%>
export <%= key %>="<%= val %>"
<%- end -%>
export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
case "$1" in
start)
log_daemon_msg "Starting <%= @name %>"
if [ -s "<%= @pid_file %>" ] && kill -0 "$(cat "<%= @pid_file %>")" >/dev/null 2>&1; then
log_progress_msg "apparently already running"
log_end_msg 0
exit 0
fi
start
;;
stop)
log_daemon_msg "Stopping <%= @name %>"
_stop
log_end_msg "$?"
rm -f "<%= @pid_file %>"
;;
reload|force-reload)
log_daemon_msg "Reloading <%= @name %>"
_reload
log_end_msg "$?"
;;
restart)
set +e
log_daemon_msg "Restarting <%= @name %>"
if [ -s "<%= @pid_file %>" ] && kill -0 "$(cat "<%= @pid_file %>")" >/dev/null 2>&1; then
_stop || true
sleep 1
else
log_warning_msg "<%= @name %> not running, attempting to start."
rm -f "<%= @pid_file %>"
fi
start
;;
status)
set +e
_status
exit $?
;;
*)
echo "Usage: /etc/init.d/<%= @name %> {start|stop|reload|force-reload|restart|status}"
exit 1
esac
exit 0

View File

@@ -0,0 +1,49 @@
# <%= @name %> generated by poise-service for <%= @new_resource.to_s %>
description "<%= @name %>"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
respawn limit 10 5
umask 022
chdir <%= @directory %>
<%- @environment.each do |key, val| -%>
env <%= key %>="<%= val %>"
<%- end -%>
<%- if @upstart_features[:setuid] -%>
setuid <%= @user %>
<%- end -%>
<%- if @upstart_features[:kill_signal] -%>
kill signal <%= @stop_signal %>
<%- end -%>
<%- if @upstart_features[:reload_signal] -%>
reload signal <%= @reload_signal %>
<%- end -%>
<%- if @upstart_features[:setuid] -%>
exec <%= @command %>
<%- else -%>
script
exec /opt/chef/embedded/bin/ruby <<EOH
require 'etc'
ent = Etc.getpwnam(<%= @user.inspect %>)
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
end
ENV["HOME"] = Dir.home(<%= @user.inspect %>) rescue nil
exec(*<%= Shellwords.split(@command).inspect %>)
EOH
end script
<%- end -%>
<%- if !@upstart_features[:kill_signal] && @stop_signal != 'TERM' -%>
pre-stop script
PID=`initctl status <%= @name %> | sed 's/^.*process \([0-9]*\)$/\1/'`
if [ -n "$PID" ]; then
kill -<%= @stop_signal %> "$PID"
fi
end script
<%- end -%>