cindy-cm 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Changelog.md +6 -0
- data/README.md +19 -9
- data/lib/cindy/all.rb +1 -0
- data/lib/cindy/cindy.rb +77 -2
- data/lib/cindy/command.rb +4 -3
- data/lib/cindy/executor/base.rb +101 -0
- data/lib/cindy/executor/local.rb +23 -17
- data/lib/cindy/executor/ssh.rb +26 -26
- data/lib/cindy/template.rb +18 -14
- data/lib/cindy/version.rb +1 -1
- data/test/templates/deploy.tpl +0 -0
- data/test/test_cindy.rb +13 -2
- data/test/test_local_executor.rb +27 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8c7c22d266d8267c749cd4c9b98eb41a52af41c
|
4
|
+
data.tar.gz: 880741fc31c94b0f89aa77d6f7c250c1310d322b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af5a8f616854bad4d4fc7be8dcc234abbd597cda1c00e3a5e1a191ab28b7ebc9fa0fd5aadddf0753d843132f349f9741c465a5915f91d08707086ec1ea7c5d80
|
7
|
+
data.tar.gz: 928a046d7dc654ed9f9abbf18ac4677091bb50dc2b1c9d7a797161459fb6fdd336a060bdece1780dcc6fe8a5fe5a827c9bde36df6cd6e939e7162b26a11d09cd
|
data/Changelog.md
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
## Introduction
|
2
2
|
|
3
|
-
Tired to modify your configuration files depending on the targeted
|
3
|
+
Tired to modify your configuration files depending on the targeted host? Turn them out into ERB templates and deploy them in one command.
|
4
4
|
|
5
5
|
The purpose is to implement a kind of shell with limited dependencies to automate configuration and deployment on various (Unix) environments.
|
6
6
|
|
@@ -10,7 +10,7 @@ Dependencies: net-ssh, highline
|
|
10
10
|
|
11
11
|
`gem install cindy-cm`
|
12
12
|
|
13
|
-
## Usage
|
13
|
+
## Usage of cindy command
|
14
14
|
|
15
15
|
* reload => force Cindy to reload its configuration file
|
16
16
|
* environment (shortcut: env)
|
@@ -22,16 +22,18 @@ Dependencies: net-ssh, highline
|
|
22
22
|
* deploy => install the generated file on the given environment
|
23
23
|
* print => display output configuration file as it would be deployed on the given environment
|
24
24
|
* details => list all applicable variables to the given template, their values and scopes
|
25
|
+
|
25
26
|
## Example
|
26
27
|
|
27
28
|
Create ~/.cindy as follows:
|
29
|
+
|
28
30
|
```ruby
|
29
31
|
# create 2 environments named "production" and "development"
|
30
|
-
environment :development
|
32
|
+
environment :development
|
31
33
|
environment :production, 'ssh://root@www.xxx.tld/'
|
32
34
|
|
33
|
-
# register the template
|
34
|
-
template :nginx, '
|
35
|
+
# register the template ~/cindy/templates/nginx.conf.tpl (see below) for our nginx configuration
|
36
|
+
template :nginx, '~/cindy/templates/nginx.conf.tpl' do
|
35
37
|
# default variables
|
36
38
|
|
37
39
|
# have_gzip_static will be set to true or false depending on the result of the following command
|
@@ -49,10 +51,16 @@ template :nginx, '/home/julp/cindy/templates/nginx.conf.tpl' do
|
|
49
51
|
var :server_name, 'www.xxx.lan'
|
50
52
|
var :root, '/home/julp/app/public'
|
51
53
|
end
|
54
|
+
|
55
|
+
# after deployment, check syntax
|
56
|
+
postcmd 'nginx -tc $INSTALL_FILE'
|
57
|
+
# if no error, reload configuration
|
58
|
+
postcmd 'nginx -s reload'
|
52
59
|
end
|
53
60
|
```
|
54
61
|
|
55
|
-
And
|
62
|
+
And ~/cindy/templates/nginx.conf.tpl as:
|
63
|
+
|
56
64
|
```
|
57
65
|
# <%= _install_file_ %>
|
58
66
|
|
@@ -71,6 +79,7 @@ server {
|
|
71
79
|
```
|
72
80
|
|
73
81
|
Running `cindy template nginx environment production print`, result in:
|
82
|
+
|
74
83
|
```
|
75
84
|
# /usr/local/etc/nginx.conf
|
76
85
|
|
@@ -84,19 +93,20 @@ server {
|
|
84
93
|
(if we admit that nginx is built, on production environment, with Gzip Precompression module)
|
85
94
|
|
86
95
|
After `cindy template nginx environment production deploy`, output of `ls -l /etc/nginx.conf*` is:
|
96
|
+
|
87
97
|
```
|
88
98
|
lrwxrwxrwx [...] /usr/local/etc/nginx.conf -> /usr/local/etc/nginx.conf.201502262311
|
89
99
|
-rw-r--r-- [...] /usr/local/etc/nginx.conf.201502262209 # file at previous deployment
|
90
100
|
-rw-r--r-- [...] /usr/local/etc/nginx.conf.201502262311 # current version (1h02m later)
|
91
101
|
```
|
92
102
|
|
93
|
-
## What is a *
|
103
|
+
## What is a *Command* object?
|
94
104
|
|
95
105
|
It is a kind of dynamic variable: instead of hardcoding a value which depends on the remote host, we execute the associated command before each
|
96
106
|
time the template is rendered. It is more convenient mainly if this value can change at any time.
|
97
107
|
|
98
|
-
The result of the command is a boolean based on its exit status (0 => true,
|
99
|
-
standard output else a string with the content
|
108
|
+
The result of the command is a boolean based on its exit status (0 => true, everything else => false) if the command does not print anything on
|
109
|
+
standard output else a string with the content sent on standard output.
|
100
110
|
|
101
111
|
In the example above, `nginx -V 2>&1 >/dev/null | grep -qF -- --with-http_gzip_static_module` is intended to determine if nginx, on the remote
|
102
112
|
server, is compiled or not with the gzip_static module.
|
data/lib/cindy/all.rb
CHANGED
data/lib/cindy/cindy.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
1
3
|
module Cindy
|
2
4
|
|
3
5
|
class UndefinedEnvironmentError < ::NameError
|
@@ -11,6 +13,7 @@ module Cindy
|
|
11
13
|
|
12
14
|
class Cindy
|
13
15
|
|
16
|
+
# Default location of Cindy's configuration file
|
14
17
|
CONFIGURATION_FILE = File.expand_path '~/.cindy'
|
15
18
|
|
16
19
|
module DSL
|
@@ -20,6 +23,10 @@ module Cindy
|
|
20
23
|
@envname = envname
|
21
24
|
end
|
22
25
|
|
26
|
+
def cmd(command, options = {})
|
27
|
+
Command.new command, options
|
28
|
+
end
|
29
|
+
|
23
30
|
def var(varname, value)
|
24
31
|
@tpl.set_variable @envname, varname, value
|
25
32
|
end
|
@@ -32,6 +39,10 @@ module Cindy
|
|
32
39
|
@tpl = tpl
|
33
40
|
end
|
34
41
|
|
42
|
+
def cmd(command, options = {})
|
43
|
+
Command.new command, options
|
44
|
+
end
|
45
|
+
|
35
46
|
def var(varname, value)
|
36
47
|
@tpl.set_variable nil, varname, value
|
37
48
|
end
|
@@ -42,6 +53,10 @@ module Cindy
|
|
42
53
|
@tpl.set_path_for_environment envname, file
|
43
54
|
TemplateEnvironmentNode.new(@tpl, envname).instance_eval &block if block_given?
|
44
55
|
end
|
56
|
+
|
57
|
+
def postcmd(cmd, options = {})
|
58
|
+
@tpl.add_postcmd cmd, options
|
59
|
+
end
|
45
60
|
end
|
46
61
|
|
47
62
|
class CindyNode
|
@@ -55,23 +70,40 @@ module Cindy
|
|
55
70
|
tpl
|
56
71
|
end
|
57
72
|
|
58
|
-
def environment(envname, uri =
|
73
|
+
def environment(envname, uri = '')
|
59
74
|
@cindy.environment_add envname, uri
|
60
75
|
end
|
61
76
|
end
|
62
77
|
end
|
63
78
|
|
79
|
+
# DSL
|
80
|
+
# \@!method environment(envname, uri = '')
|
81
|
+
# \@!method template(tplname, path, &block)
|
82
|
+
|
83
|
+
# @return [Logger] the logger associated to the cindy instance
|
84
|
+
attr_accessor :logger
|
85
|
+
|
86
|
+
# Creates an "empty" Cindy object
|
64
87
|
def initialize
|
65
88
|
@environments = {}
|
66
89
|
@templates = {}
|
90
|
+
@logger = Logger.new STDERR
|
67
91
|
end
|
68
92
|
|
93
|
+
# Creates a Cindy object from a string
|
94
|
+
#
|
95
|
+
# @param string [String] the configuration string to evaluate
|
96
|
+
# @return [Cindy]
|
69
97
|
def self.from_string(string)
|
70
98
|
cindy = Cindy.new
|
71
99
|
DSL::CindyNode.new(cindy).instance_eval string
|
72
100
|
cindy
|
73
101
|
end
|
74
102
|
|
103
|
+
# Creates a Cindy object from a file
|
104
|
+
#
|
105
|
+
# @param filename [String] the file to evaluate ; default location {CONFIGURATION_FILE} is used if nil
|
106
|
+
# @return [Cindy]
|
75
107
|
def self.load(filename = nil)
|
76
108
|
@filename = filename || CONFIGURATION_FILE
|
77
109
|
cindy = Cindy.new
|
@@ -83,36 +115,67 @@ module Cindy
|
|
83
115
|
(@environments.values.map(&:to_s) + [''] + @templates.values.map(&:to_s)).join("\n")
|
84
116
|
end
|
85
117
|
|
118
|
+
# Get known environments
|
119
|
+
#
|
120
|
+
# @return [Array<Symbol>] list of environment names
|
86
121
|
def environments
|
87
122
|
@environments.values
|
88
123
|
end
|
89
124
|
|
125
|
+
# Does environment exist?
|
126
|
+
#
|
127
|
+
# @param envname [String, Symbol] the name of the environment to check for existence
|
90
128
|
def has_environment?(envname)
|
91
129
|
envname = envname.intern
|
92
130
|
@environments.key? envname
|
93
131
|
end
|
94
132
|
|
133
|
+
# Add a new environment
|
134
|
+
#
|
135
|
+
# @param envname [String, Symbol] the name of the new environment
|
136
|
+
# @param attributes [String] the uri of the host (eg 'ssh://user@1.2.3.4/' for a
|
137
|
+
# remote one or 'file:///' to work directely on actual host)
|
138
|
+
# @return [Environment] the registered environment
|
95
139
|
def environment_add(envname, attributes)
|
96
140
|
envname = envname.intern
|
97
141
|
# assert !@environments.key? envname
|
98
142
|
@environments[envname] = Environment.new(envname, attributes)
|
99
143
|
end
|
100
144
|
|
145
|
+
# Get registered templates
|
146
|
+
#
|
147
|
+
# @return [Array<Symbol>] list of template names
|
101
148
|
def templates
|
102
149
|
@templates.values
|
103
150
|
end
|
104
151
|
|
152
|
+
# Does template exist?
|
153
|
+
#
|
154
|
+
# @param tplname [String, Symbol] the name of the template to check for existence
|
105
155
|
def has_template?(tplname)
|
106
156
|
tplname = tplname.intern
|
107
157
|
@templates.key? tplname
|
108
158
|
end
|
109
159
|
|
160
|
+
# Add a new template
|
161
|
+
#
|
162
|
+
# @param tplname [String, Symbol] the name of the new template
|
163
|
+
# @param file [String] the location of the template file. Note it is expanded
|
164
|
+
# ({File#expand_path}) to permit usage of ~ so it may be a good idea to avoid
|
165
|
+
# relative paths
|
166
|
+
# @return [Template] the registered template
|
110
167
|
def template_add(tplname, file)
|
111
168
|
tplname = tplname.intern
|
112
169
|
# assert !@templates.key? name
|
113
|
-
@templates[tplname] = Template.new File.expand_path(file), tplname
|
170
|
+
@templates[tplname] = Template.new self, File.expand_path(file), tplname
|
114
171
|
end
|
115
172
|
|
173
|
+
# Print on stdout the result of template generation
|
174
|
+
#
|
175
|
+
# @param envname [String, Symbol] the name of the environment
|
176
|
+
# @param tplname [String, Symbol] the name of the template
|
177
|
+
# @raise [UndefinedEnvironmentError] if environment does not exist
|
178
|
+
# @raise [UndefinedTemplateError] if template does not exist
|
116
179
|
def template_environment_print(envname, tplname)
|
117
180
|
envname = envname.intern
|
118
181
|
tplname = tplname.intern
|
@@ -121,6 +184,12 @@ module Cindy
|
|
121
184
|
@templates[tplname].print @environments[envname]
|
122
185
|
end
|
123
186
|
|
187
|
+
# Deploy the result of template generation on the given environment
|
188
|
+
#
|
189
|
+
# @param envname [String, Symbol] the name of the environment
|
190
|
+
# @param tplname [String, Symbol] the name of the template
|
191
|
+
# @raise [UndefinedEnvironmentError] if environment does not exist
|
192
|
+
# @raise [UndefinedTemplateError] if template does not exist
|
124
193
|
def template_environment_deploy(envname, tplname)
|
125
194
|
envname = envname.intern
|
126
195
|
tplname = tplname.intern
|
@@ -129,6 +198,12 @@ module Cindy
|
|
129
198
|
@templates[tplname].deploy @environments[envname]
|
130
199
|
end
|
131
200
|
|
201
|
+
# Print on stdout a detail (value and scope) of applicable variables
|
202
|
+
#
|
203
|
+
# @param envname [String, Symbol] the name of the environment
|
204
|
+
# @param tplname [String, Symbol] the name of the template
|
205
|
+
# @raise [UndefinedEnvironmentError] if environment does not exist
|
206
|
+
# @raise [UndefinedTemplateError] if template does not exist
|
132
207
|
def template_environment_variables(envname, tplname)
|
133
208
|
envname = envname.intern
|
134
209
|
tplname = tplname.intern
|
data/lib/cindy/command.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
module Cindy
|
2
2
|
class Command
|
3
|
-
def initialize(command)
|
3
|
+
def initialize(command, options = {})
|
4
4
|
@command = command
|
5
|
+
@options = options
|
5
6
|
end
|
6
7
|
|
7
8
|
def to_s
|
@@ -9,11 +10,11 @@ module Cindy
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def inspect
|
12
|
-
"#{self.class.name}.new(#{@command.inspect})"
|
13
|
+
"#{self.class.name}.new(#{@command.inspect}, TODO)"
|
13
14
|
end
|
14
15
|
|
15
16
|
def call(executor)
|
16
|
-
executor.exec(@command,
|
17
|
+
executor.exec(@command, @options)
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Cindy
|
4
|
+
module Executor
|
5
|
+
class CommandFailedError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class Base
|
9
|
+
# X
|
10
|
+
#
|
11
|
+
# @param [Logger] logger
|
12
|
+
def initialize(logger)
|
13
|
+
@logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_uri(uri, logger)
|
17
|
+
uri = URI.parse(uri)
|
18
|
+
ObjectSpace.each_object(Class).select { |v| return v.new(uri, logger) if v.ancestors.include?(self) && v.handle?(uri) }
|
19
|
+
raise Exception.new 'Unexpected protocol'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Executes the given command
|
23
|
+
#
|
24
|
+
# @param command [String] the command to execute
|
25
|
+
# @param options [Hash] the options to execute the command with
|
26
|
+
# @option options [String] :stdin the string to send as/on stdin
|
27
|
+
# @option options [Boolean] :ignore_failure don't raise an exception if true when the command returns a non 0 exit value
|
28
|
+
# @option options [Hash] :env the environment variables to set/pass for command execution
|
29
|
+
# @option options [Boolean] :check_status_only simply return true if the command is successful else false
|
30
|
+
# @return [String, Boolean]
|
31
|
+
def exec(command, options = {})
|
32
|
+
=begin
|
33
|
+
stdout, stderr, status = exec_imp command, options[:stdin]
|
34
|
+
stdout.chomp!
|
35
|
+
# <logging>
|
36
|
+
if status.zero?
|
37
|
+
if stdout.empty?
|
38
|
+
@logger.info 'Command "%s" executed successfully' % command
|
39
|
+
else
|
40
|
+
@logger.info 'Command "%s" executed successfully (with "%s" returned)' % [ command, stdout ]
|
41
|
+
end
|
42
|
+
else
|
43
|
+
@logger.send(options[:ignore_failure] ? :warn : :error, 'Command "%s" failed with "%s"' % [ command, stderr ])
|
44
|
+
end
|
45
|
+
# </logging>
|
46
|
+
return status.zero? if options[:check_status_only]
|
47
|
+
raise CommandFailedError.new "Command '#{command}' failed" if !options[:ignore_failure] && 0 != status
|
48
|
+
=end
|
49
|
+
stdout, stderr, status = exec_and_log command, options
|
50
|
+
# ?!?
|
51
|
+
stdout
|
52
|
+
end
|
53
|
+
|
54
|
+
def exec!(command, options = {})
|
55
|
+
stdout, stderr, status = exec_and_log command, options
|
56
|
+
raise CommandFailedError.new "Command '#{command}' failed" unless 0 == status
|
57
|
+
stdout
|
58
|
+
end
|
59
|
+
|
60
|
+
# Close the eventual underlaying connection
|
61
|
+
def close
|
62
|
+
# NOP
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def exec_and_log(command, options = {})
|
68
|
+
stdout, stderr, status = exec_imp command, options[:stdin]
|
69
|
+
stdout.chomp!
|
70
|
+
# <logging>
|
71
|
+
if status.zero?
|
72
|
+
if stdout.empty?
|
73
|
+
@logger.info 'Command "%s" executed successfully' % command
|
74
|
+
else
|
75
|
+
@logger.info 'Command "%s" executed successfully (with "%s" returned)' % [ command, stdout ]
|
76
|
+
end
|
77
|
+
else
|
78
|
+
@logger.send(options[:ignore_failure] ? :warn : :error, 'Command "%s" failed with "%s"' % [ command, stderr ])
|
79
|
+
end
|
80
|
+
[ stdout, stderr, status ]
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# @abstract
|
86
|
+
def self.handle?(uri)
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
# @abstract
|
91
|
+
# @param command [String] the command to execute
|
92
|
+
# @return [Array<(String, String, Fixnum)>] an array containing the following elements (in this order):
|
93
|
+
# 1. output on stdout
|
94
|
+
# 2. output on stderr
|
95
|
+
# 3. exit status
|
96
|
+
def exec_imp(command, stdin_str)
|
97
|
+
raise NotImplementedError.new
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/cindy/executor/local.rb
CHANGED
@@ -1,28 +1,34 @@
|
|
1
|
+
require 'uri'
|
1
2
|
require 'open3'
|
2
3
|
|
3
4
|
module Cindy
|
4
5
|
module Executor
|
5
|
-
class Local
|
6
|
-
def
|
6
|
+
class Local < Base
|
7
|
+
def self.handle?(uri)
|
8
|
+
[ nil, 'file' ].include? uri.scheme
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(uri, logger)
|
12
|
+
super logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def exec_imp(command, stdin_str)
|
7
16
|
exit_status = 1
|
8
17
|
stdout_str = stderr_str = ''
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
18
|
+
begin
|
19
|
+
Open3.popen3({ 'PATH' => "#{ENV['PATH']}:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin" }, command) do |stdin, stdout, stderr, wait_thr|
|
20
|
+
if stdin_str
|
21
|
+
stdin.write stdin_str
|
22
|
+
stdin.close
|
23
|
+
end
|
24
|
+
stdout_str = stdout.read
|
25
|
+
stderr_str = stderr.read
|
26
|
+
exit_status = wait_thr.value.exitstatus
|
13
27
|
end
|
14
|
-
|
15
|
-
stderr_str =
|
16
|
-
exit_status = wait_thr.value
|
28
|
+
rescue Errno::ENOENT => e
|
29
|
+
stderr_str = e.message
|
17
30
|
end
|
18
|
-
|
19
|
-
raise Exception.new if 0 != exit_status && !stderr_str.empty?
|
20
|
-
return nil if status_only && 0 != exit_status
|
21
|
-
stdout_str.chomp
|
22
|
-
end
|
23
|
-
|
24
|
-
def close
|
25
|
-
# NOP
|
31
|
+
[ stdout_str, stderr_str, exit_status ]
|
26
32
|
end
|
27
33
|
end
|
28
34
|
end
|
data/lib/cindy/executor/ssh.rb
CHANGED
@@ -1,38 +1,38 @@
|
|
1
|
+
require 'uri'
|
1
2
|
require 'net/ssh'
|
2
3
|
|
3
4
|
module Cindy
|
4
5
|
module Executor
|
5
|
-
class SSH
|
6
|
-
def
|
7
|
-
|
6
|
+
class SSH < Base
|
7
|
+
def self.handle?(uri)
|
8
|
+
'ssh' == uri.scheme
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
+
def initialize(uri, logger)
|
12
|
+
@cnx = Net::SSH.start(uri.host, uri.user)
|
13
|
+
super logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def exec_imp(command, stdin_str)
|
11
17
|
exit_status = 1
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
channel.
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
exit_status = data.read_long
|
24
|
-
end
|
25
|
-
channel.send_data stdin_str if stdin_str
|
26
|
-
channel.eof!
|
18
|
+
stdout_str = stderr_str = ''
|
19
|
+
@cnx.open_channel do |channel|
|
20
|
+
channel.exec(command) do |ch, success|
|
21
|
+
channel.on_data do |ch, data|
|
22
|
+
stdout_str += data
|
23
|
+
end
|
24
|
+
channel.on_extended_data do |ch, type, data|
|
25
|
+
stderr_str += data
|
26
|
+
end
|
27
|
+
channel.on_request 'exit-status' do |ch, data|
|
28
|
+
exit_status = data.read_long
|
27
29
|
end
|
30
|
+
channel.send_data stdin_str.force_encoding('ASCII-8BIT') if stdin_str
|
31
|
+
channel.eof!
|
28
32
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
# result.chomp if result.respond_to? :chomp # as result can be nil <=> result.chomp if result <=> result.try? :chomp (rails way)
|
33
|
-
# end
|
34
|
-
return nil if status_only && 0 != exit_status
|
35
|
-
stdout_str.chomp
|
33
|
+
end
|
34
|
+
@cnx.loop
|
35
|
+
[ stdout_str, stderr_str, exit_status ]
|
36
36
|
end
|
37
37
|
|
38
38
|
def close
|
data/lib/cindy/template.rb
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
require 'erb'
|
2
|
-
require 'uri'
|
3
2
|
require 'ostruct'
|
3
|
+
require 'shellwords'
|
4
4
|
|
5
5
|
module Cindy
|
6
6
|
class Template
|
7
7
|
|
8
|
-
attr_reader :file, :alias, :paths, :defvars, :envvars
|
8
|
+
attr_reader :file, :alias, :paths, :defvars, :envvars, :postcmds
|
9
9
|
|
10
|
-
def initialize(file, name)
|
10
|
+
def initialize(owner, file, name)
|
11
|
+
@owner = owner # owner is Cindy objet
|
11
12
|
@file = file # local template filename
|
12
13
|
@alias = name
|
13
14
|
@paths = {} # remote filenames (<environment name> => <filename>)
|
14
15
|
@defvars = {} # default/global variables
|
15
16
|
@envvars = {} # environment specific variables
|
17
|
+
@postcmds = [] # commands to run after deployment
|
16
18
|
end
|
17
19
|
|
18
20
|
IDENT_STRING = ' ' * 4
|
@@ -38,15 +40,25 @@ module Cindy
|
|
38
40
|
puts render(env)
|
39
41
|
end
|
40
42
|
|
43
|
+
def add_postcmd(cmd, options)
|
44
|
+
@postcmds << cmd
|
45
|
+
end
|
46
|
+
|
41
47
|
def deploy(env)
|
42
48
|
executor = executor_for_env env
|
43
49
|
remote_filename = @paths[env.name]
|
44
50
|
sudo = ''
|
45
|
-
sudo = 'sudo
|
51
|
+
sudo = 'sudo' unless 0 == executor.exec('id -u').to_i
|
46
52
|
suffix = executor.exec('date \'+%Y%m%d%H%M\'') # use remote - not local - time machine
|
47
53
|
executor.exec("[ -e \"#{remote_filename}\" ] && [ ! -h \"#{remote_filename}\" ] && #{sudo} mv -i \"#{remote_filename}\" \"#{remote_filename}.pre\"")
|
48
|
-
executor.exec("#{sudo} tee #{remote_filename}.#{suffix} > /dev/null", render(env, executor))
|
54
|
+
executor.exec("#{sudo} tee #{remote_filename}.#{suffix} > /dev/null", stdin: render(env, executor))
|
49
55
|
executor.exec("#{sudo} ln -snf \"#{remote_filename}.#{suffix}\" \"#{remote_filename}\"")
|
56
|
+
shell = executor.exec('ps -p $$ -ocomm=')
|
57
|
+
env = { 'INSTALL_FILE' => remote_filename }
|
58
|
+
env_string = env.inject([]) { |a, b| a << b.map(&:shellescape).join('=') }.join(' ')
|
59
|
+
@postcmds.each do |cmd|
|
60
|
+
executor.exec("#{sudo} #{'env' if shell =~ /csh\z/} #{env_string} sh -c '#{cmd}'") # TODO: escape single quotes in cmd?
|
61
|
+
end
|
50
62
|
executor.close
|
51
63
|
end
|
52
64
|
|
@@ -94,15 +106,7 @@ module Cindy
|
|
94
106
|
private
|
95
107
|
|
96
108
|
def executor_for_env(env)
|
97
|
-
|
98
|
-
case uri.scheme
|
99
|
-
when nil, 'file'
|
100
|
-
Executor::Local.new
|
101
|
-
when 'ssh'
|
102
|
-
Executor::SSH.new Net::SSH.start(uri.host, uri.user)
|
103
|
-
else
|
104
|
-
raise Exception.new 'Unexpected protocol'
|
105
|
-
end
|
109
|
+
Executor::Base.from_uri env.uri, @owner.logger
|
106
110
|
end
|
107
111
|
|
108
112
|
def render(env, executor = nil)
|
data/lib/cindy/version.rb
CHANGED
File without changes
|
data/test/test_cindy.rb
CHANGED
@@ -1,9 +1,20 @@
|
|
1
|
+
require 'logger'
|
1
2
|
require 'minitest_helper'
|
2
3
|
|
3
4
|
class CindyTest < Minitest::Test
|
4
5
|
|
5
|
-
def
|
6
|
-
|
6
|
+
def test_deploy
|
7
|
+
cindy = Cindy::Cindy.from_string <<-EOS
|
8
|
+
environment :foo, 'file:///'
|
9
|
+
|
10
|
+
template :bar, '#{File.join(__dir__, 'templates', 'deploy.tpl')}' do
|
11
|
+
on :foo, '/tmp/#{$$}'
|
12
|
+
end
|
13
|
+
EOS
|
14
|
+
cindy.logger = Logger.new File.open('/dev/null', File::WRONLY)
|
15
|
+
|
16
|
+
cindy.template_environment_deploy :foo, :bar
|
17
|
+
assert File.symlink?("/tmp/#{$$}")
|
7
18
|
end
|
8
19
|
|
9
20
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'minitest_helper'
|
3
|
+
|
4
|
+
class LocalExecutorTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_local_executor
|
7
|
+
logger = Logger.new File.open('/dev/null', File::WRONLY)
|
8
|
+
locexec = Cindy::Executor::Local.new nil, logger
|
9
|
+
# unknown command
|
10
|
+
assert_raises Cindy::Executor::CommandFailedError do
|
11
|
+
locexec.exec "eko foo"
|
12
|
+
end
|
13
|
+
assert_equal '', locexec.exec("eko foo", ignore_failure: true)
|
14
|
+
assert !locexec.exec("eko foo", ignore_failure: true, check_status_only: true)
|
15
|
+
# command failure
|
16
|
+
assert_raises Cindy::Executor::CommandFailedError do
|
17
|
+
locexec.exec 'grep -q foo', stdin: 'bar'
|
18
|
+
end
|
19
|
+
assert_equal '', locexec.exec("grep -q foo", stdin: 'bar', ignore_failure: true)
|
20
|
+
assert !locexec.exec("grep -q foo", stdin: 'bar', ignore_failure: true, check_status_only: true)
|
21
|
+
# valid command
|
22
|
+
assert_equal 'foo', locexec.exec('echo foo')
|
23
|
+
assert_equal 'foo', locexec.exec('echo foo', ignore_failure: true)
|
24
|
+
assert locexec.exec('echo foo', ignore_failure: true, check_status_only: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cindy-cm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- julp
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: net-ssh
|
@@ -84,14 +84,17 @@ files:
|
|
84
84
|
- lib/cindy/cli.rb
|
85
85
|
- lib/cindy/command.rb
|
86
86
|
- lib/cindy/environment.rb
|
87
|
+
- lib/cindy/executor/base.rb
|
87
88
|
- lib/cindy/executor/local.rb
|
88
89
|
- lib/cindy/executor/ssh.rb
|
89
90
|
- lib/cindy/template.rb
|
90
91
|
- lib/cindy/variable.rb
|
91
92
|
- lib/cindy/version.rb
|
92
93
|
- test/minitest_helper.rb
|
94
|
+
- test/templates/deploy.tpl
|
93
95
|
- test/test_cindy.rb
|
94
96
|
- test/test_dsl.rb
|
97
|
+
- test/test_local_executor.rb
|
95
98
|
homepage: https://github.com/julp/cindy
|
96
99
|
licenses:
|
97
100
|
- BSD
|
@@ -112,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
115
|
version: '0'
|
113
116
|
requirements: []
|
114
117
|
rubyforge_project:
|
115
|
-
rubygems_version: 2.4.
|
118
|
+
rubygems_version: 2.4.8
|
116
119
|
signing_key:
|
117
120
|
specification_version: 4
|
118
121
|
summary: Turn out your configuration files into ERB templates and deploy them
|