├── .gitignore ├── .kitchen.yml ├── .travis.yml ├── .yardopts ├── .yo-rc.json ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── chef ├── attributes │ └── default.rb └── templates │ └── default │ ├── dummy.json.erb │ ├── inittab.sh.erb │ ├── systemd.service.erb │ ├── sysvinit.sh.erb │ └── upstart.conf.erb ├── lib ├── poise_service.rb └── poise_service │ ├── cheftie.rb │ ├── error.rb │ ├── resources.rb │ ├── resources │ ├── poise_service.rb │ ├── poise_service_test.rb │ └── poise_service_user.rb │ ├── service_mixin.rb │ ├── service_providers.rb │ ├── service_providers │ ├── base.rb │ ├── dummy.rb │ ├── inittab.rb │ ├── systemd.rb │ ├── sysvinit.rb │ └── upstart.rb │ ├── utils.rb │ └── version.rb ├── poise-service.gemspec └── test ├── cookbook ├── metadata.rb ├── providers │ └── mixin.rb ├── recipes │ ├── default.rb │ └── mixin.rb └── resources │ └── mixin.rb ├── gemfiles ├── chef-12.gemfile ├── chef-13.gemfile └── master.gemfile ├── integration └── default │ └── serverspec │ ├── Gemfile │ ├── default_spec.rb │ └── mixin_spec.rb └── spec ├── resources ├── poise_service_spec.rb └── poise_service_user_spec.rb ├── service_mixin_spec.rb ├── service_providers ├── base_spec.rb ├── dummy_spec.rb ├── inittab_spec.rb ├── systemd_spec.rb ├── sysvinit_spec.rb └── upstart_spec.rb ├── spec_helper.rb └── utils_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Berksfile.lock 2 | Gemfile.lock 3 | test/gemfiles/*.lock 4 | .kitchen/ 5 | .kitchen.local.yml 6 | test/docker/ 7 | test/ec2/ 8 | coverage/ 9 | pkg/ 10 | .yardoc/ 11 | doc/ 12 | -------------------------------------------------------------------------------- /.kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | #<% require 'poise_boiler' %> 3 | <%= PoiseBoiler.kitchen(platforms: %w{ubuntu-14.04 ubuntu-16.04 centos-6 centos-7}, driver: 'rackspace') %> 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | cache: bundler 4 | language: ruby 5 | env: 6 | global: 7 | - RACKSPACE_USERNAME=coderanger 8 | - secure: XpFA4AwBW5v4o3IuwKVSCTeVr6jXsW13T6ShGPpru4q+W2Zpcwh1qyBbxkkIWlkNjbhAT7G0HzQOqYcvUssYLEUYUNSlN10hxjpTZxvVj5sGjjhS3iTXbSop0NXzQthNRHfVZeK9ZWc+zP1MHGImjGCkErkin1+vu/SwMfIl2/8= 9 | - secure: k36byJyrxjPXKqMjlhojJJwA3iTgVcy1z8zJzUMf0v6JGLsbLbMLfOxkTwIhuLZ3mFEQHvv0TZ8rm84Mg8pYb95fChF2rZNHasRDDB5rFBd++HaYirSC0kndXpZ5gLBhSZXggDv8ROANgKwgWmI0PDDZz96rR/tPDD7edZIvgfc= 10 | - secure: OZhgvnu2op+rxg6ECSYlWGwaD1xIyOhzQtxqwzA4F/59RiR667JWaSTAmdEDN6SKmrqphxmZatzDEVXLaidbzAC0yAVv7zt4JbkLLZwmLFpq0YL+128sjgI4iUXomJhLxFqZyl8xmDhbu2pZLVyIR0S5Y0VUnu4kTmKDpvBwYsA= 11 | before_install: 12 | - 'if [[ $BUNDLE_GEMFILE == *master.gemfile ]]; then gem update --system; fi' 13 | - gem --version 14 | - gem install bundler 15 | - bundle --version 16 | - 'bundle config --local path ${BUNDLE_PATH:-$(dirname $BUNDLE_GEMFILE)/vendor/bundle}' 17 | - bundle config --local bin $PWD/bin 18 | install: bundle update --jobs=3 --retry=3 19 | script: 20 | - ./bin/rake travis 21 | matrix: 22 | include: 23 | - rvm: 2.3.1 24 | gemfile: test/gemfiles/chef-12.gemfile 25 | - rvm: 2.4.3 26 | gemfile: test/gemfiles/chef-13.gemfile 27 | - rvm: 2.5.0 28 | gemfile: test/gemfiles/master.gemfile 29 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --plugin classmethods 2 | --embed-mixin ClassMethods 3 | --hide-api private 4 | --markup markdown 5 | --hide-void-return 6 | --tag provides:Provides 7 | --tag action:Actions 8 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-poise": { 3 | "created": true, 4 | "name": "poise-service", 5 | "cookbookName": "auto", 6 | "noMinor": true 7 | } 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Poise-Service Changelog 2 | 3 | ## v1.5.2 4 | 5 | * Set `declared_type` on the mixin-created `poise_service` resource so it works 6 | correctly with ChefSpec. 7 | 8 | ## v1.5.1 9 | 10 | * Fix the `sysvinit` provider on Amazon Linux under Chef 13. 11 | 12 | ## v1.5.0 13 | 14 | * Added `never_start` and `never_stop` provider options to prevent Chef from starting 15 | or stopping a service. 16 | * Automatically reload systemd when removing a service if auto_reload is enabled. 17 | * Improved dummy provider, records process output to `/var/run/service_name.out` 18 | and a `restart_delay` provider option to the dummy provider to wait between 19 | stopping and starting. 20 | 21 | ## v1.4.2 22 | 23 | * Fix the `noterm` test service to work on Ruby 2.3. 24 | 25 | ## v1.4.1 26 | 27 | * Fix `poise_service_user` on Solaris and make it closer to being usable on Windows. 28 | 29 | ## v1.4.0 30 | 31 | * [#31](https://github.com/poise/poise-service/pull/31) Add `shell` property to 32 | `poise_service_user` resource. 33 | 34 | ## v1.3.1 35 | 36 | * [#25](https://github.com/poise/poise-service/pull/25) Cope with a service user 37 | with an invalid home directory. 38 | * Use the correct default cookbook for `service_template` when used with additional plugins. 39 | 40 | ## v1.3.0 41 | 42 | * Allow setting `pid_file_external false` as a provider option for the `sysvinit` 43 | provider to have non-standard path but keep the internal handling. 44 | * Improved quoting for environment variables in the `inittab` provider. 45 | 46 | ## v1.2.1 47 | 48 | * [#23](https://github.com/poise/poise-service/pull/23) Fix service templates on AIX and FreeBSD to use the correct root group. 49 | 50 | ## v1.2.0 51 | 52 | * The `Restart` mode for systemd services can now be controlled via provider 53 | option and defaults to `on-failure` to match other providers. 54 | 55 | ## v1.1.2 56 | 57 | * [#22](https://github.com/poise/poise-service/pull/22) Set all script commands 58 | for the `sysvinit` provider. This should fix compatibility with EL5. 59 | 60 | ## v1.1.1 61 | 62 | * Fix an incorrect value in `poise_service_test`. This is not relevant to 63 | end-users of `poise-service`. 64 | 65 | ## v1.1.0 66 | 67 | * Added `inittab` provider to manage services using old-fashioned `/etc/inittab`. 68 | 69 | ## v1.0.4 70 | 71 | * Set GID correctly in all service providers. 72 | * Allow overriding the path to the generated sysvinit script. 73 | 74 | ## v1.0.3 75 | 76 | * [#10](https://github.com/poise/poise-service/pull/10) Fixes for ensuring services are restarted when their command or user changes. 77 | * [#11](https://github.com/poise/poise-service/pull/11) Revamp the `sysvinit` provider for non-Debian platforms to be more stable. 78 | * [#12](https://github.com/poise/poise-service/pull/12) Improve the `dummy` provider to handle dropping privs correctly. 79 | 80 | ## v1.0.2 81 | 82 | * Fix a potential infinite loop when starting a service with the dummy provider. 83 | * [#2](https://github.com/poise/poise-service/pull/2) Remove usage of root 84 | default files so uploading with Berkshelf works (for now). 85 | 86 | ## v1.0.1 87 | 88 | * Don't use a shared, mutable default value for `#environment`. 89 | 90 | ## v1.0.0 91 | 92 | * Initial release! 93 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | source 'https://rubygems.org/' 18 | 19 | gemspec path: File.expand_path('..', __FILE__) 20 | 21 | def dev_gem(name, path: File.join('..', name), github: nil) 22 | path = File.expand_path(File.join('..', path), __FILE__) 23 | if File.exist?(path) 24 | gem name, path: path 25 | elsif github 26 | gem name, git: "https://github.com/#{github}.git" 27 | end 28 | end 29 | 30 | dev_gem 'halite' 31 | dev_gem 'poise' 32 | dev_gem 'poise-boiler' 33 | dev_gem 'poise-profiler' 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poise-Service Cookbook 2 | 3 | [![Build Status](https://img.shields.io/travis/poise/poise-service.svg)](https://travis-ci.org/poise/poise-service) 4 | [![Gem Version](https://img.shields.io/gem/v/poise-service.svg)](https://rubygems.org/gems/poise-service) 5 | [![Cookbook Version](https://img.shields.io/cookbook/v/poise-service.svg)](https://supermarket.chef.io/cookbooks/poise-service) 6 | [![Coverage](https://img.shields.io/codecov/c/github/poise/poise-service.svg)](https://codecov.io/github/poise/poise-service) 7 | [![Gemnasium](https://img.shields.io/gemnasium/poise/poise-service.svg)](https://gemnasium.com/poise/poise-service) 8 | [![License](https://img.shields.io/badge/license-Apache_2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 9 | 10 | A [Chef](https://www.chef.io/) cookbook to provide a unified interface for 11 | services. 12 | 13 | ### What is poise-service? 14 | 15 | Poise-service is a tool for developers of "library cookbooks" to define a 16 | service without forcing the end-user of the library to adhere to their choice of 17 | service management framework. The `poise_service` resource represents an 18 | abstract service to be run, which can then be customized by node attributes and 19 | the `poise_service_options` resource. This is a technique called [dependency 20 | injection](https://en.wikipedia.org/wiki/Dependency_injection), and allows a 21 | measure of decoupling between the library and application cookbooks. 22 | 23 | ### Why would I use poise-service? 24 | 25 | Poise-service is most useful for authors of library-style cookbooks, for example 26 | the `apache2`, `mysql`, or `application` cookbooks. When using other service 27 | management options with Chef, the author of the library cookbook has to add 28 | specific code for each service management framework they want to support, often 29 | resulting in a cookbook only supporting the favorite framework of the author or 30 | depending on distribution packages for their init scripts. The `poise_service` 31 | resource allows library cookbook authors a way to write generic code for all 32 | service management frameworks while still allowing users of that cookbook to 33 | choose which service management framework best fits their needs. 34 | 35 | ### How is this different from the built-in service resource? 36 | 37 | Chef includes a `service` resource which allows interacting with certain 38 | service management frameworks such as SysV, Upstart, and systemd. 39 | `poise-service` goes further in that it actually generates the configuration 40 | files needed for the requested service management framework, as well as offering 41 | a dependency injection system for application cookbooks to customize which 42 | framework is used. 43 | 44 | ### What service management frameworks are supported? 45 | 46 | * [SysV (aka /etc/init.d)](#sysvinit) 47 | * [Upstart](#upstart) 48 | * [systemd](#systemd) 49 | * [Inittab](#inittab) 50 | * [Runit](https://github.com/poise/poise-service-runit) 51 | * [Monit](https://github.com/poise/poise-monit#service-provider) 52 | * [Solaris](https://github.com/sh9189/poise-service-solaris) 53 | * [AIX](https://github.com/johnbellone/poise-service-aix) 54 | * *Supervisor (coming soon!)* 55 | 56 | 57 | ## Quick Start 58 | 59 | To create a service user and a service to run Apache2: 60 | 61 | ```ruby 62 | poise_service_user 'www-data' 63 | 64 | poise_service 'apache2' do 65 | command '/usr/sbin/apache2 -f /etc/apache2/apache2.conf -DFOREGROUND' 66 | stop_signal 'WINCH' 67 | reload_signal 'USR1' 68 | end 69 | ``` 70 | 71 | or for a hypothetical Rails web application: 72 | 73 | ```ruby 74 | poise_service_user 'myapp' 75 | 76 | poise_service 'myapp-web' do 77 | command 'bundle exec unicorn -p 8080' 78 | user 'myapp' 79 | directory '/srv/myapp' 80 | environment RAILS_ENV: 'production' 81 | end 82 | ``` 83 | 84 | ## Resources 85 | 86 | ### `poise_service` 87 | 88 | The `poise_service` resource is the abstract definition of a service. 89 | 90 | ```ruby 91 | poise_service 'myapp' do 92 | command 'myapp --serve' 93 | environment RAILS_ENV: 'production' 94 | end 95 | ``` 96 | 97 | #### Actions 98 | 99 | * `:enable` – Create, enable and start the service. *(default)* 100 | * `:disable` – Stop, disable, and destroy the service. 101 | * `:start` – Start the service. 102 | * `:stop` – Stop the service. 103 | * `:restart` – Stop and then start the service. 104 | * `:reload` – Send the configured reload signal to the service. 105 | 106 | #### Attributes 107 | 108 | * `service_name` – Name of the service. *(name attribute)* 109 | * `command` – Command to run for the service. This command must stay in the 110 | foreground and not daemonize itself. *(required)* 111 | * `user` – User to run the service as. See 112 | [`poise_service_user`](#poise_service_user) for any easy way to create service 113 | users. *(default: root)* 114 | * `directory` – Working directory for the service. *(default: home directory for 115 | user, or / if not found)* 116 | * `environment` – Environment variables for the service. 117 | * `stop_signal` – Signal to use to stop the service. Some systems will fall back 118 | to SIGKILL if this signal fails to stop the process. *(default: TERM)* 119 | * `reload_signal` – Signal to use to reload the service. *(default: HUP)* 120 | * `restart_on_update` – If true, the service will be restarted if the service 121 | definition or configuration changes. If `'immediately'`, the notification will 122 | happen in immediate mode. *(default: true)* 123 | 124 | #### Service Options 125 | 126 | The `poise-service` library offers an additional way to pass configuration 127 | information to the final service called "options". Options are key/value pairs 128 | that are passed down to the service provider and can be used to control how it 129 | creates and manages the service. These can be set in the `poise_service` 130 | resource using the `options` method, in node attributes or via the 131 | `poise_service_options` resource. The options from all sources are merged 132 | together in to a single hash. 133 | 134 | When setting options in the resource you can either set them for all providers: 135 | 136 | ```ruby 137 | poise_service 'myapp' do 138 | command 'myapp --serve' 139 | options status_port: 8000 140 | end 141 | ``` 142 | 143 | or for a single provider: 144 | 145 | ```ruby 146 | poise_service 'myapp' do 147 | command 'myapp --serve' 148 | options :systemd, after_target: 'network' 149 | end 150 | ``` 151 | 152 | Setting via node attributes is generally how an end-user or application cookbook 153 | will set options to customize services in the library cookbooks they are using. 154 | You can set options for all services or for a single service, by service name 155 | or by resource name: 156 | 157 | ```ruby 158 | # Global, for all services. 159 | override['poise-service']['options']['after_target'] = 'network' 160 | # Single service. 161 | override['poise-service']['myapp']['template'] = 'myapp.erb' 162 | ``` 163 | 164 | The `poise_service_options` resource is also available to set node attributes 165 | for a specific service in a DSL-friendly way: 166 | 167 | ```ruby 168 | poise_service_options 'myapp' do 169 | template 'myapp.erb' 170 | restart_on_update false 171 | end 172 | ``` 173 | 174 | Unlike resource attributes, service options can be different for each provider. 175 | Not all providers support the same options so make sure to check the 176 | documentation for each provider to see what options are available. 177 | 178 | ### `poise_service_options` 179 | 180 | The `poise_service_options` resource allows setting per-service options in a 181 | DSL-friendly way. See [the Service Options](#service-options) section for more 182 | information about service options overall. 183 | 184 | ```ruby 185 | poise_service_options 'myapp' do 186 | template 'myapp.erb' 187 | restart_on_update false 188 | end 189 | ``` 190 | 191 | #### Actions 192 | 193 | * `:run` – Apply the service options. *(default)* 194 | 195 | #### Attributes 196 | 197 | * `resource` – Name of the service. *(name attribute)* 198 | * `for_provider` – Provider to set options for. 199 | 200 | All other attribute keys will be used as options data. 201 | 202 | ### `poise_service_user` 203 | 204 | The `poise_service_user` resource is an easy way to create service users. It is 205 | not required to use `poise_service`, it is only a helper. 206 | 207 | ```ruby 208 | poise_service_user 'myapp' do 209 | home '/srv/myapp' 210 | end 211 | ``` 212 | 213 | #### Actions 214 | 215 | * `:create` – Create the user and group. *(default)* 216 | * `:remove` – Remove the user and group. 217 | 218 | #### Attributes 219 | 220 | * `user` – Name of the user. *(name attribute)* 221 | * `group` – Name of the group. Set to `false` to disable group creation. *(name attribute)* 222 | * `uid` – UID of the user. *(default: automatic)* 223 | * `gid` – GID of the group. *(default: automatic)* 224 | * `home` – Home directory of the user. 225 | * `shell` – Shell of the user. *(default: /bin/nologin if present or /bin/false)* 226 | 227 | ## Providers 228 | 229 | ### `sysvinit` 230 | 231 | The `sysvinit` provider supports SystemV-style init systems on Debian-family and 232 | RHEL-family platforms. It will create the `/etc/init.d/` script 233 | and enable/disable the service using the platform-specific service resource. 234 | 235 | ```ruby 236 | poise_service 'myapp' do 237 | provider :sysvinit 238 | command 'myapp --serve' 239 | end 240 | ``` 241 | 242 | By default a PID file will be created in `/var/run/service_name.pid`. You can 243 | use the `pid_file` option detailed below to override this and rely on your 244 | process creating a PID file in the given path. 245 | 246 | #### Options 247 | 248 | * `pid_file` – Path to PID file that the service command will create. 249 | * `pid_file_external` – If true, assume the service will create the PID file 250 | itself. *(default: true if `pid_file` option is set)* 251 | * `template` – Override the default script template. If you want to use a 252 | template in a different cookbook use `'cookbook:template'`. 253 | * `command` – Override the service command. 254 | * `directory` – Override the service directory. 255 | * `environment` – Override the service environment variables. 256 | * `reload_signal` – Override the service reload signal. 257 | * `stop_signal` – Override the service stop signal. 258 | * `user` – Override the service user. 259 | * `never_start` – Never try to start the service. 260 | * `never_stop` – Never try to stop the service. 261 | * `never_restart` – Never try to restart the service. 262 | * `never_reload` – Never try to reload the service. 263 | * `script_path` – Override the path to the generated service script. 264 | 265 | ### `upstart` 266 | 267 | The `upstart` provider supports [Upstart](http://upstart.ubuntu.com/). It will 268 | create the `/etc/init/service_name.conf` configuration. 269 | 270 | ```ruby 271 | poise_service 'myapp' do 272 | provider :upstart 273 | command 'myapp --serve' 274 | end 275 | ``` 276 | 277 | As a wide variety of versions of Upstart are in use in various Linux 278 | distributions, the provider does its best to identify which features are 279 | available and provide shims as appropriate. Most of these should be invisible 280 | however Upstart older than 1.10 does not support setting a `reload signal` so 281 | only SIGHUP can be used. You can set a `reload_shim` option to enable an 282 | internal implementaion of reloading to be used for signals other than SIGHUP, 283 | however as this is implemented inside Chef code, running `initctl reload` would 284 | still result in SIGHUP being sent. For this reason, the feature is disabled by 285 | default and will throw an error if a reload signal other than SIGHUP is used. 286 | 287 | #### Options 288 | 289 | * `reload_shim` – Enable the reload signal shim. See above for a warning about 290 | this feature. 291 | * `template` – Override the default configuration template. If you want to use a 292 | template in a different cookbook use `'cookbook:template'`. 293 | * `command` – Override the service command. 294 | * `directory` – Override the service directory. 295 | * `environment` – Override the service environment variables. 296 | * `reload_signal` – Override the service reload signal. 297 | * `stop_signal` – Override the service stop signal. 298 | * `user` – Override the service user. 299 | * `never_start` – Never try to start the service. 300 | * `never_stop` – Never try to stop the service. 301 | * `never_restart` – Never try to restart the service. 302 | * `never_reload` – Never try to reload the service. 303 | 304 | ### `systemd` 305 | 306 | The `systemd` provider supports [systemd](http://www.freedesktop.org/wiki/Software/systemd/). 307 | It will create the `/etc/systemd/system/service_name.service` configuration. 308 | 309 | 310 | ```ruby 311 | poise_service 'myapp' do 312 | provider :systemd 313 | command 'myapp --serve' 314 | end 315 | ``` 316 | 317 | #### Options 318 | 319 | * `template` – Override the default configuration template. If you want to use a 320 | template in a different cookbook use `'cookbook:template'`. 321 | * `command` – Override the service command. 322 | * `directory` – Override the service directory. 323 | * `environment` – Override the service environment variables. 324 | * `reload_signal` – Override the service reload signal. 325 | * `stop_signal` – Override the service stop signal. 326 | * `user` – Override the service user. 327 | * `never_start` – Never try to start the service. 328 | * `never_stop` – Never try to stop the service. 329 | * `never_restart` – Never try to restart the service. 330 | * `never_reload` – Never try to reload the service. 331 | * `auto_reload` – Run `systemctl daemon-reload` after changes to the unit file. *(default: true)* 332 | * `restart_mode` – Restart mode for the generated service unit. *(default: on-failure)* 333 | 334 | ### `inittab` 335 | 336 | The `inittab` provider supports managing services via `/etc/inittab` using 337 | [SystemV Init](http://www.nongnu.org/sysvinit/). This can provide basic 338 | process supervision even on very old *nix machines. 339 | 340 | ```ruby 341 | poise_service 'myapp' do 342 | provider :inittab 343 | command 'myapp --serve' 344 | end 345 | ``` 346 | 347 | **NOTE:** Inittab does not allow stopping services, and they are started as soon 348 | as they are enabled. 349 | 350 | #### Options 351 | 352 | * `never_start` – Never try to start the service. 353 | * `never_stop` – Never try to stop the service. 354 | * `never_restart` – Never try to restart the service. 355 | * `never_reload` – Never try to reload the service. 356 | * `pid_file` – Path to PID file that the service command will create. 357 | * `service_id` – Unique 1-4 character tag for the service. Defaults to an 358 | auto-generated hash based on the service name. If these collide, bad things 359 | happen. Don't do that. 360 | 361 | ### `dummy` 362 | 363 | The `dummy` provider supports launching services directly from Chef itself. 364 | This is for testing purposes only and is entirely unsuitable for use in 365 | production. This is mostly useful when used alongside kitchen-docker. 366 | 367 | ```ruby 368 | poise_service 'myapp' do 369 | provider :dummy 370 | command 'myapp --serve' 371 | end 372 | ``` 373 | 374 | The service information is written to `/var/run`. The PID file is `service_name.pid`, 375 | the command output is `service_name.out`, and the service parameters are in 376 | `service_name.json`. 377 | 378 | #### Options 379 | 380 | * `never_start` – Never try to start the service. 381 | * `never_stop` – Never try to stop the service. 382 | * `never_restart` – Never try to restart the service. 383 | * `never_reload` – Never try to reload the service. 384 | * `restart_delay` – Number of seconds to wait between stop and start when 385 | restarting. *(default: 1)* 386 | 387 | ## ServiceMixin 388 | 389 | For the common case of a resource (LWRP or plain Ruby) that roughly maps to 390 | "some config files and a service" poise-service provides a mixin module, 391 | `PoiseService::ServiceMixin`. This mixin adds the standard service actions 392 | (`enable`, `disable`, `start`, `stop`, `restart`, and `reload`) with basic 393 | implementations that call those actions on a `poise_service` resource for you. 394 | You customize the service by defining a `service_options` method on your 395 | provider class: 396 | 397 | ```ruby 398 | def service_options(service) 399 | # service is the PoiseService::Resource object instance. 400 | service.command "/usr/sbin/#{new_resource.name} -f /etc/#{new_resource.name}/conf/httpd.conf -DFOREGROUND" 401 | service.stop_signal 'WINCH' 402 | service.reload_signal 'USR1' 403 | end 404 | ``` 405 | 406 | You will generally want to override the `enable` action to install things 407 | related to the service like packages, users and configuration files: 408 | 409 | ```ruby 410 | def action_enable 411 | notifying_block do 412 | package 'apache2' 413 | poise_service_user 'www-data' 414 | template "/etc/#{new_resource.name}/conf/httpd.conf" do 415 | # ... 416 | end 417 | end 418 | # This super call will run the normal service enable, 419 | # creating the service and starting it. 420 | super 421 | end 422 | ``` 423 | 424 | See [the poise_service_test_mixin resource](test/cookbooks/poise-service_test/resources/mixin.rb) 425 | and [provider](test/cookbooks/poise-service_test/providers/mixin.rb) for 426 | examples of using `ServiceMixin` in an LWRP. 427 | 428 | ## Sponsors 429 | 430 | Development sponsored by [Bloomberg](http://www.bloomberg.com/company/technology/). 431 | 432 | The Poise test server infrastructure is sponsored by [Rackspace](https://rackspace.com/). 433 | 434 | ## License 435 | 436 | Copyright 2015-2016, Noah Kantrowitz 437 | 438 | Licensed under the Apache License, Version 2.0 (the "License"); 439 | you may not use this file except in compliance with the License. 440 | You may obtain a copy of the License at 441 | 442 | http://www.apache.org/licenses/LICENSE-2.0 443 | 444 | Unless required by applicable law or agreed to in writing, software 445 | distributed under the License is distributed on an "AS IS" BASIS, 446 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 447 | See the License for the specific language governing permissions and 448 | limitations under the License. 449 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_boiler/rakefile' 18 | -------------------------------------------------------------------------------- /chef/attributes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | default['poise-service']['provider'] = 'auto' 18 | 19 | default['poise-service']['options'] = {} 20 | -------------------------------------------------------------------------------- /chef/templates/default/dummy.json.erb: -------------------------------------------------------------------------------- 1 | <%= {command: @command, 2 | directory: @directory, 3 | environment: @environment, 4 | name: @name, 5 | reload_signal: @reload_signal, 6 | stop_signal: @stop_signal, 7 | user: @user}.to_json %> 8 | -------------------------------------------------------------------------------- /chef/templates/default/inittab.sh.erb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec /opt/chef/embedded/bin/ruby <", Process.pid) 5 | Dir.chdir("<%= @directory %>") 6 | ent = Etc.getpwnam("<%= @user %>") 7 | if Process.euid != ent.uid || Process.egid != ent.gid 8 | Process.initgroups(ent.name, ent.gid) 9 | Process::GID.change_privilege(ent.gid) if Process.egid != ent.gid 10 | Process::UID.change_privilege(ent.uid) if Process.euid != ent.uid 11 | end 12 | (ENV["HOME"] = Dir.home("<%= @user %>")) rescue nil 13 | <%= @environment.map {|key, value| "ENV[#{key.to_s.inspect}] = #{value.to_s.inspect}" }.join("; ") %> 14 | exec(*<%= Shellwords.split(@command).inspect %>) 15 | EOH 16 | -------------------------------------------------------------------------------- /chef/templates/default/systemd.service.erb: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=<%= @name %> 3 | 4 | [Service] 5 | Environment=<%= @environment.map {|key, val| %Q{"#{key}=#{val}"} }.join(' ') %> 6 | ExecStart=<%= @command %> 7 | ExecReload=/bin/kill -<%= @reload_signal %> $MAINPID 8 | KillSignal=<%= @stop_signal %> 9 | User=<%= @user %> 10 | WorkingDirectory=<%= @directory %> 11 | Restart=<%= @restart_mode %> 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /chef/templates/default/sysvinit.sh.erb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Init script for <%= @name %> generated by poise-service 3 | # 4 | ### BEGIN INIT INFO 5 | # Provides: <%= @name %> 6 | # Required-Start: $remote_fs $syslog 7 | # Required-Stop: $remote_fs $syslog 8 | # Default-Start: 2 3 4 5 9 | # Default-Stop: 0 1 6 10 | # Short-Description: Init script for <%= @name %> 11 | # Description: Init script for <%= @name %> 12 | ### END INIT INFO 13 | 14 | <%- if @platform_family == 'debian' -%> 15 | . /lib/lsb/init-functions 16 | 17 | _start() { 18 | start-stop-daemon --start --quiet --background \ 19 | --pidfile "<%= @pid_file %>"<% unless @pid_file_external %> --make-pidfile<% end %> \ 20 | --chuid "<%= @user %>" --chdir "<%= @directory %>" \ 21 | --exec "<%= @daemon %>" -- <%= @daemon_options %> 22 | } 23 | 24 | _stop() { 25 | start-stop-daemon --stop --quiet --pidfile "<%= @pid_file %>" --user "<%= @user %>" --signal "<%= @stop_signal %>" 26 | } 27 | 28 | _status() { 29 | status_of_proc -p "<%= @pid_file %>" "<%= @daemon %>" "<%= @name %>" 30 | } 31 | 32 | _reload() { 33 | start-stop-daemon --stop --quiet --pidfile "<%= @pid_file %>" --user "<%= @user %>" --signal "<%= @reload_signal %>" 34 | } 35 | 36 | <%- else -%> 37 | _start() { 38 | <%# Implementing this using RedHat's bash helpers is too painful. Sorry. %> 39 | <%# See dummy.rb for a more commented version of this code. %> 40 | /opt/chef/embedded/bin/ruby < 43 | File.unlink(pid_file) if File.exist?(pid_file) 44 | if Process.fork 45 | sleep(1) until File.exist?(pid_file) 46 | else 47 | Process.daemon(true) 48 | Dir.chdir(<%= @directory.inspect %>) 49 | <%- unless @pid_file_external -%> 50 | IO.write(pid_file, Process.pid) 51 | <%- end -%> 52 | ent = Etc.getpwnam(<%= @user.inspect %>) 53 | if Process.euid != ent.uid || Process.egid != ent.gid 54 | Process.initgroups(ent.name, ent.gid) 55 | Process::GID.change_privilege(ent.gid) if Process.egid != ent.gid 56 | Process::UID.change_privilege(ent.uid) if Process.euid != ent.uid 57 | end 58 | Kernel.exec(*<%= Shellwords.split(@command).inspect %>) 59 | exit! 60 | end 61 | EOH 62 | } 63 | 64 | _stop() { 65 | if [ -r "<%= @pid_file %>" ]; then 66 | kill -<%= @stop_signal%> "$(cat "<%= @pid_file %>")" 67 | else 68 | return 0 69 | fi 70 | } 71 | 72 | _status() { 73 | if [ -r "<%= @pid_file %>" ]; then 74 | kill -0 "$(cat "<%= @pid_file %>")" 75 | else 76 | return 1 77 | fi 78 | } 79 | 80 | _reload() { 81 | if [ -r "<%= @pid_file %>" ]; then 82 | kill -<%= @reload_signal%> "$(cat "<%= @pid_file %>")" 83 | else 84 | return 1 85 | fi 86 | } 87 | 88 | <%# Some functions to match LSB %> 89 | 90 | log_daemon_msg() { 91 | echo -n "$1" 92 | } 93 | 94 | log_progress_msg() { 95 | echo -n "$1" 96 | } 97 | 98 | log_warning_msg() { 99 | echo -n "$1" 100 | } 101 | 102 | log_failure_msg() { 103 | echo -n "$1" 104 | } 105 | 106 | log_end_msg() { 107 | if [ "$1" = 0 ]; then 108 | echo " [ OK ]" 109 | else 110 | echo " [FAILED]" 111 | fi 112 | } 113 | <%- end -%> 114 | 115 | set -e 116 | 117 | start() { 118 | if _start 119 | then 120 | rc=0 121 | sleep 1 122 | if ! kill -0 "$(cat "<%= @pid_file %>")" >/dev/null 2>&1; then 123 | log_failure_msg "<%= @name %> failed to start" 124 | rc=1 125 | fi 126 | else 127 | rc=1 128 | fi 129 | if [ "$rc" -eq 0 ]; then 130 | log_end_msg 0 131 | else 132 | log_end_msg 1 133 | rm -f "<%= @pid_file %>" 134 | fi 135 | } 136 | 137 | <%- @environment.each do |key, val| -%> 138 | export <%= key %>="<%= val %>" 139 | <%- end -%> 140 | export PATH="${PATH:+$PATH:}/usr/sbin:/sbin" 141 | 142 | case "$1" in 143 | start) 144 | log_daemon_msg "Starting <%= @name %>" 145 | if [ -s "<%= @pid_file %>" ] && kill -0 "$(cat "<%= @pid_file %>")" >/dev/null 2>&1; then 146 | log_progress_msg "apparently already running" 147 | log_end_msg 0 148 | exit 0 149 | fi 150 | start 151 | ;; 152 | 153 | stop) 154 | log_daemon_msg "Stopping <%= @name %>" 155 | _stop 156 | log_end_msg "$?" 157 | rm -f "<%= @pid_file %>" 158 | ;; 159 | 160 | reload|force-reload) 161 | log_daemon_msg "Reloading <%= @name %>" 162 | _reload 163 | log_end_msg "$?" 164 | ;; 165 | 166 | restart) 167 | set +e 168 | log_daemon_msg "Restarting <%= @name %>" 169 | if [ -s "<%= @pid_file %>" ] && kill -0 "$(cat "<%= @pid_file %>")" >/dev/null 2>&1; then 170 | _stop || true 171 | sleep 1 172 | else 173 | log_warning_msg "<%= @name %> not running, attempting to start." 174 | rm -f "<%= @pid_file %>" 175 | fi 176 | start 177 | ;; 178 | 179 | status) 180 | set +e 181 | _status 182 | exit $? 183 | ;; 184 | 185 | *) 186 | echo "Usage: /etc/init.d/<%= @name %> {start|stop|reload|force-reload|restart|status}" 187 | exit 1 188 | esac 189 | 190 | exit 0 191 | -------------------------------------------------------------------------------- /chef/templates/default/upstart.conf.erb: -------------------------------------------------------------------------------- 1 | # <%= @name %> generated by poise-service for <%= @new_resource.to_s %> 2 | 3 | description "<%= @name %>" 4 | 5 | start on runlevel [2345] 6 | stop on runlevel [!2345] 7 | 8 | respawn 9 | respawn limit 10 5 10 | umask 022 11 | chdir <%= @directory %> 12 | <%- @environment.each do |key, val| -%> 13 | env <%= key %>="<%= val %>" 14 | <%- end -%> 15 | <%- if @upstart_features[:setuid] -%> 16 | setuid <%= @user %> 17 | <%- end -%> 18 | <%- if @upstart_features[:kill_signal] -%> 19 | kill signal <%= @stop_signal %> 20 | <%- end -%> 21 | <%- if @upstart_features[:reload_signal] -%> 22 | reload signal <%= @reload_signal %> 23 | <%- end -%> 24 | 25 | <%- if @upstart_features[:setuid] -%> 26 | exec <%= @command %> 27 | <%- else -%> 28 | script 29 | exec /opt/chef/embedded/bin/ruby <) 32 | if Process.euid != ent.uid || Process.egid != ent.gid 33 | Process.initgroups(ent.name, ent.gid) 34 | Process::GID.change_privilege(ent.gid) if Process.egid != ent.gid 35 | Process::UID.change_privilege(ent.uid) if Process.euid != ent.uid 36 | end 37 | ENV["HOME"] = Dir.home(<%= @user.inspect %>) rescue nil 38 | exec(*<%= Shellwords.split(@command).inspect %>) 39 | EOH 40 | end script 41 | <%- end -%> 42 | <%- if !@upstart_features[:kill_signal] && @stop_signal != 'TERM' -%> 43 | pre-stop script 44 | PID=`initctl status <%= @name %> | sed 's/^.*process \([0-9]*\)$/\1/'` 45 | if [ -n "$PID" ]; then 46 | kill -<%= @stop_signal %> "$PID" 47 | fi 48 | end script 49 | <%- end -%> 50 | -------------------------------------------------------------------------------- /lib/poise_service.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | module PoiseService 19 | autoload :Error, 'poise_service/error' 20 | autoload :Resources, 'poise_service/resources' 21 | autoload :ServiceMixin, 'poise_service/service_mixin' 22 | autoload :ServiceProviders, 'poise_service/service_providers' 23 | autoload :Utils, 'poise_service/utils' 24 | autoload :VERSION, 'poise_service/version' 25 | end 26 | -------------------------------------------------------------------------------- /lib/poise_service/cheftie.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_service/resources' 18 | require 'poise_service/service_providers' 19 | -------------------------------------------------------------------------------- /lib/poise_service/error.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | module PoiseService 18 | class Error < ::Exception 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/poise_service/resources.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_service/resources/poise_service' 18 | require 'poise_service/resources/poise_service_user' 19 | 20 | 21 | module PoiseService 22 | # Chef resources and providers for poise-service. 23 | # 24 | # @since 1.0.0 25 | module Resources 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/poise_service/resources/poise_service.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'etc' 18 | 19 | require 'chef/mash' 20 | require 'chef/resource' 21 | require 'poise' 22 | 23 | require 'poise_service/error' 24 | 25 | 26 | module PoiseService 27 | module Resources 28 | # (see PoiseService::Resource) 29 | module PoiseService 30 | # `poise_service` resource. Provides a unified service interface with a 31 | # dependency injection framework. 32 | # 33 | # @since 1.0.0 34 | # @provides poise_service 35 | # @action enable 36 | # @action disable 37 | # @action start 38 | # @action stop 39 | # @action restart 40 | # @action reload 41 | # @example 42 | # poise_service 'myapp' do 43 | # command 'myapp --serve' 44 | # user 'myuser' 45 | # directory '/home/myapp' 46 | # end 47 | class Resource < Chef::Resource 48 | include Poise(inversion: true) 49 | provides(:poise_service) 50 | actions(:enable, :disable, :start, :stop, :restart, :reload) 51 | 52 | # @!attribute service_name 53 | # Name of the service to the underlying init system. Defaults to the name 54 | # of the resource. 55 | # @return [String] 56 | attribute(:service_name, kind_of: String, name_attribute: true) 57 | # @!attribute command 58 | # Command to run inside the service. This command must remain in the 59 | # foreground and not daemoinize itself. 60 | # @return [String] 61 | attribute(:command, kind_of: String, required: true) 62 | # @!attribute user 63 | # User to run the service as. See {UserResource} for an easy way to 64 | # create service users. Defaults to root. 65 | # @return [String] 66 | attribute(:user, kind_of: String, default: 'root') 67 | # @!attribute directory 68 | # Working directory for the service. Defaults to the home directory of 69 | # the configured user or / if not found. 70 | # @return [String] 71 | attribute(:directory, kind_of: String, default: lazy { default_directory }) 72 | # @!attribute environment 73 | # Environment variables for the service. 74 | # @return [Hash] 75 | attribute(:environment, kind_of: Hash, default: lazy { Mash.new }) 76 | # @!attribute stop_signal 77 | # Signal to use to stop the service. Some systems will fall back to 78 | # KILL if this signal fails to stop the process. Defaults to TERM. 79 | # @return [String, Symbol, Integer] 80 | attribute(:stop_signal, kind_of: [String, Symbol, Integer], default: 'TERM') 81 | # @!attribute reload_signal 82 | # Signal to use to reload the service. Defaults to HUP. 83 | # @return [String, Symbol, Integer] 84 | attribute(:reload_signal, kind_of: [String, Symbol, Integer], default: 'HUP') 85 | # @!attribute restart_on_update 86 | # If true, the service will be restarted if the service definition or 87 | # configuration changes. If 'immediately', the notification will happen 88 | # in immediate mode. 89 | # @return [Boolean, String] 90 | attribute(:restart_on_update, equal_to: [true, false, 'immediately', :immediately], default: true) 91 | 92 | # Resource DSL callback. 93 | # 94 | # @api private 95 | def after_created 96 | # Set signals to clean values. 97 | stop_signal(clean_signal(stop_signal)) 98 | reload_signal(clean_signal(reload_signal)) 99 | end 100 | 101 | # Return the PID of the main process for this service or nil if the service 102 | # isn't running or the PID cannot be found. 103 | # 104 | # @return [Integer, nil] 105 | # @example 106 | # execute "kill -WINCH #{resources('poise_test[myapp]').pid}" 107 | def pid 108 | # :pid isn't a real action, but this should still work. 109 | provider_for_action(:pid).pid 110 | end 111 | 112 | private 113 | 114 | # Try to find the home diretory for the configured user. This will fail if 115 | # nsswitch.conf was changed during this run such as with LDAP. Defaults to 116 | # the system root directory. 117 | # 118 | # @see #directory 119 | # @return [String] 120 | def default_directory 121 | # Default fallback. 122 | sysroot = case node['platform_family'] 123 | when 'windows' 124 | ENV.fetch('SystemRoot', 'C:\\') 125 | else 126 | '/' 127 | end 128 | # For root we always want the system root path. 129 | return sysroot if user == 'root' 130 | # Force a reload in case any users were created earlier in the run. 131 | Etc.endpwent 132 | # ArgumentError means we can't find the user, possibly nsswitch caching? 133 | home = begin 134 | Dir.home(user) 135 | rescue ArgumentError 136 | sysroot 137 | end 138 | # If the home doesn't exist or is empty, use sysroot. 139 | home = sysroot if home.empty? || !::File.directory?(home) 140 | home 141 | end 142 | 143 | # Clean up a signal string/integer. Ints are mapped to the signal name, 144 | # and strings are reformatted to upper case and without the SIG. 145 | # 146 | # @see #stop_signal 147 | # @param signal [String, Symbol, Integer] Signal value to clean. 148 | # @return [String] 149 | def clean_signal(signal) 150 | if signal.is_a?(Integer) 151 | raise Error.new("Unknown signal #{signal}") unless (0..31).include?(signal) 152 | Signal.signame(signal) 153 | else 154 | short_sig = signal.to_s.upcase 155 | short_sig = short_sig[3..-1] if short_sig.start_with?('SIG') 156 | raise Error.new("Unknown signal #{signal}") unless Signal.list.include?(short_sig) 157 | short_sig 158 | end 159 | end 160 | 161 | # Providers can be found under service_providers/. 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/poise_service/resources/poise_service_test.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/resource' 18 | require 'chef/provider' 19 | require 'poise' 20 | 21 | 22 | module PoiseService 23 | module Resources 24 | # (see PoiseServiceTest::Resource) 25 | module PoiseServiceTest 26 | # A `poise_service_test` resource for integration testing service providers. 27 | # This is used in Test-Kitchen tests to ensure all providers behave 28 | # similarly. 29 | # 30 | # @since 1.0.0 31 | # @provides poise_service_test 32 | # @action run 33 | # @example 34 | # poise_service_test 'upstart' do 35 | # service_provider :upstart 36 | # base_port 5000 37 | # end 38 | class Resource < Chef::Resource 39 | include Poise 40 | provides(:poise_service_test) 41 | actions(:run) 42 | 43 | # @!attribute service_provider 44 | # Service provider to set for the test group. 45 | # @return [Symbol] 46 | attribute(:service_provider, kind_of: Symbol) 47 | # @!attribute service_options 48 | # Service options to set for the test group. 49 | # @return [Hash, nil] 50 | attribute(:service_options, kind_of: [Hash, NilClass]) 51 | # @!attribute base_port 52 | # Port number to start from for the test group. 53 | # @return [Integer] 54 | attribute(:base_port, kind_of: Integer) 55 | end 56 | 57 | # Provider for `poise_service_test`. 58 | # 59 | # @see Resource 60 | # @provides poise_service_test 61 | class Provider < Chef::Provider 62 | include Poise 63 | provides(:poise_service_test) 64 | 65 | SERVICE_SCRIPT = <<-EOH 66 | require 'webrick' 67 | require 'json' 68 | require 'etc' 69 | FILE_DATA = '' 70 | server = WEBrick::HTTPServer.new(Port: ARGV[0].to_i) 71 | server.mount_proc '/' do |req, res| 72 | res.body = { 73 | directory: Dir.getwd, 74 | user: Etc.getpwuid(Process.uid).name, 75 | euser: Etc.getpwuid(Process.euid).name, 76 | group: Etc.getgrgid(Process.gid).name, 77 | egroup: Etc.getgrgid(Process.egid).name, 78 | environment: ENV.to_hash, 79 | file_data: FILE_DATA, 80 | pid: Process.pid, 81 | }.to_json 82 | end 83 | EOH 84 | 85 | # `run` action for `poise_service_test`. Create all test services. 86 | # 87 | # @return [void] 88 | def action_run 89 | notifying_block do 90 | create_script 91 | create_noterm_script 92 | create_user 93 | create_tests 94 | end 95 | end 96 | 97 | private 98 | 99 | def create_script 100 | file '/usr/bin/poise_test' do 101 | owner 'root' 102 | group 'root' 103 | mode '755' 104 | content <<-EOH 105 | #!/opt/chef/embedded/bin/ruby 106 | #{SERVICE_SCRIPT} 107 | def load_file 108 | FILE_DATA.replace(IO.read(ARGV[1])) 109 | end 110 | if ARGV[1] 111 | load_file 112 | trap('HUP') do 113 | load_file 114 | end 115 | end 116 | server.start 117 | EOH 118 | end 119 | end 120 | 121 | def create_noterm_script 122 | file '/usr/bin/poise_test_noterm' do 123 | owner 'root' 124 | group 'root' 125 | mode '755' 126 | content <<-EOH 127 | #!/opt/chef/embedded/bin/ruby 128 | trap('HUP', 'IGNORE') 129 | trap('TERM', 'IGNORE') 130 | #{SERVICE_SCRIPT} 131 | while true 132 | begin 133 | server.start 134 | rescue Exception 135 | rescue StandardError 136 | end 137 | end 138 | EOH 139 | end 140 | end 141 | 142 | def create_user 143 | poise_service_user 'poise' do 144 | home '/tmp' 145 | end 146 | end 147 | 148 | def create_tests 149 | poise_service "poise_test_#{new_resource.name}" do 150 | if new_resource.service_provider 151 | provider new_resource.service_provider 152 | options new_resource.service_provider, new_resource.service_options if new_resource.service_options 153 | end 154 | command "/usr/bin/poise_test #{new_resource.base_port}" 155 | end 156 | 157 | poise_service "poise_test_#{new_resource.name}_params" do 158 | if new_resource.service_provider 159 | provider new_resource.service_provider 160 | options new_resource.service_provider, new_resource.service_options if new_resource.service_options 161 | end 162 | command "/usr/bin/poise_test #{new_resource.base_port + 1}" 163 | environment POISE_ENV: new_resource.name 164 | user 'poise' 165 | end 166 | 167 | poise_service "poise_test_#{new_resource.name}_noterm" do 168 | if new_resource.service_provider 169 | provider new_resource.service_provider 170 | options new_resource.service_provider, new_resource.service_options if new_resource.service_options 171 | end 172 | action [:enable, :disable] 173 | command "/usr/bin/poise_test_noterm #{new_resource.base_port + 2}" 174 | stop_signal 'kill' 175 | end 176 | 177 | {'restart' => 3, 'reload' => 4}.each do |action, port| 178 | # Stop it before writing the file so we always start with first. 179 | poise_service "poise_test_#{new_resource.name}_#{action} stop" do 180 | if new_resource.service_provider 181 | provider new_resource.service_provider 182 | options new_resource.service_provider, new_resource.service_options if new_resource.service_options 183 | end 184 | action(:disable) 185 | service_name "poise_test_#{new_resource.name}_#{action}" 186 | end 187 | 188 | # Write the content to the read on service launch. 189 | file "/etc/poise_test_#{new_resource.name}_#{action}" do 190 | content 'first' 191 | end 192 | 193 | # Launch the service, reading in first. 194 | poise_service "poise_test_#{new_resource.name}_#{action}" do 195 | if new_resource.service_provider 196 | provider new_resource.service_provider 197 | options new_resource.service_provider, new_resource.service_options if new_resource.service_options 198 | end 199 | command "/usr/bin/poise_test #{new_resource.base_port + port} /etc/poise_test_#{new_resource.name}_#{action}" 200 | end 201 | 202 | # Rewrite the file to second, restart/reload to trigger an update. 203 | file "/etc/poise_test_#{new_resource.name}_#{action} again" do 204 | path "/etc/poise_test_#{new_resource.name}_#{action}" 205 | content 'second' 206 | notifies action.to_sym, "poise_service[poise_test_#{new_resource.name}_#{action}]" 207 | end 208 | end 209 | 210 | # Test the #pid accessor. 211 | ruby_block "/tmp/poise_test_#{new_resource.name}_pid" do 212 | block do 213 | pid = resources("poise_service[poise_test_#{new_resource.name}]").pid 214 | IO.write("/tmp/poise_test_#{new_resource.name}_pid", pid.to_s) 215 | end 216 | end 217 | 218 | # Test changing the service definition itself. 219 | poise_service "poise_test_#{new_resource.name}_change" do 220 | if new_resource.service_provider 221 | provider new_resource.service_provider 222 | options new_resource.service_provider, new_resource.service_options if new_resource.service_options 223 | end 224 | command "/usr/bin/poise_test #{new_resource.base_port + 5}" 225 | end 226 | 227 | poise_service "poise_test_#{new_resource.name}_change_second" do 228 | service_name "poise_test_#{new_resource.name}_change" 229 | if new_resource.service_provider 230 | provider new_resource.service_provider 231 | options new_resource.service_provider, new_resource.service_options if new_resource.service_options 232 | end 233 | command "/usr/bin/poise_test #{new_resource.base_port + 6}" 234 | end 235 | 236 | end 237 | end 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /lib/poise_service/resources/poise_service_user.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/resource' 18 | require 'chef/provider' 19 | require 'poise' 20 | 21 | 22 | module PoiseService 23 | module Resources 24 | # (see PoiseServiceUser::Resource) 25 | # @since 1.0.0 26 | module PoiseServiceUser 27 | # Shells to look for in order. 28 | # @api private 29 | DEFAULT_SHELLS = %w{/bin/nologin /usr/bin/nologin /bin/false} 30 | 31 | # A `poise_service_user` resource to create service users/groups. 32 | # 33 | # @since 1.0.0 34 | # @provides poise_service_user 35 | # @action create 36 | # @action remove 37 | # @example 38 | # poise_service_user 'myapp' do 39 | # home '/var/tmp' 40 | # group 'nogroup' 41 | # end 42 | class Resource < Chef::Resource 43 | include Poise 44 | provides(:poise_service_user) 45 | actions(:create, :remove) 46 | 47 | # @!attribute user 48 | # Name of the user to create. Defaults to the name of the resource. 49 | # @return [String] 50 | attribute(:user, kind_of: String, name_attribute: true) 51 | # @!attribute group 52 | # Name of the group to create. Defaults to the name of the user, 53 | # except on Windows where it defaults to false. Set to false to 54 | # disable group creation. 55 | # @return [String, false] 56 | attribute(:group, kind_of: [String, FalseClass], default: lazy { default_group }) 57 | # @!attribute uid 58 | # UID of the user to create. Optional, if not set the UID will be 59 | # allocated automatically. 60 | # @return [Integer] 61 | attribute(:uid, kind_of: Integer) 62 | # @!attribute gid 63 | # GID of the group to create. Optional, if not set the GID will be 64 | # allocated automatically. 65 | # @return [Integer] 66 | attribute(:gid, kind_of: Integer) 67 | # @!attribute shell 68 | # Login shell for the user. Optional, if not set the shell will be 69 | # determined automatically. 70 | # @return [String] 71 | attribute(:shell, kind_of: String, default: lazy { default_shell }) 72 | # @!attribute home 73 | # Home directory of the user. This directory will not be created if it 74 | # does not exist. Optional. 75 | # @return [String] 76 | attribute(:home, kind_of: String) 77 | 78 | private 79 | 80 | # Find a default shell for service users. Tries to use nologin, but fall 81 | # back on false. 82 | # 83 | # @api private 84 | # @return [String] 85 | def default_shell 86 | DEFAULT_SHELLS.find {|s| ::File.exist?(s) } || DEFAULT_SHELLS.last 87 | end 88 | 89 | # Find the default group name. Returns false on Windows because service 90 | # groups aren't needed there. Otherwise use the name of the service user. 91 | # 92 | # @api private 93 | # @return [String, false] 94 | def default_group 95 | if node.platform_family?('windows') 96 | false 97 | else 98 | user 99 | end 100 | end 101 | end 102 | 103 | # Provider for `poise_service_user`. 104 | # 105 | # @since 1.0.0 106 | # @see Resource 107 | # @provides poise_service_user 108 | class Provider < Chef::Provider 109 | include Poise 110 | provides(:poise_service_user) 111 | 112 | # `create` action for `poise_service_user`. Ensure the user and group (if 113 | # enabled) exist. 114 | # 115 | # @return [void] 116 | def action_create 117 | notifying_block do 118 | create_group if new_resource.group 119 | create_user 120 | end 121 | end 122 | 123 | # `remove` action for `poise_service_user`. Ensure the user and group (if 124 | # enabled) are destroyed. 125 | # 126 | # @return [void] 127 | def action_remove 128 | notifying_block do 129 | remove_user 130 | remove_group if new_resource.group 131 | end 132 | end 133 | 134 | private 135 | 136 | # Create the system group. 137 | # 138 | # @api private 139 | # @return [void] 140 | def create_group 141 | group new_resource.group do 142 | gid new_resource.gid 143 | # Solaris doesn't support the idea of system groups. 144 | system true unless node.platform_family?('solaris2') 145 | end 146 | end 147 | 148 | # Create the system user. 149 | # 150 | # @api private 151 | # @return [void] 152 | def create_user 153 | user new_resource.user do 154 | comment "Service user for #{new_resource.name}" 155 | gid new_resource.group if new_resource.group 156 | home new_resource.home 157 | shell new_resource.shell 158 | # Solaris doesn't support the idea of system users. 159 | system true unless node.platform_family?('solaris2') 160 | uid new_resource.uid 161 | end 162 | end 163 | 164 | # Remove the system group. 165 | # 166 | # @api private 167 | # @return [void] 168 | def remove_group 169 | create_group.tap do |r| 170 | r.action(:remove) 171 | end 172 | end 173 | 174 | # Remove the system user. 175 | # 176 | # @api private 177 | # @return [void] 178 | def remove_user 179 | create_user.tap do |r| 180 | r.action(:remove) 181 | end 182 | end 183 | end 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/poise_service/service_mixin.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise' 18 | 19 | require 'poise_service/resources/poise_service' 20 | 21 | 22 | module PoiseService 23 | # Mixin for application services. This is any resource that will be part of 24 | # an application deployment and involves running a persistent service. 25 | # 26 | # @since 1.0.0 27 | # @example 28 | # module MyApp 29 | # class Resource < Chef::Resource 30 | # include Poise 31 | # provides(:my_app) 32 | # include PoiseService::ServiceMixin 33 | # end 34 | # 35 | # class Provider < Chef::Provider 36 | # include Poise 37 | # provides(:my_app) 38 | # include PoiseService::ServiceMixin 39 | # 40 | # def action_enable 41 | # notifying_block do 42 | # template '/etc/myapp.conf' do 43 | # # ... 44 | # end 45 | # end 46 | # super 47 | # end 48 | # 49 | # def service_options(r) 50 | # r.command('myapp --serve') 51 | # end 52 | # end 53 | # end 54 | module ServiceMixin 55 | include Poise::Utils::ResourceProviderMixin 56 | 57 | # Mixin for service wrapper resources. 58 | # 59 | # @see ServiceMixin 60 | module Resource 61 | include Poise::Resource 62 | 63 | module ClassMethods 64 | # @api private 65 | def included(klass) 66 | super 67 | klass.extend(ClassMethods) 68 | klass.class_exec do 69 | actions(:enable, :disable, :start, :stop, :restart, :reload) 70 | attribute(:service_name, kind_of: String, name_attribute: true) 71 | end 72 | end 73 | end 74 | 75 | extend ClassMethods 76 | end 77 | 78 | # Mixin for service wrapper providers. 79 | # 80 | # @see ServiceMixin 81 | module Provider 82 | include Poise::Provider 83 | 84 | # Default enable action for service wrappers. 85 | # 86 | # @return [void] 87 | def action_enable 88 | notify_if_service do 89 | service_resource.run_action(:enable) 90 | end 91 | end 92 | 93 | # Default disable action for service wrappers. 94 | # 95 | # @return [void] 96 | def action_disable 97 | notify_if_service do 98 | service_resource.run_action(:disable) 99 | end 100 | end 101 | 102 | # Default start action for service wrappers. 103 | # 104 | # @return [void] 105 | def action_start 106 | notify_if_service do 107 | service_resource.run_action(:start) 108 | end 109 | end 110 | 111 | # Default stop action for service wrappers. 112 | # 113 | # @return [void] 114 | def action_stop 115 | notify_if_service do 116 | service_resource.run_action(:stop) 117 | end 118 | end 119 | 120 | # Default restart action for service wrappers. 121 | # 122 | # @return [void] 123 | def action_restart 124 | notify_if_service do 125 | service_resource.run_action(:restart) 126 | end 127 | end 128 | 129 | # Default reload action for service wrappers. 130 | # 131 | # @return [void] 132 | def action_reload 133 | notify_if_service do 134 | service_resource.run_action(:reload) 135 | end 136 | end 137 | 138 | # @todo Add reload once poise-service supports it. 139 | 140 | private 141 | 142 | # Set the current resource as notified if the provided block updates the 143 | # service resource. 144 | # 145 | # @api public 146 | # @param block [Proc] Block to run. 147 | # @return [void] 148 | # @example 149 | # notify_if_service do 150 | # service_resource.run_action(:enable) 151 | # end 152 | def notify_if_service(&block) 153 | service_resource.updated_by_last_action(false) 154 | block.call if block 155 | new_resource.updated_by_last_action(true) if service_resource.updated_by_last_action? 156 | end 157 | 158 | # Service resource for this service wrapper. This returns a 159 | # poise_service resource that will not be added to the resource 160 | # collection. Override {#service_options} to set service resource 161 | # parameters. 162 | # 163 | # @api public 164 | # @return [Chef::Resource] 165 | # @example 166 | # service_resource.run_action(:restart) 167 | def service_resource 168 | @service_resource ||= PoiseService::Resources::PoiseService::Resource.new(new_resource.name, run_context).tap do |r| 169 | # Set some defaults. 170 | r.declared_type = :poise_service 171 | r.enclosing_provider = self 172 | r.source_line = new_resource.source_line 173 | r.service_name(new_resource.service_name) 174 | # Call the subclass hook for more specific settings. 175 | service_options(r) 176 | end 177 | end 178 | 179 | # Abstract hook to set parameters on {#service_resource} when it is 180 | # created. This is required to set at least `resource.command`. 181 | # 182 | # @api public 183 | # @param resource [Chef::Resource] Resource instance to set parameters on. 184 | # @return [void] 185 | # @example 186 | # def service_options(resource) 187 | # resource.command('myapp --serve') 188 | # end 189 | def service_options(resource) 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/poise_service/service_providers.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/platform/provider_priority_map' 18 | 19 | require 'poise_service/service_providers/dummy' 20 | require 'poise_service/service_providers/inittab' 21 | require 'poise_service/service_providers/systemd' 22 | require 'poise_service/service_providers/sysvinit' 23 | require 'poise_service/service_providers/upstart' 24 | 25 | 26 | module PoiseService 27 | # Inversion providers for the poise_service resource. 28 | # 29 | # @since 1.0.0 30 | module ServiceProviders 31 | # Set up priority maps 32 | Chef::Platform::ProviderPriorityMap.instance.priority(:poise_service, [ 33 | PoiseService::ServiceProviders::Systemd, 34 | PoiseService::ServiceProviders::Upstart, 35 | PoiseService::ServiceProviders::Sysvinit, 36 | ]) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/poise_service/service_providers/base.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/provider' 18 | require 'poise' 19 | 20 | 21 | module PoiseService 22 | module ServiceProviders 23 | class Base < Chef::Provider 24 | include Poise(inversion: :poise_service) 25 | 26 | # Extend the default lookup behavior to check for service_name too. 27 | # 28 | # @api private 29 | def self.resolve_inversion_provider(node, resource) 30 | attrs = resolve_inversion_attribute(node) 31 | (attrs[resource.service_name] && attrs[resource.service_name]['provider']) || super 32 | end 33 | 34 | # Extend the default options to check for service_name too. 35 | # 36 | # @api private 37 | def self.inversion_options(node, resource) 38 | super.tap do |opts| 39 | attrs = resolve_inversion_attribute(node) 40 | opts.update(attrs[resource.service_name]) if attrs[resource.service_name] 41 | run_state = Mash.new(node.run_state.fetch('poise_inversion', {}).fetch(inversion_resource, {}))[resource.service_name] || {} 42 | opts.update(run_state['*']) if run_state['*'] 43 | opts.update(run_state[provides]) if run_state[provides] 44 | end 45 | end 46 | 47 | # Cache the service hints to improve performance. This is called from the 48 | # provides_auto? on most service providers and hits the filesystem a lot. 49 | # 50 | # @return [Array] 51 | def self.service_resource_hints 52 | @@service_resource_hints ||= Chef::Platform::ServiceHelpers.service_resource_providers 53 | end 54 | 55 | def action_enable 56 | include_recipe(*Array(recipes)) if recipes 57 | notifying_block do 58 | create_service 59 | end 60 | enable_service 61 | action_start 62 | end 63 | 64 | def action_disable 65 | action_stop 66 | disable_service 67 | notifying_block do 68 | destroy_service 69 | end 70 | end 71 | 72 | def action_start 73 | return if options['never_start'] 74 | notify_if_service do 75 | service_resource.run_action(:start) 76 | end 77 | end 78 | 79 | def action_stop 80 | return if options['never_stop'] 81 | notify_if_service do 82 | service_resource.run_action(:stop) 83 | end 84 | end 85 | 86 | def action_restart 87 | return if options['never_restart'] 88 | notify_if_service do 89 | service_resource.run_action(:restart) 90 | end 91 | end 92 | 93 | def action_reload 94 | return if options['never_reload'] 95 | notify_if_service do 96 | service_resource.run_action(:reload) 97 | end 98 | end 99 | 100 | def pid 101 | raise NotImplementedError 102 | end 103 | 104 | private 105 | 106 | # Recipes to include for this provider to work. Subclasses can override. 107 | # 108 | # @return [String, Array] 109 | def recipes 110 | end 111 | 112 | # Subclass hook to create the required files et al for the service. 113 | def create_service 114 | raise NotImplementedError 115 | end 116 | 117 | # Subclass hook to remove the required files et al for the service. 118 | def destroy_service 119 | raise NotImplementedError 120 | end 121 | 122 | def enable_service 123 | notify_if_service do 124 | service_resource.run_action(:enable) 125 | end 126 | end 127 | 128 | def disable_service 129 | notify_if_service do 130 | service_resource.run_action(:disable) 131 | end 132 | end 133 | 134 | def notify_if_service(&block) 135 | service_resource.updated_by_last_action(false) 136 | block.call 137 | new_resource.updated_by_last_action(true) if service_resource.updated_by_last_action? 138 | end 139 | 140 | # Subclass hook to create the resource used to delegate start, stop, and 141 | # restart actions. 142 | def service_resource 143 | @service_resource ||= Chef::Resource::Service.new(new_resource.service_name, run_context).tap do |r| 144 | r.declared_type = :service 145 | r.enclosing_provider = self 146 | r.source_line = new_resource.source_line 147 | r.supports(status: true, restart: true, reload: true) 148 | end 149 | end 150 | 151 | def service_template(path, default_source, &block) 152 | # Sigh scoping. 153 | template path do 154 | owner 'root' 155 | group node['root_group'] 156 | mode '644' 157 | if options['template'] 158 | # If we have a template override, allow specifying a cookbook via 159 | # "cookbook:template". 160 | parts = options['template'].split(/:/, 2) 161 | if parts.length == 2 162 | source parts[1] 163 | cookbook parts[0] 164 | else 165 | source parts.first 166 | cookbook new_resource.cookbook_name.to_s 167 | end 168 | else 169 | source default_source 170 | cookbook self.poise_defined_in_cookbook 171 | end 172 | variables( 173 | command: options['command'] || new_resource.command, 174 | directory: options['directory'] || new_resource.directory, 175 | environment: options['environment'] || new_resource.environment, 176 | name: new_resource.service_name, 177 | new_resource: new_resource, 178 | options: options, 179 | reload_signal: options['reload_signal'] || new_resource.reload_signal, 180 | stop_signal: options['stop_signal'] || new_resource.stop_signal, 181 | user: options['user'] || new_resource.user, 182 | ) 183 | # Don't trigger a restart if the template doesn't already exist, this 184 | # prevents restarting on the run that first creates the service. 185 | restart_on_update = options.fetch('restart_on_update', new_resource.restart_on_update) 186 | if restart_on_update && ::File.exist?(path) 187 | mode = restart_on_update.to_s == 'immediately' ? :immediately : :delayed 188 | notifies :restart, new_resource, mode 189 | end 190 | instance_exec(&block) if block 191 | end 192 | end 193 | 194 | end 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /lib/poise_service/service_providers/dummy.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'etc' 18 | require 'shellwords' 19 | 20 | require 'poise_service/service_providers/base' 21 | 22 | 23 | module PoiseService 24 | module ServiceProviders 25 | class Dummy < Base 26 | provides(:dummy) 27 | 28 | # @api private 29 | def self.default_inversion_options(node, resource) 30 | super.merge({ 31 | # Time to wait between stop and start. 32 | restart_delay: 1, 33 | }) 34 | end 35 | 36 | def action_start 37 | return if options['never_start'] 38 | return if pid 39 | Chef::Log.debug("[#{new_resource}] Starting #{new_resource.command}") 40 | # Clear the pid file if it exists. 41 | ::File.unlink(pid_file) if ::File.exist?(pid_file) 42 | if Process.fork 43 | # Parent, wait for the final child to write the pid file. 44 | now = Time.now 45 | until ::File.exist?(pid_file) 46 | sleep(1) 47 | # After 30 seconds, show output at a higher level to avoid too much 48 | # confusing on failed process launches. 49 | if Time.now - now <= 30 50 | Chef::Log.debug("[#{new_resource}] Waiting for PID file") 51 | else 52 | Chef::Log.warning("[#{new_resource}] Waiting for PID file at #{pid_file} to be created") 53 | end 54 | end 55 | else 56 | # :nocov: 57 | begin 58 | Chef::Log.debug("[#{new_resource}] Forked") 59 | # First child, daemonize and go to town. This handles multi-fork, 60 | # setsid, and shutting down stdin/out/err. 61 | Process.daemon(true) 62 | Chef::Log.debug("[#{new_resource}] Daemonized") 63 | # Daemonized, set up process environment. 64 | Dir.chdir(new_resource.directory) 65 | Chef::Log.debug("[#{new_resource}] Directory changed to #{new_resource.directory}") 66 | ENV['HOME'] = Dir.home(new_resource.user) 67 | new_resource.environment.each do |key, val| 68 | ENV[key.to_s] = val.to_s 69 | end 70 | Chef::Log.debug("[#{new_resource}] Process environment configured") 71 | # Make sure to open the output file and write the pid file before we 72 | # drop privs. 73 | output = ::File.open(output_file, 'ab') 74 | IO.write(pid_file, Process.pid) 75 | Chef::Log.debug("[#{new_resource}] PID #{Process.pid} written to #{pid_file}") 76 | ent = Etc.getpwnam(new_resource.user) 77 | if Process.euid != ent.uid || Process.egid != ent.gid 78 | Process.initgroups(ent.name, ent.gid) 79 | Process::GID.change_privilege(ent.gid) if Process.egid != ent.gid 80 | Process::UID.change_privilege(ent.uid) if Process.euid != ent.uid 81 | Chef::Log.debug("[#{new_resource}] Changed privs to #{new_resource.user} (#{ent.uid}:#{ent.gid})") 82 | end 83 | # Log the command. Happens before ouput redirect or this ends up in the file. 84 | Chef::Log.debug("[#{new_resource}] Execing #{new_resource.command}") 85 | # Set up output logging. 86 | Chef::Log.debug("[#{new_resource}] Logging output to #{output_file}") 87 | $stdout.reopen(output) 88 | $stdout.sync = true 89 | $stderr.reopen(output) 90 | $stderr.sync = true 91 | $stdout.write("#{Time.now} Starting #{new_resource.command}") 92 | # Split the command so we don't get an extra sh -c. 93 | Kernel.exec(*Shellwords.split(new_resource.command)) 94 | # Just in case, bail out. 95 | $stdout.reopen(STDOUT) 96 | $stderr.reopen(STDERR) 97 | Chef::Log.debug("[#{new_resource}] Exec failed, bailing out.") 98 | exit! 99 | rescue Exception => e 100 | # Welp, we tried. 101 | $stdout.reopen(STDOUT) 102 | $stderr.reopen(STDERR) 103 | Chef::Log.error("[#{new_resource}] Error during process spawn: #{e}") 104 | exit! 105 | end 106 | # :nocov: 107 | end 108 | Chef::Log.debug("[#{new_resource}] Started.") 109 | end 110 | 111 | def action_stop 112 | return if options['never_stop'] 113 | return unless pid 114 | Chef::Log.debug("[#{new_resource}] Stopping with #{new_resource.stop_signal}. Current PID is #{pid.inspect}.") 115 | Process.kill(new_resource.stop_signal, pid) 116 | ::File.unlink(pid_file) 117 | end 118 | 119 | def action_restart 120 | return if options['never_restart'] 121 | action_stop 122 | # Give things a moment to stop before we try starting again. 123 | sleep(options['restart_delay']) 124 | action_start 125 | end 126 | 127 | def action_reload 128 | return if options['never_reload'] 129 | return unless pid 130 | Chef::Log.debug("[#{new_resource}] Reloading with #{new_resource.reload_signal}. Current PID is #{pid.inspect}.") 131 | Process.kill(new_resource.reload_signal, pid) 132 | end 133 | 134 | def pid 135 | return nil unless ::File.exist?(pid_file) 136 | pid = IO.read(pid_file).to_i 137 | begin 138 | # Check if the PID is running. 139 | Process.kill(0, pid) 140 | pid 141 | rescue Errno::ESRCH 142 | nil 143 | end 144 | end 145 | 146 | private 147 | 148 | def service_resource 149 | # Intentionally not implemented. 150 | raise NotImplementedError 151 | end 152 | 153 | def enable_service 154 | end 155 | 156 | # Write all major service parameters to a file so that if they change, we 157 | # can restart the service. This also makes debuggin a bit easier so you 158 | # can still see what it thinks it was starting without sifting through 159 | # piles of debug output. 160 | def create_service 161 | service_template(run_file, 'dummy.json.erb') 162 | end 163 | 164 | def disable_service 165 | end 166 | 167 | # Delete the tracking file. 168 | def destroy_service 169 | file run_file do 170 | action :delete 171 | end 172 | 173 | file pid_file do 174 | action :delete 175 | end 176 | end 177 | 178 | # Path to the run parameters tracking file. 179 | def run_file 180 | "/var/run/#{new_resource.service_name}.json" 181 | end 182 | 183 | # Path to the PID file. 184 | def pid_file 185 | "/var/run/#{new_resource.service_name}.pid" 186 | end 187 | 188 | # Path to the output file. 189 | def output_file 190 | "/var/run/#{new_resource.service_name}.out" 191 | end 192 | 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/poise_service/service_providers/inittab.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/util/file_edit' 18 | 19 | require 'poise_service/service_providers/base' 20 | 21 | 22 | module PoiseService 23 | module ServiceProviders 24 | class Inittab < Base 25 | provides(:inittab) 26 | 27 | def self.provides_auto?(node, resource) 28 | ::File.exist?('/etc/inittab') 29 | end 30 | 31 | def pid 32 | IO.read(pid_file).to_i if ::File.exist?(pid_file) 33 | end 34 | 35 | # Don't try to stop when disabling because we can't. 36 | def action_disable 37 | disable_service 38 | notifying_block do 39 | destroy_service 40 | end 41 | end 42 | 43 | def action_start 44 | Chef::Log.debug("[#{new_resource}] Inittab services are always started.") 45 | end 46 | 47 | def action_stop 48 | raise NotImplementedError.new("[#{new_resource}] Inittab services cannot be stopped") 49 | end 50 | 51 | def action_restart 52 | return if options['never_restart'] 53 | # Just kill it and let init restart it. 54 | Process.kill(new_resource.stop_signal, pid) if pid 55 | end 56 | 57 | def action_reload 58 | return if options['never_reload'] 59 | Process.kill(new_resource.reload_signal, pid) if pid 60 | end 61 | 62 | private 63 | 64 | def service_resource 65 | # Intentionally not implemented. 66 | raise NotImplementedError 67 | end 68 | 69 | def enable_service 70 | end 71 | 72 | def disable_service 73 | end 74 | 75 | def create_service 76 | # Sigh scoping. 77 | pid_file_ = pid_file 78 | # Inittab only allows 127 characters for the command, so cram stuff in 79 | # a file. Writing to a file is gross, but so is using inittab so ¯\_(ツ)_/¯. 80 | service_template("/sbin/poise_service_#{new_resource.service_name}", 'inittab.sh.erb') do 81 | mode '755' 82 | variables.update( 83 | pid_file: pid_file_, 84 | ) 85 | end 86 | # Add to inittab. 87 | edit_inittab do |content| 88 | inittab_line = "#{service_id}:2345:respawn:/sbin/poise_service_#{new_resource.service_name}" 89 | if content =~ /^# #{Regexp.escape(service_tag)}$/ 90 | # Existing line, update in place. 91 | content.gsub!(/^(# #{Regexp.escape(service_tag)}\n)(.*)$/, "\\1#{inittab_line}") 92 | else 93 | # Add to the end. 94 | content << "# #{service_tag}\n#{inittab_line}\n" 95 | end 96 | end 97 | end 98 | 99 | def destroy_service 100 | # Remove from inittab. 101 | edit_inittab do |content| 102 | content.gsub!(/^# #{Regexp.escape(service_tag)}\n.*?\n$/, '') 103 | end 104 | 105 | file "/sbin/poise_service_#{new_resource.service_name}" do 106 | action :delete 107 | end 108 | 109 | file pid_file do 110 | action :delete 111 | end 112 | end 113 | 114 | # The shortened ID because sysvinit only allows 4 characters. 115 | def service_id 116 | # This is a terrible hash, but it should be good enough. 117 | options['service_id'] || begin 118 | sum = new_resource.service_name.sum(20).to_s(36) 119 | if sum.length < 4 120 | 'p' + sum 121 | else 122 | sum 123 | end 124 | end 125 | end 126 | 127 | # Tag to put in a comment in inittab for tracking. 128 | def service_tag 129 | "poise_service(#{new_resource.service_name})" 130 | end 131 | 132 | def pid_file 133 | options['pid_file'] || "/var/run/#{new_resource.service_name}.pid" 134 | end 135 | 136 | def edit_inittab(&block) 137 | inittab = IO.read('/etc/inittab') 138 | original_inittab = inittab.dup 139 | block.call(inittab) 140 | if inittab != original_inittab 141 | file '/etc/inittab' do 142 | content inittab 143 | end 144 | 145 | execute 'telinit q' 146 | end 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/poise_service/service_providers/systemd.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/mixin/shell_out' 18 | 19 | require 'poise_service/service_providers/base' 20 | 21 | 22 | module PoiseService 23 | module ServiceProviders 24 | class Systemd < Base 25 | include Chef::Mixin::ShellOut 26 | provides(:systemd) 27 | 28 | # @api private 29 | def self.provides_auto?(node, resource) 30 | service_resource_hints.include?(:systemd) 31 | end 32 | 33 | # @api private 34 | def self.default_inversion_options(node, resource) 35 | super.merge({ 36 | # Automatically reload systemd on changes. 37 | auto_reload: true, 38 | # Service restart mode. 39 | restart_mode: 'on-failure', 40 | }) 41 | end 42 | 43 | def pid 44 | cmd = shell_out(%w{systemctl status} + [new_resource.service_name]) 45 | if !cmd.error? && cmd.stdout.include?('Active: active (running)') && md = cmd.stdout.match(/Main PID: (\d+)/) 46 | md[1].to_i 47 | else 48 | nil 49 | end 50 | end 51 | 52 | private 53 | 54 | def service_resource 55 | super.tap do |r| 56 | r.provider(Chef::Provider::Service::Systemd) 57 | end 58 | end 59 | 60 | def systemctl_daemon_reload 61 | execute 'systemctl daemon-reload' do 62 | action :nothing 63 | user 'root' 64 | end 65 | end 66 | 67 | def create_service 68 | reloader = systemctl_daemon_reload 69 | service_template("/etc/systemd/system/#{new_resource.service_name}.service", 'systemd.service.erb') do 70 | notifies :run, reloader, :immediately if options['auto_reload'] 71 | variables.update(auto_reload: options['auto_reload'], restart_mode: options['restart_mode']) 72 | end 73 | end 74 | 75 | def destroy_service 76 | reloader = systemctl_daemon_reload 77 | file "/etc/systemd/system/#{new_resource.service_name}.service" do 78 | action :delete 79 | notifies :run, reloader, :immediately if options['auto_reload'] 80 | end 81 | end 82 | 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/poise_service/service_providers/sysvinit.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_service/service_providers/base' 18 | 19 | 20 | module PoiseService 21 | module ServiceProviders 22 | class Sysvinit < Base 23 | provides(:sysvinit) 24 | 25 | def self.provides_auto?(node, resource) 26 | [:debian, :redhat, :invokercd].any? {|name| service_resource_hints.include?(name) } 27 | end 28 | 29 | def pid 30 | IO.read(pid_file).to_i if ::File.exist?(pid_file) 31 | end 32 | 33 | private 34 | 35 | def service_resource 36 | super.tap do |r| 37 | r.provider(case node['platform_family'] 38 | when 'debian' 39 | Chef::Provider::Service::Debian 40 | when 'rhel', 'amazon' 41 | Chef::Provider::Service::Redhat 42 | else 43 | # Better than nothing I guess? Will fail on enable I think. 44 | Chef::Provider::Service::Init 45 | end) 46 | r.init_command(script_path) 47 | # Pending https://github.com/chef/chef/pull/4709. 48 | r.start_command("#{script_path} start") 49 | r.stop_command("#{script_path} stop") 50 | r.status_command("#{script_path} status") 51 | r.restart_command("#{script_path} restart") 52 | r.reload_command("#{script_path} reload") 53 | end 54 | end 55 | 56 | def create_service 57 | # Split the command into the binary and its arguments. This is for 58 | # start-stop-daemon since it treats those differently. 59 | parts = new_resource.command.split(/ /, 2) 60 | daemon = ENV['PATH'].split(/:/) 61 | .map {|path| ::File.absolute_path(parts[0], path) } 62 | .find {|path| ::File.exist?(path) } || parts[0] 63 | # Sigh scoping. 64 | pid_file_ = pid_file 65 | # Render the service template 66 | service_template(script_path, 'sysvinit.sh.erb') do 67 | mode '755' 68 | variables.update( 69 | daemon: daemon, 70 | daemon_options: parts[1].to_s, 71 | pid_file: pid_file_, 72 | pid_file_external: options['pid_file_external'].nil? ? !!options['pid_file'] : options['pid_file_external'], 73 | platform_family: node['platform_family'], 74 | ) 75 | end 76 | end 77 | 78 | def destroy_service 79 | file script_path do 80 | action :delete 81 | end 82 | 83 | file pid_file do 84 | action :delete 85 | end 86 | end 87 | 88 | def script_path 89 | options['script_path'] || "/etc/init.d/#{new_resource.service_name}" 90 | end 91 | 92 | def pid_file 93 | options['pid_file'] || "/var/run/#{new_resource.service_name}.pid" 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/poise_service/service_providers/upstart.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Used in the template. 18 | require 'shellwords' 19 | 20 | require 'chef/mixin/shell_out' 21 | 22 | require 'poise_service/error' 23 | require 'poise_service/service_providers/base' 24 | 25 | 26 | module PoiseService 27 | module ServiceProviders 28 | class Upstart < Base 29 | include Chef::Mixin::ShellOut 30 | provides(:upstart) 31 | 32 | def self.provides_auto?(node, resource) 33 | service_resource_hints.include?(:upstart) 34 | end 35 | 36 | # @api private 37 | def self.default_inversion_options(node, resource) 38 | super.merge({ 39 | # Time to wait between stop and start. 40 | restart_delay: 1, 41 | }) 42 | end 43 | 44 | # True restart in Upstart preserves the original config data, we want the 45 | # more obvious behavior like everything else in the world that restart 46 | # would re-read the updated config file. Use stop+start to get this 47 | # behavior. http://manpages.ubuntu.com/manpages/raring/man8/initctl.8.html 48 | def action_restart 49 | return if options['never_restart'] 50 | action_stop 51 | # Give things a moment to stop before we try starting again. 52 | sleep(options['restart_delay']) 53 | action_start 54 | end 55 | 56 | # Shim out reload if we have a version that predates reload support. 57 | def action_reload 58 | return if options['never_reload'] 59 | if !upstart_features[:reload_signal] && new_resource.reload_signal != 'HUP' 60 | if options[:reload_shim] 61 | Process.kill(new_resource.reload_signal, pid) 62 | else 63 | check_reload_signal! 64 | end 65 | else 66 | super 67 | end 68 | end 69 | 70 | def pid 71 | cmd = shell_out(%w{initctl status} + [new_resource.service_name]) 72 | if !cmd.error? && md = cmd.stdout.match(/process (\d+)/) 73 | md[1].to_i 74 | else 75 | nil 76 | end 77 | end 78 | 79 | private 80 | 81 | def service_resource 82 | super.tap do |r| 83 | r.provider(Chef::Provider::Service::Upstart) 84 | end 85 | end 86 | 87 | def create_service 88 | check_reload_signal! 89 | # Set features so it will be a closure below. 90 | features = upstart_features 91 | service_template("/etc/init/#{new_resource.service_name}.conf", 'upstart.conf.erb') do 92 | variables.update( 93 | upstart_features: features, 94 | ) 95 | end 96 | end 97 | 98 | def destroy_service 99 | file "/etc/init/#{new_resource.service_name}.conf" do 100 | action :delete 101 | end 102 | end 103 | 104 | def upstart_version 105 | cmd = shell_out(%w{initctl --version}) 106 | if !cmd.error? && md = cmd.stdout.match(/upstart ([^)]+)\)/) 107 | md[1] 108 | else 109 | '0' 110 | end 111 | end 112 | 113 | def upstart_features 114 | @upstart_features ||= begin 115 | upstart_ver = Gem::Version.new(upstart_version) 116 | versions_added = { 117 | kill_signal: '1.3', 118 | reload_signal: '1.10', 119 | setuid: '1.4', 120 | } 121 | versions_added.inject({}) do |memo, (feature, version)| 122 | memo[feature] = Gem::Requirement.create(">= #{version}").satisfied_by?(upstart_ver) 123 | memo 124 | end 125 | end 126 | end 127 | 128 | def check_reload_signal! 129 | if !options['reload_shim'] && !upstart_features[:reload_signal] && new_resource.reload_signal != 'HUP' 130 | 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}") 131 | end 132 | end 133 | 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/poise_service/utils.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'pathname' 18 | 19 | 20 | module PoiseService 21 | # Utility methods for PoiseService. 22 | # 23 | # @api public 24 | # @since 1.0.0 25 | module Utils 26 | # Methods are also available as module-level methods as well as a mixin. 27 | extend self 28 | 29 | # Common segments to ignore 30 | COMMON_SEGMENTS = %w{var www current etc}.inject({}) {|memo, seg| memo[seg] = true; memo } 31 | 32 | # Parse the service name from a path. Look at the last component of the 33 | # path, ignoring some common names. 34 | # 35 | # @param path [String] Path to parse. 36 | # @return [String] 37 | # @example 38 | # attribute(:service_name, kind_of: String, default: lazy { PoiseService::Utils.parse_service_name(path) }) 39 | def parse_service_name(path) 40 | parts = Pathname.new(path).each_filename.to_a.reverse! 41 | # Find the last segment not in common segments, fall back to the last segment. 42 | parts.find {|seg| !COMMON_SEGMENTS[seg] } || parts.first 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/poise_service/version.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | module PoiseService 19 | VERSION = '1.5.3.pre' 20 | end 21 | -------------------------------------------------------------------------------- /poise-service.gemspec: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | lib = File.expand_path('../lib', __FILE__) 18 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 19 | require 'poise_service/version' 20 | 21 | Gem::Specification.new do |spec| 22 | spec.name = 'poise-service' 23 | spec.version = PoiseService::VERSION 24 | spec.authors = ['Noah Kantrowitz'] 25 | spec.email = %w{noah@coderanger.net} 26 | spec.description = "A Chef cookbook for managing system services." 27 | spec.summary = spec.description 28 | spec.homepage = 'https://github.com/poise/poise-service' 29 | spec.license = 'Apache-2.0' 30 | spec.metadata['platforms'] = 'ubuntu debian centos redhat fedora amazon suse opensuse' 31 | 32 | spec.files = `git ls-files`.split($/) 33 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 34 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 35 | spec.require_paths = %w{lib} 36 | 37 | spec.add_dependency 'chef', '>= 12', '< 15' 38 | spec.add_dependency 'halite', '~> 1.0' 39 | spec.add_dependency 'poise', '~> 2.0' 40 | 41 | spec.add_development_dependency 'kitchen-rackspace', '~> 0.14' 42 | spec.add_development_dependency 'poise-boiler', '~> 1.6' 43 | end 44 | -------------------------------------------------------------------------------- /test/cookbook/metadata.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | name 'poise-service_test' 18 | depends 'poise-service' 19 | -------------------------------------------------------------------------------- /test/cookbook/providers/mixin.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_service/service_mixin' 18 | 19 | include PoiseService::ServiceMixin 20 | 21 | def action_enable 22 | notifying_block do 23 | file "/usr/bin/poise_mixin_#{new_resource.service_name}" do 24 | owner 'root' 25 | group 'root' 26 | mode '755' 27 | content <<-EOH 28 | #!/opt/chef/embedded/bin/ruby 29 | require 'webrick' 30 | server = WEBrick::HTTPServer.new(Port: #{new_resource.port}) 31 | server.mount_proc '/' do |req, res| 32 | res.body = #{new_resource.message.inspect} 33 | end 34 | server.start 35 | EOH 36 | end 37 | end 38 | super 39 | end 40 | 41 | def service_options(resource) 42 | resource.command("/usr/bin/poise_mixin_#{new_resource.service_name}") 43 | end 44 | -------------------------------------------------------------------------------- /test/cookbook/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_service/resources/poise_service_test' 18 | 19 | # Create the various services. 20 | poise_service_test 'default' do 21 | base_port 5000 22 | end 23 | 24 | if node['platform_family'] == 'rhel' && node['platform_version'].start_with?('7') || \ 25 | node['platform'] == 'ubuntu' && node['platform_version'].to_i >= 16 26 | file '/no_sysvinit' 27 | file '/no_upstart' 28 | file '/no_inittab' 29 | 30 | poise_service_test 'systemd' do 31 | service_provider :systemd 32 | base_port 8000 33 | end 34 | else 35 | file '/no_systemd' 36 | 37 | poise_service_test 'sysvinit' do 38 | service_provider :sysvinit 39 | base_port 6000 40 | end 41 | 42 | if node['platform_family'] == 'rhel' && node['platform_version'].start_with?('5') 43 | file '/no_upstart' 44 | 45 | poise_service_test 'inittab' do 46 | service_provider :inittab 47 | base_port 10000 48 | end 49 | else 50 | file '/no_inittab' 51 | 52 | poise_service_test 'upstart' do 53 | service_provider :upstart 54 | base_port 7000 55 | end 56 | end 57 | end 58 | 59 | poise_service_test 'dummy' do 60 | service_provider :dummy 61 | base_port 9000 62 | end 63 | 64 | include_recipe 'poise-service_test::mixin' 65 | -------------------------------------------------------------------------------- /test/cookbook/recipes/mixin.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Integration tests for the mixin. 18 | poise_service_test_mixin 'default' do 19 | service_name 'poise_mixin_default' 20 | message 'Hello world!' 21 | port 4000 22 | end 23 | 24 | poise_service_test_mixin 'update' do 25 | service_name 'poise_mixin_update' 26 | message 'first' 27 | port 4001 28 | end 29 | 30 | poise_service_test_mixin 'update again' do 31 | service_name 'poise_mixin_update' 32 | message 'second' 33 | port 4001 34 | end 35 | -------------------------------------------------------------------------------- /test/cookbook/resources/mixin.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_service/service_mixin' 18 | 19 | include PoiseService::ServiceMixin 20 | 21 | attribute :message 22 | attribute :port 23 | 24 | def after_created 25 | notifies(:restart, self) 26 | end 27 | -------------------------------------------------------------------------------- /test/gemfiles/chef-12.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.19' 20 | -------------------------------------------------------------------------------- /test/gemfiles/chef-13.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.7' 20 | -------------------------------------------------------------------------------- /test/gemfiles/master.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../../Gemfile', __FILE__) 18 | 19 | gem 'chef', git: 'https://github.com/chef/chef.git' 20 | gem 'chefspec', git: 'https://github.com/sethvargo/chefspec.git' 21 | gem 'fauxhai', git: 'https://github.com/customink/fauxhai.git' 22 | gem 'foodcritic', git: 'https://github.com/foodcritic/foodcritic.git' 23 | gem 'halite', git: 'https://github.com/poise/halite.git' 24 | gem 'ohai', git: 'https://github.com/chef/ohai.git' 25 | gem 'poise', git: 'https://github.com/poise/poise.git' 26 | gem 'poise-boiler', git: 'https://github.com/poise/poise-boiler.git' 27 | gem 'poise-profiler', git: 'https://github.com/poise/poise-profiler.git' 28 | -------------------------------------------------------------------------------- /test/integration/default/serverspec/Gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | source 'https://rubygems.org/' 18 | 19 | gem 'poise-service-spechelper' 20 | -------------------------------------------------------------------------------- /test/integration/default/serverspec/default_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_service/spec_helper' 18 | 19 | # CentOS 6 doesn't show upstart services in chkconfig, which is how specinfra 20 | # checkes what is enabled. 21 | old_upstart = os[:family] == 'redhat' && os[:release].start_with?('6') 22 | 23 | describe 'default provider' do 24 | it_should_behave_like 'a poise_service_test', 'default', 5000, !old_upstart 25 | 26 | describe process('ruby /usr/bin/poise_test') do 27 | it { is_expected.to be_running } 28 | end 29 | end 30 | 31 | describe 'sysvinit provider', unless: File.exist?('/no_sysvinit') do 32 | it_should_behave_like 'a poise_service_test', 'sysvinit', 6000 33 | end 34 | 35 | describe 'upstart provider', unless: File.exist?('/no_upstart') do 36 | it_should_behave_like 'a poise_service_test', 'upstart', 7000, !old_upstart 37 | end 38 | 39 | describe 'systemd provider', unless: File.exist?('/no_systemd') do 40 | it_should_behave_like 'a poise_service_test', 'systemd', 8000 41 | end 42 | 43 | describe 'dummy provider' do 44 | it_should_behave_like 'a poise_service_test', 'dummy', 9000, false 45 | end 46 | 47 | describe 'inittab provider', unless: File.exist?('/no_inittab') do 48 | it_should_behave_like 'a poise_service_test', 'inittab', 10000, false 49 | end 50 | -------------------------------------------------------------------------------- /test/integration/default/serverspec/mixin_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'net/http' 18 | require 'uri' 19 | 20 | require 'serverspec' 21 | set :backend, :exec 22 | 23 | describe 'poise_service_test_mixin' do 24 | let(:port) { } 25 | let(:url) { "http://localhost:#{port}/" } 26 | subject { Net::HTTP.get(URI(url)) } 27 | 28 | describe 'default' do 29 | let(:port) { 4000 } 30 | it { is_expected.to eq 'Hello world!'} 31 | end 32 | 33 | describe 'update' do 34 | let(:port) { 4001 } 35 | it { is_expected.to eq 'second' } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/spec/resources/poise_service_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe PoiseService::Resources::PoiseService::Resource do 20 | service_provider('auto') 21 | service_resource_hints(%i{debian redhat upstart systemd}) 22 | 23 | describe 'provider lookup' do 24 | recipe(subject: false) do 25 | poise_service 'test' 26 | end 27 | subject do 28 | chef_run.find_resource(:poise_service, 'test').provider_for_action(:enable).class.provides 29 | end 30 | 31 | context 'auto on debian' do 32 | service_resource_hints(:debian) 33 | it { is_expected.to eq :sysvinit } 34 | end # /context auto on debian 35 | 36 | context 'auto on redhat' do 37 | service_resource_hints(:redhat) 38 | it { is_expected.to eq :sysvinit } 39 | end # /context auto on redhat 40 | 41 | context 'auto on upstart' do 42 | service_resource_hints(:upstart) 43 | it { is_expected.to eq :upstart } 44 | end # /context auto on upstart 45 | 46 | context 'auto on systemd' do 47 | service_resource_hints(:systemd) 48 | it { is_expected.to eq :systemd } 49 | end # /context auto on systemd 50 | 51 | context 'auto on multiple systems' do 52 | service_resource_hints(%i{debian invokerd upstart}) 53 | it { is_expected.to eq :upstart } 54 | end # /context auto on multiple systems 55 | 56 | context 'global override' do 57 | service_provider('sysvinit') 58 | it { is_expected.to eq :sysvinit } 59 | end # /context global override 60 | 61 | context 'per-service override' do 62 | service_provider('test', 'sysvinit') 63 | it { is_expected.to eq :sysvinit } 64 | end # /context global override 65 | 66 | context 'per-service override for a different service' do 67 | service_provider('other', 'sysvinit') 68 | it { is_expected.to eq :systemd } 69 | end # /context global override for a different service 70 | 71 | context 'recipe DSL override' do 72 | recipe(subject: false) do 73 | poise_service 'test' do 74 | provider :sysvinit 75 | end 76 | end 77 | it { is_expected.to eq :sysvinit } 78 | end # /context recipe DSL override 79 | end # /describe provider lookup 80 | 81 | describe '#clean_signal' do 82 | let(:signal) { } 83 | subject do 84 | described_class.new(nil, nil).send(:clean_signal, signal) 85 | end 86 | 87 | context 'with a short string' do 88 | let(:signal) { 'term' } 89 | it { is_expected.to eq 'TERM' } 90 | end # /context with a short string 91 | 92 | context 'with a long string' do 93 | let(:signal) { 'sigterm' } 94 | it { is_expected.to eq 'TERM' } 95 | end # /context with a long string 96 | 97 | context 'with a short string in caps' do 98 | let(:signal) { 'TERM' } 99 | it { is_expected.to eq 'TERM' } 100 | end # /context with a short string in caps 101 | 102 | context 'with a long string in caps' do 103 | let(:signal) { 'SIGTERM' } 104 | it { is_expected.to eq 'TERM' } 105 | end # /context with a long string in caps 106 | 107 | context 'with a number' do 108 | let(:signal) { 15 } 109 | it { is_expected.to eq 'TERM' } 110 | end # /context with a number 111 | 112 | context 'with a symbol' do 113 | let(:signal) { :term } 114 | it { is_expected.to eq 'TERM' } 115 | end # /context with a symbol 116 | 117 | context 'with an invalid string' do 118 | let(:signal) { 'nope' } 119 | it { expect { subject }.to raise_error PoiseService::Error } 120 | end # /context with an invalid string 121 | 122 | context 'with an invalid number' do 123 | let(:signal) { 100 } 124 | it { expect { subject }.to raise_error PoiseService::Error } 125 | end # /context with an invalid number 126 | end # /describe #clean_stop_signal 127 | 128 | describe '#options' do 129 | subject { chef_run.find_resource('poise_service', 'test') } 130 | recipe(subject: false) do 131 | poise_service 'test' do 132 | options template: 'source.erb' 133 | options :sysvinit, template: 'override.erb' 134 | end 135 | end 136 | 137 | its(:options) { are_expected.to eq({'template' => 'source.erb'}) } 138 | it { expect(subject.options(:sysvinit)).to eq({'template' => 'override.erb'}) } 139 | end # /describe #options 140 | 141 | describe '#default_directory' do 142 | let(:user) { 'root' } 143 | subject do 144 | described_class.new('test', chef_run.run_context).tap {|r| r.user(user) }.send(:default_directory) 145 | end 146 | 147 | context 'with root' do 148 | context 'on Linux' do 149 | let(:chefspec_options) { {platform: 'ubuntu', version: '14.04'} } 150 | it { is_expected.to eq '/' } 151 | end # /context 'on Linux 152 | 153 | context 'on Windows' do 154 | let(:chefspec_options) { {platform: 'windows', version: '2012R2'} } 155 | before { allow(Poise::Utils::Win32).to receive(:admin_user).and_return('Administrator') } if defined?(Poise::Utils::Win32) 156 | it { is_expected.to eq 'C:\\' } 157 | end # /context on Windows 158 | end # /context with root 159 | 160 | context 'with a normal user' do 161 | let(:user) { 'poise' } 162 | before do 163 | expect(Dir).to receive(:home).with('poise').and_return('/home/poise') 164 | allow(File).to receive(:directory?).and_call_original 165 | allow(File).to receive(:directory?).with('/home/poise').and_return(true) 166 | end 167 | 168 | it { is_expected.to eq '/home/poise' } 169 | end # /context with a normal user 170 | 171 | context 'with an invalid user' do 172 | let(:user) { 'poise' } 173 | before do 174 | expect(Dir).to receive(:home).with('poise').and_raise(ArgumentError) 175 | end 176 | 177 | it { is_expected.to eq '/' } 178 | end # /context with an invalid user 179 | 180 | context 'with a non-existent directory' do 181 | let(:user) { 'poise' } 182 | before do 183 | expect(Dir).to receive(:home).with('poise').and_return('/home/poise') 184 | allow(File).to receive(:directory?).and_call_original 185 | allow(File).to receive(:directory?).with('/home/poise').and_return(false) 186 | end 187 | 188 | it { is_expected.to eq '/' } 189 | end # /context with a non-existent directory 190 | 191 | context 'with a blank directory' do 192 | let(:user) { 'poise' } 193 | before do 194 | expect(Dir).to receive(:home).with('poise').and_return('') 195 | end 196 | 197 | it { is_expected.to eq '/' } 198 | end # /context with a blank directory 199 | end # /describe #default_directory 200 | 201 | describe '#restart_on_update' do 202 | service_provider('sysvinit') 203 | step_into(:poise_service) 204 | before do 205 | allow(File).to receive(:exist?).and_call_original 206 | allow(File).to receive(:exist?).with('/etc/init.d/test').and_return(true) 207 | end 208 | subject { chef_run.template('/etc/init.d/test') } 209 | 210 | context 'with true' do 211 | recipe(subject: false) do 212 | poise_service 'test' do 213 | command 'myapp --serve' 214 | end 215 | end 216 | it { is_expected.to notify('poise_service[test]').to(:restart) } 217 | end # /context with true 218 | 219 | context 'with false' do 220 | recipe(subject: false) do 221 | poise_service 'test' do 222 | command 'myapp --serve' 223 | restart_on_update false 224 | end 225 | end 226 | it { is_expected.to_not notify('poise_service[test]').to(:restart) } 227 | end # /context with false 228 | 229 | context 'with immediately' do 230 | recipe(subject: false) do 231 | poise_service 'test' do 232 | command 'myapp --serve' 233 | restart_on_update 'immediately' 234 | end 235 | end 236 | it { is_expected.to notify('poise_service[test]').to(:restart).immediately } 237 | end # /context with immediately 238 | 239 | context 'with :immediately' do 240 | recipe(subject: false) do 241 | poise_service 'test' do 242 | command 'myapp --serve' 243 | restart_on_update :immediately 244 | end 245 | end 246 | it { is_expected.to notify('poise_service[test]').to(:restart).immediately } 247 | end # /context with :immediately 248 | end # /describe #restart_on_update 249 | 250 | describe '#pid' do 251 | subject { described_class.new(nil, nil) } 252 | it do 253 | fake_pid = double('pid') 254 | expect(subject).to receive(:provider_for_action).with(:pid).and_return(double(pid: fake_pid)) 255 | expect(subject.pid).to eq fake_pid 256 | end 257 | end # /describe #pid 258 | end 259 | -------------------------------------------------------------------------------- /test/spec/resources/poise_service_user_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe PoiseService::Resources::PoiseServiceUser do 20 | step_into(:poise_service_user) 21 | # We need a platform because Chef no longer maps the user resource for chefspec. 22 | # https://github.com/chef/chef/issues/5242 23 | let(:chefspec_options) { {platform: 'ubuntu', version: '14.04'} } 24 | let(:shells) { [] } 25 | before do 26 | allow(File).to receive(:exist?).and_call_original 27 | described_class::DEFAULT_SHELLS.each do |shell| 28 | allow(File).to receive(:exist?).with(shell).and_return(shells.include?(shell)) 29 | end 30 | end 31 | recipe do 32 | poise_service_user 'poise' 33 | end 34 | 35 | it { is_expected.to create_group('poise').with(gid: nil, system: true) } 36 | it { is_expected.to create_user('poise').with(gid: 'poise', home: nil, system: true, uid: nil, shell: '/bin/false') } 37 | 38 | context 'with an explicit user and group name' do 39 | recipe do 40 | poise_service_user 'poise' do 41 | user 'poise_user' 42 | group 'poise_group' 43 | end 44 | end 45 | 46 | it { is_expected.to create_group('poise_group').with(gid: nil, system: true) } 47 | it { is_expected.to create_user('poise_user').with(gid: 'poise_group', home: nil, system: true, uid: nil, shell: '/bin/false') } 48 | end # /context with an explicit user and group name 49 | 50 | context 'with no group' do 51 | recipe do 52 | poise_service_user 'poise' do 53 | group false 54 | end 55 | end 56 | 57 | it { is_expected.to_not create_group('poise') } 58 | it { is_expected.to create_user('poise').with(gid: nil, home: nil, system: true, uid: nil, shell: '/bin/false') } 59 | end # /context with no group 60 | 61 | context 'with explicit uid' do 62 | recipe do 63 | poise_service_user 'poise' do 64 | uid 100 65 | end 66 | end 67 | 68 | it { is_expected.to create_group('poise').with(gid: nil, system: true) } 69 | it { is_expected.to create_user('poise').with(gid: 'poise', home: nil, system: true, uid: 100, shell: '/bin/false') } 70 | end # /context with explicit uid 71 | 72 | context 'with explicit gid' do 73 | recipe do 74 | poise_service_user 'poise' do 75 | gid 100 76 | end 77 | end 78 | 79 | it { is_expected.to create_group('poise').with(gid: 100, system: true) } 80 | it { is_expected.to create_user('poise').with(gid: 'poise', home: nil, system: true, uid: nil, shell: '/bin/false') } 81 | end # /context with explicit gid 82 | 83 | context 'with home directory' do 84 | recipe do 85 | poise_service_user 'poise' do 86 | home '/home/poise' 87 | end 88 | end 89 | 90 | it { is_expected.to create_group('poise').with(gid: nil, system: true) } 91 | it { is_expected.to create_user('poise').with(gid: 'poise', home: '/home/poise', system: true, uid: nil, shell: '/bin/false') } 92 | end # /context with home directory 93 | 94 | context 'with shell' do 95 | recipe do 96 | poise_service_user 'poise' do 97 | shell '/bin/bash' 98 | end 99 | end 100 | 101 | it { is_expected.to create_group('poise').with(gid: nil, system: true) } 102 | it { is_expected.to create_user('poise').with(gid: 'poise', home: nil, system: true, uid: nil, shell: '/bin/bash') } 103 | end # /context with shell 104 | 105 | context 'with /bin/nologin existing' do 106 | let(:shells) { %w{/bin/nologin} } 107 | recipe do 108 | poise_service_user 'poise' 109 | end 110 | 111 | it { is_expected.to create_user('poise').with(shell: '/bin/nologin') } 112 | end # /context with /bin/nologin existing 113 | 114 | context 'with action :remove' do 115 | recipe do 116 | poise_service_user 'poise' do 117 | action :remove 118 | end 119 | end 120 | 121 | it { is_expected.to remove_group('poise') } 122 | it { is_expected.to remove_user('poise') } 123 | 124 | context 'with an explicit user and group name' do 125 | recipe do 126 | poise_service_user 'poise' do 127 | action :remove 128 | user 'poise_user' 129 | group 'poise_group' 130 | end 131 | end 132 | 133 | it { is_expected.to remove_group('poise_group') } 134 | it { is_expected.to remove_user('poise_user') } 135 | end # /context with an explicit user and group name 136 | 137 | context 'with no group' do 138 | recipe do 139 | poise_service_user 'poise' do 140 | action :remove 141 | group false 142 | end 143 | end 144 | 145 | it { is_expected.to_not remove_group('poise') } 146 | it { is_expected.to remove_user('poise') } 147 | end # /context with no group 148 | end # context with action :remove 149 | 150 | context 'on Solaris' do 151 | let(:chefspec_options) { {platform: 'solaris2', version: '5.11'} } 152 | 153 | it { is_expected.to create_group('poise').with(gid: nil, system: nil) } 154 | it { is_expected.to create_user('poise').with(gid: 'poise', home: nil, system: false, uid: nil, shell: '/bin/false') } 155 | end # /context on Solaris 156 | 157 | context 'on Windows' do 158 | let(:chefspec_options) { {platform: 'windows', version: '2012R2'} } 159 | 160 | it { is_expected.to_not create_group('poise') } 161 | it { is_expected.to create_user('poise').with(gid: nil, home: nil, system: true, uid: nil, shell: '/bin/false') } 162 | end # /context on Windows 163 | 164 | end 165 | -------------------------------------------------------------------------------- /test/spec/service_mixin_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe PoiseService::ServiceMixin do 20 | context 'in a resource' do 21 | resource(:poise_test) do 22 | include PoiseService::ServiceMixin 23 | end 24 | subject { resource(:poise_test) } 25 | 26 | it { is_expected.to include PoiseService::ServiceMixin } 27 | it { is_expected.to include PoiseService::ServiceMixin::Resource } 28 | end # /context in a resource 29 | 30 | context 'in a provider' do 31 | provider(:poise_test) do 32 | include PoiseService::ServiceMixin 33 | end 34 | subject { provider(:poise_test) } 35 | 36 | it { is_expected.to include PoiseService::ServiceMixin } 37 | it { is_expected.to include PoiseService::ServiceMixin::Provider } 38 | end # /context in a provider 39 | end # /describe PoiseService::ServiceMixin 40 | 41 | describe PoiseService::ServiceMixin::Resource do 42 | subject { resource(:poise_test).new('test', nil) } 43 | 44 | context 'with Poise already included' do 45 | resource(:poise_test) do 46 | include Poise 47 | provides(:poise_test_other) 48 | actions(:doit) 49 | include PoiseService::ServiceMixin::Resource 50 | end 51 | 52 | it { expect(Array(subject.action)).to eq %i{doit} } 53 | its(:allowed_actions) { is_expected.to eq %i{nothing doit enable disable start stop restart reload} } 54 | its(:service_name) { is_expected.to eq 'test' } 55 | end # /context with Poise already included 56 | 57 | context 'without Poise already included' do 58 | resource(:poise_test) do 59 | include PoiseService::ServiceMixin::Resource 60 | actions(:doit) 61 | end 62 | 63 | it { expect(Array(subject.action)).to eq %i{enable} } 64 | its(:allowed_actions) { is_expected.to eq %i{nothing enable disable start stop restart reload doit} } 65 | its(:service_name) { is_expected.to eq 'test' } 66 | end # /context without Poise already included 67 | end # /describe PoiseService::ServiceMixin::Resource 68 | 69 | describe PoiseService::ServiceMixin::Provider do 70 | let(:new_resource) { double('new_resource', name: 'test') } 71 | let(:service_resource) { double('service_resource', updated_by_last_action: nil) } 72 | provider(:poise_test) do 73 | include PoiseService::ServiceMixin::Provider 74 | end 75 | subject { provider(:poise_test).new(new_resource, nil) } 76 | 77 | describe 'actions' do 78 | before do 79 | allow(subject).to receive(:notify_if_service) {|&block| block.call } 80 | allow(subject).to receive(:service_resource).and_return(service_resource) 81 | end 82 | 83 | describe '#action_enable' do 84 | it do 85 | expect(service_resource).to receive(:run_action).with(:enable) 86 | subject.action_enable 87 | end 88 | end # /describe #action_enable 89 | 90 | describe '#action_disable' do 91 | it do 92 | expect(service_resource).to receive(:run_action).with(:disable) 93 | subject.action_disable 94 | end 95 | end # /describe #action_disable 96 | 97 | describe '#action_start' do 98 | it do 99 | expect(service_resource).to receive(:run_action).with(:start) 100 | subject.action_start 101 | end 102 | end # /describe #action_start 103 | 104 | describe '#action_stop' do 105 | it do 106 | expect(service_resource).to receive(:run_action).with(:stop) 107 | subject.action_stop 108 | end 109 | end # /describe #action_stop 110 | 111 | describe '#action_restart' do 112 | it do 113 | expect(service_resource).to receive(:run_action).with(:restart) 114 | subject.action_restart 115 | end 116 | end # /describe #action_restart 117 | 118 | describe '#action_reload' do 119 | it do 120 | expect(service_resource).to receive(:run_action).with(:reload) 121 | subject.action_reload 122 | end 123 | end # /describe #action_reload 124 | end # /describe actions 125 | 126 | describe '#notify_if_service' do 127 | before do 128 | allow(subject).to receive(:service_resource).and_return(service_resource) 129 | end 130 | 131 | context 'with an update' do 132 | it do 133 | expect(service_resource).to receive(:updated_by_last_action?).and_return(true) 134 | expect(new_resource).to receive(:updated_by_last_action).with(true) 135 | subject.send(:notify_if_service) 136 | end 137 | end # /context with an update 138 | 139 | context 'with no update' do 140 | it do 141 | expect(service_resource).to receive(:updated_by_last_action?).and_return(false) 142 | subject.send(:notify_if_service) 143 | end 144 | end # /context with no update 145 | end # /describe #notify_if_service 146 | 147 | describe '#service_resource' do 148 | it do 149 | allow(new_resource).to receive(:source_line).and_return('path.rb:1') 150 | allow(new_resource).to receive(:service_name).and_return('test') 151 | fake_poise_service = double('poise_service') 152 | expect(PoiseService::Resources::PoiseService::Resource).to receive(:new).with('test', nil).and_return(fake_poise_service) 153 | expect(fake_poise_service).to receive(:declared_type=).with(:poise_service) 154 | expect(fake_poise_service).to receive(:enclosing_provider=).with(subject) 155 | expect(fake_poise_service).to receive(:source_line=).with('path.rb:1') 156 | expect(fake_poise_service).to receive(:service_name).with('test') 157 | expect(subject).to receive(:service_options).with(fake_poise_service) 158 | subject.send(:service_resource) 159 | end 160 | end # /describe #service_resource 161 | 162 | describe '#service_options' do 163 | it { expect(subject.send(:service_options, nil)).to be_nil } 164 | end # /describe #service_options 165 | end # /describe PoiseService::ServiceMixin::Provider 166 | -------------------------------------------------------------------------------- /test/spec/service_providers/base_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe PoiseService::ServiceProviders::Base do 20 | let(:new_resource) { double('new_resource') } 21 | let(:run_context) { double('run_context') } 22 | let(:service_resource) do 23 | double('service_resource').tap do |r| 24 | allow(r).to receive(:updated_by_last_action).with(false) 25 | allow(r).to receive(:updated_by_last_action?).and_return(false) 26 | end 27 | end 28 | let(:options) { Hash.new } 29 | subject(:provider) do 30 | described_class.new(new_resource, run_context).tap do |provider| 31 | allow(provider).to receive(:notifying_block) {|&block| block.call } 32 | allow(provider).to receive(:service_resource).and_return(service_resource) 33 | allow(provider).to receive(:options).and_return(options) 34 | end 35 | end 36 | 37 | describe '#action_enable' do 38 | it do 39 | expect(subject).to receive(:create_service).ordered 40 | expect(service_resource).to receive(:run_action).with(:enable).ordered 41 | expect(service_resource).to receive(:run_action).with(:start).ordered 42 | subject.action_enable 43 | end 44 | end # /describe #action_enable 45 | 46 | describe '#action_disable' do 47 | it do 48 | expect(service_resource).to receive(:run_action).with(:stop).ordered 49 | expect(service_resource).to receive(:run_action).with(:disable).ordered 50 | expect(subject).to receive(:destroy_service).ordered 51 | subject.action_disable 52 | end 53 | end # /describe #action_disable 54 | 55 | describe '#action_start' do 56 | it do 57 | expect(service_resource).to receive(:run_action).with(:start).ordered 58 | subject.action_start 59 | end 60 | 61 | context 'with never_start' do 62 | before { options['never_start'] = true } 63 | it do 64 | expect(service_resource).to_not receive(:run_action).with(:start).ordered 65 | subject.action_start 66 | end 67 | end # /context with never_start 68 | end # /describe #action_start 69 | 70 | describe '#action_stop' do 71 | it do 72 | expect(service_resource).to receive(:run_action).with(:stop).ordered 73 | subject.action_stop 74 | end 75 | 76 | context 'with never_stop' do 77 | before { options['never_stop'] = true } 78 | it do 79 | expect(service_resource).to_not receive(:run_action).with(:stop).ordered 80 | subject.action_stop 81 | end 82 | end # /context with never_stop 83 | end # /describe #action_stop 84 | 85 | describe '#action_restart' do 86 | it do 87 | expect(service_resource).to receive(:run_action).with(:restart).ordered 88 | subject.action_restart 89 | end 90 | 91 | context 'with never_restart' do 92 | before { options['never_restart'] = true } 93 | it do 94 | expect(service_resource).to_not receive(:run_action).with(:restart).ordered 95 | subject.action_restart 96 | end 97 | end # /context with never_restart 98 | end # /describe #action_restart 99 | 100 | describe '#action_reload' do 101 | it do 102 | expect(service_resource).to receive(:run_action).with(:reload).ordered 103 | subject.action_reload 104 | end 105 | 106 | context 'with never_reload' do 107 | before { options['never_reload'] = true } 108 | it do 109 | expect(service_resource).to_not receive(:run_action).with(:reload).ordered 110 | subject.action_reload 111 | end 112 | end # /context with never_reload 113 | end # /describe #action_reload 114 | 115 | describe '#pid' do 116 | it do 117 | expect { subject.send(:pid) }.to raise_error(NotImplementedError) 118 | end 119 | end # /describe #pid 120 | 121 | describe '#create_service' do 122 | it do 123 | expect { subject.send(:create_service) }.to raise_error(NotImplementedError) 124 | end 125 | end # /describe #create_service 126 | 127 | describe '#destroy_service' do 128 | it do 129 | expect { subject.send(:destroy_service) }.to raise_error(NotImplementedError) 130 | end 131 | end # /describe #destroy_service 132 | 133 | describe '#service_template' do 134 | let(:new_resource) do 135 | double('new_resource', 136 | command: 'myapp --serve', 137 | cookbook_name: :test_cookbook, 138 | directory: '/cwd', 139 | environment: Hash.new, 140 | reload_signal: 'HUP', 141 | restart_on_update: true, 142 | service_name: 'myapp', 143 | stop_signal: 'TERM', 144 | user: 'root', 145 | ) 146 | end 147 | let(:run_context) { chef_run.run_context } 148 | let(:options) { Hash.new } 149 | let(:block) { Proc.new { } } 150 | before do 151 | allow(provider).to receive(:options).and_return(options) 152 | end 153 | subject do 154 | provider.send(:service_template, '/test', 'source.erb', &block) 155 | end 156 | 157 | context 'with no block' do 158 | its(:owner) { is_expected.to eq 'root' } 159 | its(:source) { is_expected.to eq 'source.erb'} 160 | its(:cookbook) { is_expected.to eq 'poise-service'} 161 | end # /context with no block 162 | 163 | context 'with a block' do 164 | let(:block) do 165 | Proc.new do 166 | owner('nobody') 167 | variables.update(mykey: 'myvalue') 168 | end 169 | end 170 | its(:owner) { is_expected.to eq 'nobody' } 171 | its(:variables) { are_expected.to include({mykey: 'myvalue'}) } 172 | its(:source) { is_expected.to eq 'source.erb'} 173 | its(:cookbook) { is_expected.to eq 'poise-service'} 174 | end # /context with a block 175 | 176 | context 'with a template override' do 177 | let(:options) { {'template' => 'override.erb'} } 178 | its(:source) { is_expected.to eq 'override.erb'} 179 | its(:cookbook) { is_expected.to eq 'test_cookbook'} 180 | end # /context with a template override 181 | 182 | context 'with a template and cookbook override' do 183 | let(:options) { {'template' => 'other:override.erb'} } 184 | its(:source) { is_expected.to eq 'override.erb'} 185 | its(:cookbook) { is_expected.to eq 'other'} 186 | end # /context with a template and cookbook override 187 | end # /describe #service_template 188 | 189 | describe '#options' do 190 | service_provider('dummy') 191 | subject { chef_run.poise_service('test').provider_for_action(:enable).options } 192 | 193 | context 'with an options resource' do 194 | recipe(subject: false) do 195 | poise_service 'test' do 196 | service_name 'other' 197 | end 198 | 199 | poise_service_options 'test' do 200 | command 'myapp' 201 | end 202 | end 203 | 204 | it { is_expected.to eq({'command' => 'myapp', 'provider' => 'dummy', 'restart_delay' => 1}) } 205 | end # /context with an options resource 206 | 207 | context 'with an options resource using service_name' do 208 | recipe(subject: false) do 209 | poise_service 'test' do 210 | service_name 'other' 211 | end 212 | 213 | poise_service_options 'other' do 214 | command 'myapp' 215 | end 216 | end 217 | 218 | it { is_expected.to eq({'command' => 'myapp', 'provider' => 'dummy', 'restart_delay' => 1}) } 219 | end # /context with an options resource using service_name 220 | 221 | context 'with node attributes' do 222 | before do 223 | override_attributes['poise-service']['test'] = {command: 'myapp'} 224 | end 225 | recipe(subject: false) do 226 | poise_service 'test' do 227 | service_name 'other' 228 | end 229 | end 230 | 231 | it { is_expected.to eq({'command' => 'myapp', 'provider' => 'dummy', 'restart_delay' => 1}) } 232 | end # /context with node attributes 233 | 234 | context 'with node attributes using service_name' do 235 | before do 236 | override_attributes['poise-service']['other'] = {command: 'myapp'} 237 | end 238 | recipe(subject: false) do 239 | poise_service 'test' do 240 | service_name 'other' 241 | end 242 | end 243 | 244 | it { is_expected.to eq({'command' => 'myapp', 'provider' => 'dummy', 'restart_delay' => 1}) } 245 | end # /context with node attributes using service_name 246 | end # /describe #options 247 | end 248 | -------------------------------------------------------------------------------- /test/spec/service_providers/dummy_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe PoiseService::ServiceProviders::Dummy do 20 | service_provider('dummy') 21 | step_into(:poise_service) 22 | before do 23 | allow(Process).to receive(:fork).and_return(0) 24 | allow(Process).to receive(:kill).with(0, 100) 25 | allow(File).to receive(:exist?).and_call_original 26 | allow(File).to receive(:exist?).with('/var/run/test.pid').and_return(true) 27 | allow(IO).to receive(:read).and_call_original 28 | allow(IO).to receive(:read).with('/var/run/test.pid').and_return('100') 29 | end 30 | 31 | describe '#action_enable' do 32 | before do 33 | allow(File).to receive(:exist?).with('/var/run/test.pid').and_return(false, false, false, true) 34 | expect_any_instance_of(described_class).to receive(:sleep).with(1).once 35 | end 36 | recipe do 37 | poise_service 'test' do 38 | command 'myapp --serve' 39 | end 40 | end 41 | 42 | it { run_chef } 43 | end # /describe #action_enable 44 | 45 | describe '#action_disable' do 46 | before do 47 | expect(Process).to receive(:kill).with('TERM', 100) 48 | allow(File).to receive(:unlink).and_call_original 49 | allow(File).to receive(:unlink).with('/var/run/test.pid') 50 | end 51 | recipe do 52 | poise_service 'test' do 53 | action :disable 54 | end 55 | end 56 | 57 | it { run_chef } 58 | end # /describe #action_disable 59 | 60 | describe '#action_restart' do 61 | before do 62 | expect_any_instance_of(described_class).to receive(:action_start) 63 | expect_any_instance_of(described_class).to receive(:action_stop) 64 | end 65 | recipe do 66 | poise_service 'test' do 67 | action :restart 68 | end 69 | end 70 | 71 | it { run_chef } 72 | end # /describe #action_restart 73 | 74 | describe '#action_reload' do 75 | before do 76 | expect(Process).to receive(:kill).with('HUP', 100) 77 | end 78 | recipe do 79 | poise_service 'test' do 80 | action :reload 81 | end 82 | end 83 | 84 | it { run_chef } 85 | end # /describe #action_reload 86 | 87 | describe '#service_resource' do 88 | subject { described_class.new(nil, nil).send(:service_resource) } 89 | it { expect { subject }.to raise_error NotImplementedError } 90 | end # /describe #service_resource 91 | end 92 | -------------------------------------------------------------------------------- /test/spec/service_providers/inittab_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe PoiseService::ServiceProviders::Inittab do 20 | service_provider('inittab') 21 | step_into(:poise_service) 22 | let(:inittab) { '' } 23 | before do 24 | allow(IO).to receive(:read).and_call_original 25 | allow(IO).to receive(:read).with('/etc/inittab').and_return(inittab) 26 | end 27 | 28 | context 'with action :enable' do 29 | recipe do 30 | poise_service 'test' do 31 | command 'myapp --serve' 32 | end 33 | end 34 | 35 | it { is_expected.to render_file('/sbin/poise_service_test').with_content(<<-EOS) } 36 | #!/bin/sh 37 | exec /opt/chef/embedded/bin/ruby <