procsd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bb2d76f653bcd73538afe6175c6e910bf77bbe86930238bfd7ec53f2bef8e939
4
+ data.tar.gz: 805808a883ea1f4c8ae03a01a390dda70729dea2a948e16e24c2b392791fa7cb
5
+ SHA512:
6
+ metadata.gz: 4e8e6057f5bc9d8a3d466a7f57fb958f0335812a11412ab1b71ef4fa4ea3a77afe37b4d49725f2c144014afcd1ede0f218fc1c3165dbf10df7608c6763848543
7
+ data.tar.gz: 32559bb91e97ed3cc238849b5bbc55609cafb27bd0ae25414cdf9fcf91e6205a35b0b6cec65fd9b6339fb153bc5eb8e5a8cfdc3851296bb8df9dad7ae00e6cb1
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in procsd.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Victor Afanasev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # Procsd
2
+
3
+ I do like the way how simple is managing of application processes in production on Heroku with [Procfile](https://devcenter.heroku.com/articles/procfile). How easily can be accessed application logs with [heroku logs](https://devcenter.heroku.com/articles/logging) command. Just type `heroku create` and you're good to go.
4
+
5
+ Can we have something similar on the cheap Ubuntu VPS from DigitalOcean? Yes we can, all we need is a Systemd wrapper which allows to export application processes from Procfile to system services, and control them/check status/access logs using simple commands.
6
+
7
+ > These days most of Linux distributions (including Ubuntu) has Systemd as a default system processes manager. That's why it is a good idea to use Systemd for managing application processes in production (for simple cases).
8
+
9
+ ## Getting started
10
+
11
+ > Install `procsd` first: `$ gem install procsd`. Required Ruby version is `>= 2.3.0`.
12
+
13
+ Let's say you have following application's Procfile:
14
+
15
+ ```yaml
16
+ web: bundle exec rails server -p $PORT
17
+ worker: bundle exec sidekiq -e $RAILS_ENV
18
+ ```
19
+ and you want to have one instance of web process && two instances of worker process. Create inside application directory `.procsd.yml` config file:
20
+
21
+ ```yaml
22
+ app: sample_app
23
+ formation: web=1,worker=2
24
+ environment:
25
+ - PORT=2500
26
+ - RAILS_ENV=production
27
+ - RAILS_LOG_TO_STDOUT=true
28
+ ```
29
+ > The only required option in `.procsd.yml` is `app` (application's name). Also you can provide custom Systemd directory path (`systemd_dir` option, default is _/etc/systemd/system_)
30
+
31
+
32
+
33
+ Configuration is done. Now it's time to **create** an application (export it to Systemd):
34
+ > To disable and remove application from Systemd there is command `$ procsd destroy`.
35
+
36
+ > Note: `create` command needs to provide a few arguments: _--user_ (name of the current user), _--dir_ (application's working directory) and `--path` (user's $PATH). Usually it's fine to provide them like on example below:
37
+
38
+ ```
39
+ deploy@server:~/sample_app$ procsd create -u $USER -d $PWD -p $PATH
40
+
41
+ Systemd directory: /etc/systemd/system
42
+ create sample_app-web.1.service
43
+ create sample_app-worker.1.service
44
+ create sample_app-worker.2.service
45
+ create sample_app.target
46
+ Created symlink /etc/systemd/system/multi-user.target.wants/sample_app.target → /etc/systemd/system/sample_app.target.
47
+ Enabled app target sample_app.target
48
+ Reloaded configuraion (daemon-reload)
49
+ App services were created and enabled. Run `start` to start them
50
+ ```
51
+
52
+
53
+ **Start** application services:
54
+ > Other control commands: `stop` and `restart`
55
+
56
+ ```
57
+ deploy@server:~/sample_app$ procsd start
58
+
59
+ Started app services (sample_app.target)
60
+ ```
61
+
62
+
63
+ Check the **status**:
64
+ > You can filter processes, like `$ procsd status worker` (show status only for worker processes) or `$ procsd status worker.2` (show status only for worker.2 process)
65
+
66
+ > To show status of the main application target: `$ procsd status --target`
67
+
68
+ ```
69
+ deploy@server:~/sample_app$ procsd status
70
+
71
+ ● sample_app-web.1.service
72
+ Loaded: loaded (/etc/systemd/system/sample_app-web.1.service; static; vendor preset: enabled)
73
+ Active: active (running) since Sun 2018-11-04 01:54:15 +04; 1min 51s ago
74
+ Main PID: 8828 (ruby)
75
+ Tasks: 13 (limit: 4915)
76
+ Memory: 83.6M
77
+ CGroup: /system.slice/sample_app-web.1.service
78
+ └─8828 puma 3.12.0 (tcp://0.0.0.0:2500) [sample_app]
79
+
80
+ 2018-11-04T01:54:15+0400 systemd[1]: Started sample_app-web.1.service.
81
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: => Booting Puma
82
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: => Rails 5.2.1 application starting in production
83
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: => Run `rails server -h` for more startup options
84
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: Puma starting in single mode...
85
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Version 3.12.0 (ruby 2.3.0-p0), codename: Llamas in Pajamas
86
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Min threads: 5, max threads: 5
87
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Environment: production
88
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Listening on tcp://0.0.0.0:2500
89
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: Use Ctrl-C to stop
90
+
91
+ ● sample_app-worker.1.service
92
+ Loaded: loaded (/etc/systemd/system/sample_app-worker.1.service; static; vendor preset: enabled)
93
+ Active: active (running) since Sun 2018-11-04 01:54:15 +04; 1min 51s ago
94
+ Main PID: 8826 (bundle)
95
+ Tasks: 15 (limit: 4915)
96
+ Memory: 87.8M
97
+ CGroup: /system.slice/sample_app-worker.1.service
98
+ └─8826 sidekiq 5.2.2 sample_app [0 of 10 busy]
99
+
100
+ 2018-11-04T01:54:15+0400 systemd[1]: Started sample_app-worker.1.service.
101
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: Running in ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
102
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: See LICENSE and the LGPL-3.0 for licensing details.
103
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org
104
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: Booting Sidekiq 5.2.2 with redis options {:id=>"Sidekiq-serv…, :url=>nil}
105
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.658Z 8826 TID-grcvqfyom INFO: Starting processing, hit Ctrl-C to stop
106
+
107
+ ● sample_app-worker.2.service
108
+ Loaded: loaded (/etc/systemd/system/sample_app-worker.2.service; static; vendor preset: enabled)
109
+ Active: active (running) since Sun 2018-11-04 01:54:15 +04; 1min 51s ago
110
+ Main PID: 8827 (bundle)
111
+ Tasks: 15 (limit: 4915)
112
+ Memory: 87.8M
113
+ CGroup: /system.slice/sample_app-worker.2.service
114
+ └─8827 sidekiq 5.2.2 sample_app [0 of 10 busy]
115
+
116
+ 2018-11-04T01:54:15+0400 systemd[1]: Started sample_app-worker.2.service.
117
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.713Z 8827 TID-gniahzm1r INFO: Running in ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
118
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.713Z 8827 TID-gniahzm1r INFO: See LICENSE and the LGPL-3.0 for licensing details.
119
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.714Z 8827 TID-gniahzm1r INFO: Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org
120
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.714Z 8827 TID-gniahzm1r INFO: Booting Sidekiq 5.2.2 with redis options {:id=>"Sidekiq-serv…, :url=>nil}
121
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.716Z 8827 TID-gniahzm1r INFO: Starting processing, hit Ctrl-C to stop
122
+ ```
123
+
124
+
125
+ Now the interesting part, **logs**:
126
+ > Like with command `status`, you can filter logs by passing the name of process as an argument: `$ procsd logs web` (show logs only for web processes, if any)
127
+
128
+ ```
129
+ deploy@server:~/sample_app$ procsd logs
130
+
131
+ -- Logs begin at Sun 2018-10-21 00:38:42 +04, end at Sun 2018-11-04 01:54:17 +04. --
132
+ 2018-11-04T01:54:15+0400 systemd[1]: Started sample_app-worker.1.service.
133
+ 2018-11-04T01:54:15+0400 systemd[1]: Started sample_app-worker.2.service.
134
+ 2018-11-04T01:54:15+0400 systemd[1]: Started sample_app-web.1.service.
135
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: Running in ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
136
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: See LICENSE and the LGPL-3.0 for licensing details.
137
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org
138
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.655Z 8826 TID-grcvqfyom INFO: Booting Sidekiq 5.2.2 with redis options {:id=>"Sidekiq-server-PID-8826", :url=>nil}
139
+ 2018-11-04T01:54:17+0400 sample_app-worker.1[8826]: 2018-11-03T21:54:17.658Z 8826 TID-grcvqfyom INFO: Starting processing, hit Ctrl-C to stop
140
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.713Z 8827 TID-gniahzm1r INFO: Running in ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
141
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.713Z 8827 TID-gniahzm1r INFO: See LICENSE and the LGPL-3.0 for licensing details.
142
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.714Z 8827 TID-gniahzm1r INFO: Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org
143
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.714Z 8827 TID-gniahzm1r INFO: Booting Sidekiq 5.2.2 with redis options {:id=>"Sidekiq-server-PID-8827", :url=>nil}
144
+ 2018-11-04T01:54:17+0400 sample_app-worker.2[8827]: 2018-11-03T21:54:17.716Z 8827 TID-gniahzm1r INFO: Starting processing, hit Ctrl-C to stop
145
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: => Booting Puma
146
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: => Rails 5.2.1 application starting in production
147
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: => Run `rails server -h` for more startup options
148
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: Puma starting in single mode...
149
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Version 3.12.0 (ruby 2.3.0-p0), codename: Llamas in Pajamas
150
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Min threads: 5, max threads: 5
151
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Environment: production
152
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: * Listening on tcp://0.0.0.0:2500
153
+ 2018-11-04T01:54:17+0400 sample_app-web.1[8828]: Use Ctrl-C to stop
154
+ ```
155
+
156
+ Systemd provides [a lot of possibilities](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs) to display and manage application logs (`journalctl` command). Procsd supports following options:
157
+ * `-n` - Specify how many last lines to print. Default is 100
158
+ * `-t` - Tail, display recent logs and leave the session open for real-time logs to stream in
159
+ * `--system` - Show only systemd messages about services (start/stop/restart/fail etc.)
160
+ * `--priority` - Filter messages by a [particular log level.](https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs#by-priority) For example show only error messages: `procsd logs --priority err`
161
+ * `--grep` - [Filter output](https://www.freedesktop.org/software/systemd/man/journalctl.html#-g) to messages where message matches the provided query (may not work for [some](https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/1751006) Linux distributions)
162
+
163
+ ## All available commands
164
+
165
+ ```
166
+ $ procsd --help
167
+
168
+ Commands:
169
+ procsd --version, -v # Print the version
170
+ procsd create d, --dir=$PWD p, --path=$PATH u, --user=$USER # Create and enable app services
171
+ procsd destroy # Stop, disable and remove app services
172
+ procsd disable # Disable app target
173
+ procsd enable # Enable app target
174
+ procsd help [COMMAND] # Describe available commands or one specific command
175
+ procsd logs # Show app services logs
176
+ procsd restart # Restart app services
177
+ procsd start # Start app services
178
+ procsd status # Show app services status
179
+ procsd stop # Stop app services
180
+ ```
181
+
182
+
183
+ ## Difference with Foreman
184
+
185
+ [Foreman](http://ddollar.github.io/foreman/) itself designed for _development_ (not production) usage only and doing it great. Yes, Foreman allows to [export](http://ddollar.github.io/foreman/#EXPORTING) Procfile to the Systemd, but that's all. After export you have to manually use `systemctl` and `journalctl` to manage/check exported services. Procsd not only exports application, but provides [simple commands](#all-available-commands) to manage exported target.
186
+
187
+ There is another difference in the export logic. Foreman systemd export uses [dymamic](https://fedoramagazine.org/systemd-template-unit-files/) services templates and as a result generates a lot of files/folders in the systemd directory even for a simple application.
188
+
189
+ Services names contains [$PORT variable](http://ddollar.github.io/foreman/#PROCFILE) at the end. If Procfile has multiple processes, each exported service will have following naming format: `<app_name>-<process_name>@<port_varible_number += 100>.service` (and it's [undocumented](http://ddollar.github.io/foreman/#SYSTEMD-EXPORT) logic). For example for Procfile and formation (_web=1, worker=2_) above, exported services with Foreman will be: `sample_app-web@2500.service`, `sample_app-worker@2600.service` and `sample_app-worker@2601.service`. My opinion about this approach: it's complicated. Why is there PORT variable, which is also incremented for each service? How I'm supposed to remember all these services names to manage or check services status/logs (it's required to provide full names of services for systemctl/journalctl)?
190
+
191
+ Procsd following one rule: simplicity. For export it uses static service files (that means for each process will be generated it's own service file) and services names have predictable, Heroku-like names.
192
+
193
+
194
+ ## Notes
195
+
196
+ * If you want to set environment variables per process, [use format](https://github.com/ddollar/foreman/wiki/Per-Process-Environment-Variables) like Foreman recommends.
197
+
198
+ ## ToDo
199
+ * Add Capistrano integration examples
200
+ * Optional possibility to generate Ngnix config (with out-of-box SSL using [Certbot](https://certbot.eff.org/lets-encrypt/ubuntubionic-nginx)) for an application to use Ngnix as a proxy and serve static files
201
+ * Add integration with [Inspeqtor](https://github.com/mperham/inspeqtor) to monitor application services and get alert notifications if someting happened
202
+
203
+
204
+ ## License
205
+
206
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "procsd"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/procsd ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "procsd"
4
+ require "procsd/cli"
5
+
6
+ Procsd::CLI.start(ARGV)
data/lib/procsd.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'thor'
2
+ require 'procsd/version'
3
+
4
+ module Procsd
5
+ DEFAULT_SYSTEMD_DIR = "/etc/systemd/system".freeze
6
+ end
data/lib/procsd/cli.rb ADDED
@@ -0,0 +1,238 @@
1
+ require 'yaml'
2
+ require_relative 'generator'
3
+
4
+ module Procsd
5
+ class CLI < Thor
6
+ class ConfigurationError < StandardError; end
7
+ map %w[--version -v] => :__print_version
8
+
9
+ desc "create", "Create and enable app services"
10
+ option :user, aliases: :u, type: :string, required: true, banner: "$USER"
11
+ option :dir, aliases: :d, type: :string, required: true, banner: "$PWD"
12
+ option :path, aliases: :p, type: :string, required: true, banner: "$PATH"
13
+ def create
14
+ preload!
15
+
16
+ if !target_exist?
17
+ gen = Generator.new
18
+ gen.export!(services, procsd: @procsd, options: options)
19
+
20
+ enable
21
+ if system "sudo", "systemctl", "daemon-reload"
22
+ say("Reloaded configuraion (daemon-reload)", :green)
23
+ end
24
+
25
+ say("App services were created and enabled. Run `start` to start them", :green)
26
+ else
27
+ say("App target `#{target_name}` already exists", :red)
28
+ end
29
+ end
30
+
31
+ desc "destroy", "Stop, disable and remove app services"
32
+ def destroy
33
+ preload!
34
+
35
+ if target_exist?
36
+ stop
37
+ disable
38
+
39
+ services.keys.push(target_name).each do |filename|
40
+ path = File.join(systemd_dir, filename)
41
+ if File.exist? path
42
+ system "sudo", "rm", path
43
+ say "Deleted #{path}"
44
+ end
45
+ end
46
+
47
+ if system "sudo", "systemctl", "daemon-reload"
48
+ say("Reloaded configuraion (daemon-reload)", :green)
49
+ end
50
+
51
+ say("App services were stopped, disabled and removed", :green)
52
+ else
53
+ say_target_not_exists
54
+ end
55
+ end
56
+
57
+ desc "enable", "Enable app target"
58
+ def enable
59
+ preload!
60
+ say_target_not_exists and return unless target_exist?
61
+
62
+ if target_enabled?
63
+ say "App target #{target_name} already enabled"
64
+ else
65
+ if system "sudo", "systemctl", "enable", target_name
66
+ say("Enabled app target #{target_name}", :green)
67
+ end
68
+ end
69
+ end
70
+
71
+ desc "disable", "Disable app target"
72
+ def disable
73
+ preload!
74
+ say_target_not_exists and return unless target_exist?
75
+
76
+ if !target_enabled?
77
+ say "App target #{target_name} already disabled"
78
+ else
79
+ if system "sudo", "systemctl", "disable", target_name
80
+ say("Disabled app target #{target_name}", :green)
81
+ end
82
+ end
83
+ end
84
+
85
+ desc "start", "Start app services"
86
+ def start
87
+ preload!
88
+ say_target_not_exists and return unless target_exist?
89
+
90
+ if target_active?
91
+ say "Already started/active (#{target_name})"
92
+ else
93
+ if system "sudo", "systemctl", "start", target_name
94
+ say("Started app services (#{target_name})", :green)
95
+ end
96
+ end
97
+ end
98
+
99
+ desc "stop", "Stop app services"
100
+ def stop
101
+ preload!
102
+ say_target_not_exists and return unless target_exist?
103
+
104
+ if !target_active?
105
+ say "Already stopped/inactive (#{target_name})"
106
+ else
107
+ if system "sudo", "systemctl", "stop", target_name
108
+ say("Stopped app services (#{target_name})", :green)
109
+ end
110
+ end
111
+ end
112
+
113
+ desc "restart", "Restart app services"
114
+ def restart
115
+ preload!
116
+ say_target_not_exists and return unless target_exist?
117
+
118
+ if system "sudo", "systemctl", "restart", target_name
119
+ say("Restarted app services (#{target_name})", :green)
120
+ end
121
+ end
122
+
123
+ desc "status", "Show app services status"
124
+ option :target, type: :string, banner: "Show main target status"
125
+ def status(service_name = nil)
126
+ preload!
127
+ say_target_not_exists and return unless target_exist?
128
+
129
+ command = %w(systemctl status --no-pager --output short-iso)
130
+ if options["target"]
131
+ command << target_name
132
+ else
133
+ filtered = filter_services(service_name)
134
+ say("Can't find any services matching given name: #{service_name}", :red) and return if filtered.empty?
135
+ command += filtered
136
+ end
137
+
138
+ system *command
139
+ end
140
+
141
+ desc "logs", "Show app services logs"
142
+ option :num, aliases: :n, type: :string, banner: "How many lines to print"
143
+ option :tail, aliases: [:t, :f], type: :boolean, banner: "Display logs in real-time"
144
+ option :system, type: :boolean, banner: "Show only system messages"
145
+ option :priority, aliases: :p, type: :string, banner: "Show messages with a particular log level"
146
+ option :grep, aliases: :g, type: :string, banner: "Filter output to entries where message matches the provided query"
147
+ def logs(service_name = nil)
148
+ preload!
149
+
150
+ command = %w(journalctl --no-pager --all --no-hostname --output short-iso)
151
+ command.push("-n", options.fetch("num", "100"))
152
+ command.push("-f") if options["tail"]
153
+ command.push("--system") if options["system"]
154
+ command.push("--priority", options["priority"]) if options["priority"]
155
+ command.push("--grep", "'" + options["grep"] + "'") if options["grep"]
156
+
157
+ filtered = filter_services(service_name)
158
+ say("Can't find any services matching given name: #{service_name}", :red) and return if filtered.empty?
159
+
160
+ filtered.each { |service| command.push("--unit", service) }
161
+ system *command
162
+ end
163
+
164
+ desc "--version, -v", "Print the version"
165
+ def __print_version
166
+ puts VERSION
167
+ end
168
+
169
+ private
170
+
171
+ def say_target_not_exists
172
+ say("App target #{target_name} is not exists", :red)
173
+ end
174
+
175
+ def filter_services(service_name)
176
+ if service_name
177
+ services.keys.select { |s| s.include?("#{app_name}-#{service_name}") }
178
+ else
179
+ services.keys
180
+ end
181
+ end
182
+
183
+ def target_exist?
184
+ File.exist?(File.join systemd_dir, target_name)
185
+ end
186
+
187
+ def systemd_dir
188
+ @procsd["systemd_dir"]
189
+ end
190
+
191
+ def target_enabled?
192
+ system "systemctl", "is-enabled", "--quiet", target_name
193
+ end
194
+
195
+ def target_active?
196
+ system "systemctl", "is-active", "--quiet", target_name
197
+ end
198
+
199
+ def target_name
200
+ "#{app_name}.target"
201
+ end
202
+
203
+ def app_name
204
+ @procsd["app"]
205
+ end
206
+
207
+ def services
208
+ all = {}
209
+ @procfile.each do |process_name, process_command|
210
+ processes_count = @procsd["formation"][process_name] || 1
211
+ processes_count.times do |i|
212
+ all["#{app_name}-#{process_name}.#{i + 1}.service"] = process_command
213
+ end
214
+ end
215
+
216
+ all
217
+ end
218
+
219
+ def preload!
220
+ raise ConfigurationError, "Procfile file doesn't exists" unless File.exist? "Procfile"
221
+ raise ConfigurationError, ".procsd.yml config file doesn't exists" unless File.exist? ".procsd.yml"
222
+
223
+ @procfile = YAML.load_file("Procfile")
224
+ @procsd = YAML.load_file(".procsd.yml")
225
+ raise ConfigurationError, "Missing app name in the .procsd.yml file" unless @procsd["app"]
226
+
227
+ if formation = @procsd["formation"]
228
+ @procsd["formation"] = formation.split(",").map { |f| f.split("=") }.to_h
229
+ @procsd["formation"].each { |k, v| @procsd["formation"][k] = v.to_i }
230
+ else
231
+ @procsd["formation"] = {}
232
+ end
233
+
234
+ @procsd["environment"] ||= []
235
+ @procsd["systemd_dir"] ||= DEFAULT_SYSTEMD_DIR
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,44 @@
1
+ module Procsd
2
+ class Generator < Thor::Group
3
+ include Thor::Actions
4
+
5
+ def self.source_root
6
+ File.dirname(__FILE__)
7
+ end
8
+
9
+ def export!(services, procsd:, options:)
10
+ self.destination_root = "/tmp"
11
+ @procsd = procsd
12
+ say "Systemd directory: #{@procsd["systemd_dir"]}"
13
+
14
+ app_name = @procsd["app"]
15
+ target_name = "#{app_name}.target"
16
+
17
+ services.each do |service_name, service_command|
18
+ service_config = options.merge(
19
+ "target_name" => target_name,
20
+ "id" => service_name.sub(".service", ""),
21
+ "command" => service_command,
22
+ "environment" => @procsd["environment"]
23
+ )
24
+ generate(service_name, service_config, type: :service)
25
+ end
26
+
27
+ target_config = {
28
+ "app" => app_name,
29
+ "services" => services.keys
30
+ }
31
+ generate(target_name, target_config, type: :target)
32
+ end
33
+
34
+ private
35
+
36
+ def generate(filename, confing, type:)
37
+ template("templates/#{type}.erb", filename, confing)
38
+
39
+ source_path = File.join(destination_root, filename)
40
+ dest_path = File.join(@procsd["systemd_dir"], filename)
41
+ system "sudo", "mv", source_path, dest_path
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ [Unit]
2
+ Requires=network.target
3
+ PartOf=<%= config["target_name"] %>
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=<%= config["user"] %>
8
+ WorkingDirectory=<%= config["dir"] %>
9
+ ExecStart=/bin/bash -lc '<%= config["command"] %>'
10
+
11
+ Restart=always
12
+ RestartSec=1
13
+ TimeoutStopSec=15
14
+ KillMode=mixed
15
+ StandardInput=null
16
+ SyslogIdentifier=<%= config["id"] %>
17
+
18
+ Environment="PATH=<%= config["path"] %>"
19
+ <% config["environment"].each do |env| -%>
20
+ Environment="<%= env %>"
21
+ <% end -%>
@@ -0,0 +1,6 @@
1
+ [Unit]
2
+ Description=<%= config["app"] %> target
3
+ Wants=<%= config["services"].join(" ") %>
4
+
5
+ [Install]
6
+ WantedBy=multi-user.target
@@ -0,0 +1,3 @@
1
+ module Procsd
2
+ VERSION = "0.1.0"
3
+ end
data/procsd.gemspec ADDED
@@ -0,0 +1,31 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "procsd/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "procsd"
8
+ spec.version = Procsd::VERSION
9
+ spec.authors = ["Victor Afanasev"]
10
+ spec.email = ["vicfreefly@gmail.com"]
11
+
12
+ spec.summary = "Manage your application processes in production hassle-free like Heroku CLI with Procfile and Systemd"
13
+ spec.homepage = "https://github.com/vifreefly/procsd"
14
+ spec.license = "MIT"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+
22
+ spec.bindir = "exe"
23
+ spec.executables = "procsd"
24
+ spec.require_paths = ["lib"]
25
+ spec.required_ruby_version = ">= 2.3.0"
26
+
27
+ spec.add_dependency "thor"
28
+ spec.add_development_dependency "bundler", "~> 1.16"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "minitest", "~> 5.0"
31
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: procsd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Victor Afanasev
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-11-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ description:
70
+ email:
71
+ - vicfreefly@gmail.com
72
+ executables:
73
+ - procsd
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/console
83
+ - bin/setup
84
+ - exe/procsd
85
+ - lib/procsd.rb
86
+ - lib/procsd/cli.rb
87
+ - lib/procsd/generator.rb
88
+ - lib/procsd/templates/service.erb
89
+ - lib/procsd/templates/target.erb
90
+ - lib/procsd/version.rb
91
+ - procsd.gemspec
92
+ homepage: https://github.com/vifreefly/procsd
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 2.3.0
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.7.6
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Manage your application processes in production hassle-free like Heroku CLI
116
+ with Procfile and Systemd
117
+ test_files: []