cindy-cm 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 +7 -0
- data/Changelog.md +7 -0
- data/Gemfile +4 -0
- data/README.md +121 -0
- data/Rakefile +9 -0
- data/bin/cindy +54 -0
- data/cindy.gemspec +22 -0
- data/lib/cindy/all.rb +11 -0
- data/lib/cindy/cindy.rb +151 -0
- data/lib/cindy/cli.rb +96 -0
- data/lib/cindy/command.rb +19 -0
- data/lib/cindy/environment.rb +20 -0
- data/lib/cindy/executor/local.rb +29 -0
- data/lib/cindy/executor/ssh.rb +43 -0
- data/lib/cindy/template.rb +130 -0
- data/lib/cindy/variable.rb +28 -0
- data/lib/cindy/version.rb +3 -0
- data/test/minitest_helper.rb +4 -0
- data/test/test_cindy.rb +9 -0
- data/test/test_dsl.rb +40 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 774f6b5df9922b3a6771968f4fb61e05d8b0f3dc
|
4
|
+
data.tar.gz: 647e09f6a7ebb222966f785c49492a525fd92c5f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8ecafc4acc58778989dbab0e650d18a70e026ccf985593ca1c1d5adf2003da8c804b90e5bfae9bad47571a90e9260335046f1a264c208fb0c8028052c411f309
|
7
|
+
data.tar.gz: 2a51c783e0ab07d5b95e0448c5b8e7bf6737cc7804dfc3935ef56fe629c29b23b7bab67d55a1c3d7c946c805dc5c563a7acefbe3566f0c85f56257cdd7f534a8
|
data/Changelog.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
### Changes from 0.0.1 to 0.1.0
|
2
|
+
|
3
|
+
* configuration format changed (XML => Ruby)
|
4
|
+
* shell completion added
|
5
|
+
* reload command added
|
6
|
+
* alternate configuration file can be specified through environment variable CINDY_CONF (eg, with (ba|k|z)sh: `CINDY_CONF=alternate/path cindy`)
|
7
|
+
* all commands which change internal state removed
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
## Introduction
|
2
|
+
|
3
|
+
Tired to modify your configuration files depending on the targeted computer? Turn them out into ERB templates and deploy them in one command.
|
4
|
+
|
5
|
+
The purpose is to implement a kind of shell with limited dependencies to automate configuration and deployment on various (Unix) environments.
|
6
|
+
|
7
|
+
Dependencies: net-ssh, highline
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
`gem install cindy-cm`
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
* reload => force Cindy to reload its configuration file
|
16
|
+
* environment (shortcut: env)
|
17
|
+
* list => list all known environments
|
18
|
+
* template (shortcut: tpl)
|
19
|
+
* list => list all known templates
|
20
|
+
* \<name>
|
21
|
+
* environment \<name>
|
22
|
+
* deploy => install the generated file on the given environment
|
23
|
+
* print => display output configuration file as it would be deployed on the given environment
|
24
|
+
* details => list all applicable variables to the given template, their values and scopes
|
25
|
+
## Example
|
26
|
+
|
27
|
+
Create ~/.cindy as follows:
|
28
|
+
```ruby
|
29
|
+
# create 2 environments named "production" and "development"
|
30
|
+
environment :development, 'file:///'
|
31
|
+
environment :production, 'ssh://root@www.xxx.tld/'
|
32
|
+
|
33
|
+
# register the template /home/julp/cindy/templates/nginx.conf.tpl (see below) for our nginx configuration
|
34
|
+
template :nginx, '/home/julp/cindy/templates/nginx.conf.tpl' do
|
35
|
+
# default variables
|
36
|
+
|
37
|
+
# have_gzip_static will be set to true or false depending on the result of the following command
|
38
|
+
var :have_gzip_static, Command.new('nginx -V 2>&1 >/dev/null | grep -qF -- --with-http_gzip_static_module') # (ba|k)sh
|
39
|
+
|
40
|
+
on :production, '/usr/local/etc/nginx.conf' do
|
41
|
+
# define and/or override some variables for nginx when on our production environment
|
42
|
+
var :server_name, 'www.xxx.tld'
|
43
|
+
var :root, '/home/julp/app/current/public'
|
44
|
+
var :have_gzip_static, Command.new('(nginx -V > /dev/null) |& grep -qF -- --with-http_gzip_static_module') # (t)csh
|
45
|
+
end
|
46
|
+
|
47
|
+
on :development, '/etc/nginx.conf' do
|
48
|
+
# (re)define variables for nginx when in development
|
49
|
+
var :server_name, 'www.xxx.lan'
|
50
|
+
var :root, '/home/julp/app/public'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
And /home/julp/cindy/templates/nginx.conf.tpl as:
|
56
|
+
```
|
57
|
+
# <%= _install_file_ %>
|
58
|
+
|
59
|
+
server {
|
60
|
+
server_name <%= server_name %>;
|
61
|
+
root <%= root %>;
|
62
|
+
|
63
|
+
<% if false %>
|
64
|
+
this content never appears
|
65
|
+
<% end %>
|
66
|
+
|
67
|
+
<% if have_gzip_static %>
|
68
|
+
gzip_static on;
|
69
|
+
<% end %>
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
Running `cindy template nginx environment production print`, result in:
|
74
|
+
```
|
75
|
+
# /usr/local/etc/nginx.conf
|
76
|
+
|
77
|
+
server {
|
78
|
+
server_name www.xxx.tld;
|
79
|
+
root /home/julp/app/current/public;
|
80
|
+
|
81
|
+
gzip_static on;
|
82
|
+
}
|
83
|
+
```
|
84
|
+
(if we admit that nginx is built, on production environment, with Gzip Precompression module)
|
85
|
+
|
86
|
+
After `cindy template nginx environment production deploy`, output of `ls -l /etc/nginx.conf*` is:
|
87
|
+
```
|
88
|
+
lrwxrwxrwx [...] /usr/local/etc/nginx.conf -> /usr/local/etc/nginx.conf.201502262311
|
89
|
+
-rw-r--r-- [...] /usr/local/etc/nginx.conf.201502262209 # file at previous deployment
|
90
|
+
-rw-r--r-- [...] /usr/local/etc/nginx.conf.201502262311 # current version (1h02m later)
|
91
|
+
```
|
92
|
+
|
93
|
+
## What is a *command* typed variable?
|
94
|
+
|
95
|
+
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
|
+
time the template is rendered. It is more convenient mainly if this value can change at any time.
|
97
|
+
|
98
|
+
The result of the command is a boolean based on its exit status (0 => true, everithing else => false) if the command does not print anything on
|
99
|
+
standard output else a string with the content of standard output.
|
100
|
+
|
101
|
+
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
|
+
server, is compiled or not with the gzip_static module.
|
103
|
+
|
104
|
+
As you can see in this same example, note that commands may depend on the "remote shell" (redirections in particular) and also on the value of the
|
105
|
+
PATH environment variable.
|
106
|
+
|
107
|
+
## Predefined variables in templates
|
108
|
+
|
109
|
+
* `_install_dir_`: directory in which output file will be deployed (equivalent to `File.dirname _install_file_` but more convenient)
|
110
|
+
* `_install_file_`: filename under which the file will be deployed
|
111
|
+
|
112
|
+
## Limitations
|
113
|
+
|
114
|
+
* `sudo` prompt to ask passwords is not handled: use a passwordless configuration
|
115
|
+
* you need to configure your ssh keys in `~/.ssh/config`, eg:
|
116
|
+
|
117
|
+
```
|
118
|
+
Host www.domain.tld
|
119
|
+
User julp
|
120
|
+
IdentityFile /path/to/your/private/key
|
121
|
+
```
|
data/Rakefile
ADDED
data/bin/cindy
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'cindy/all'
|
4
|
+
require 'cindy/cli'
|
5
|
+
|
6
|
+
require 'readline'
|
7
|
+
require 'shellwords'
|
8
|
+
|
9
|
+
cli = Cindy::CLI.new
|
10
|
+
|
11
|
+
Readline.completion_append_character = " "
|
12
|
+
Readline.completion_proc = proc do |s|
|
13
|
+
args = Shellwords.split Readline.line_buffer[0, Readline.point]
|
14
|
+
args.pop unless Readline.line_buffer.end_with? Readline.completion_append_character
|
15
|
+
(
|
16
|
+
case args
|
17
|
+
when []
|
18
|
+
%w(reload environment template)
|
19
|
+
when %w(environment), %w(env)
|
20
|
+
%w(list)
|
21
|
+
when %w(template), %w(tpl)
|
22
|
+
%w(list) + cli.templates
|
23
|
+
else
|
24
|
+
if 'template' == Cindy::CLI::ARGS_ALIASES[args[0]] && args.length >= 2
|
25
|
+
tplname = args.delete_at 1
|
26
|
+
if 1 == args.length
|
27
|
+
%w(environment) unless 'list' == tplname
|
28
|
+
elsif 'environment' == Cindy::CLI::ARGS_ALIASES[args[1]]
|
29
|
+
case args.length
|
30
|
+
when 2
|
31
|
+
cli.environments
|
32
|
+
when 3
|
33
|
+
%w(deploy print details)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end || []
|
38
|
+
).select { |v| v.start_with? s }
|
39
|
+
end
|
40
|
+
|
41
|
+
CINDY_EXCEPTIONS = ObjectSpace.each_object(Class).select { |v| v.ancestors.include?(Exception) && 'Cindy' == v.name.split('::').first }
|
42
|
+
|
43
|
+
if ARGV.any?
|
44
|
+
cli.parse ARGV
|
45
|
+
else
|
46
|
+
while line = Readline.readline('# ', true)
|
47
|
+
begin
|
48
|
+
cli.parse Shellwords.split(line)
|
49
|
+
rescue => e
|
50
|
+
raise unless CINDY_EXCEPTIONS.include? e.class
|
51
|
+
puts "[ ERR ] #{e}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/cindy.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cindy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'cindy-cm'
|
8
|
+
s.version = Cindy::VERSION
|
9
|
+
s.authors = ['julp']
|
10
|
+
s.summary = 'Turn out your configuration files into ERB templates and deploy them'
|
11
|
+
s.homepage = 'https://github.com/julp/cindy'
|
12
|
+
s.license = 'BSD'
|
13
|
+
s.files = `git ls-files -z`.split("\x0")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
#s.require_paths = %w(lib)
|
17
|
+
s.required_ruby_version = '>= 2.0.0'
|
18
|
+
s.add_dependency 'net-ssh'
|
19
|
+
s.add_dependency 'highline'
|
20
|
+
s.add_development_dependency 'bundler'
|
21
|
+
s.add_development_dependency 'rake'
|
22
|
+
end
|
data/lib/cindy/all.rb
ADDED
data/lib/cindy/cindy.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
module Cindy
|
2
|
+
|
3
|
+
class UndefinedEnvironmentError < ::NameError
|
4
|
+
end
|
5
|
+
|
6
|
+
class UndefinedTemplateError < ::NameError
|
7
|
+
end
|
8
|
+
|
9
|
+
class AlreadyExistsError < ::NameError
|
10
|
+
end
|
11
|
+
|
12
|
+
class Cindy
|
13
|
+
|
14
|
+
CONFIGURATION_FILE = File.expand_path '~/.cindy'
|
15
|
+
|
16
|
+
module DSL
|
17
|
+
class TemplateEnvironmentNode
|
18
|
+
def initialize(tpl, envname)
|
19
|
+
@tpl = tpl
|
20
|
+
@envname = envname
|
21
|
+
end
|
22
|
+
|
23
|
+
def var(varname, value)
|
24
|
+
@tpl.set_variable @envname, varname, value
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :variable, :var
|
28
|
+
end
|
29
|
+
|
30
|
+
class TemplateNode
|
31
|
+
def initialize(tpl)
|
32
|
+
@tpl = tpl
|
33
|
+
end
|
34
|
+
|
35
|
+
def var(varname, value)
|
36
|
+
@tpl.set_variable nil, varname, value
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method :variable, :var
|
40
|
+
|
41
|
+
def on(envname, file, &block)
|
42
|
+
@tpl.set_path_for_environment envname, file
|
43
|
+
TemplateEnvironmentNode.new(@tpl, envname).instance_eval &block if block_given?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class CindyNode
|
48
|
+
def initialize(cindy)
|
49
|
+
@cindy = cindy
|
50
|
+
end
|
51
|
+
|
52
|
+
def template(tplname, path, &block)
|
53
|
+
tpl = @cindy.template_add tplname, path
|
54
|
+
TemplateNode.new(tpl).instance_eval &block
|
55
|
+
tpl
|
56
|
+
end
|
57
|
+
|
58
|
+
def environment(envname, uri = nil)
|
59
|
+
@cindy.environment_add envname, uri
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@environments = {}
|
66
|
+
@templates = {}
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.from_string(string)
|
70
|
+
cindy = Cindy.new
|
71
|
+
DSL::CindyNode.new(cindy).instance_eval string
|
72
|
+
cindy
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.load(filename = nil)
|
76
|
+
@filename = filename || CONFIGURATION_FILE
|
77
|
+
cindy = Cindy.new
|
78
|
+
DSL::CindyNode.new(cindy).instance_eval(File.read(@filename), File.basename(@filename), 0)
|
79
|
+
cindy
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_s
|
83
|
+
(@environments.values.map(&:to_s) + [''] + @templates.values.map(&:to_s)).join("\n")
|
84
|
+
end
|
85
|
+
|
86
|
+
def environments
|
87
|
+
@environments.values
|
88
|
+
end
|
89
|
+
|
90
|
+
def has_environment?(envname)
|
91
|
+
envname = envname.intern
|
92
|
+
@environments.key? envname
|
93
|
+
end
|
94
|
+
|
95
|
+
def environment_add(envname, attributes)
|
96
|
+
envname = envname.intern
|
97
|
+
# assert !@environments.key? envname
|
98
|
+
@environments[envname] = Environment.new(envname, attributes)
|
99
|
+
end
|
100
|
+
|
101
|
+
def templates
|
102
|
+
@templates.values
|
103
|
+
end
|
104
|
+
|
105
|
+
def has_template?(tplname)
|
106
|
+
tplname = tplname.intern
|
107
|
+
@templates.key? tplname
|
108
|
+
end
|
109
|
+
|
110
|
+
def template_add(tplname, file)
|
111
|
+
tplname = tplname.intern
|
112
|
+
# assert !@templates.key? name
|
113
|
+
@templates[tplname] = Template.new File.expand_path(file), tplname
|
114
|
+
end
|
115
|
+
|
116
|
+
def template_environment_print(envname, tplname)
|
117
|
+
envname = envname.intern
|
118
|
+
tplname = tplname.intern
|
119
|
+
check_environment! envname
|
120
|
+
check_template! tplname
|
121
|
+
@templates[tplname].print @environments[envname]
|
122
|
+
end
|
123
|
+
|
124
|
+
def template_environment_deploy(envname, tplname)
|
125
|
+
envname = envname.intern
|
126
|
+
tplname = tplname.intern
|
127
|
+
check_environment! envname
|
128
|
+
check_template! tplname
|
129
|
+
@templates[tplname].deploy @environments[envname]
|
130
|
+
end
|
131
|
+
|
132
|
+
def template_environment_variables(envname, tplname)
|
133
|
+
envname = envname.intern
|
134
|
+
tplname = tplname.intern
|
135
|
+
check_environment! envname
|
136
|
+
check_template! tplname
|
137
|
+
@templates[tplname].list_variables envname
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def check_environment!(envname)
|
143
|
+
raise UndefinedEnvironmentError.new "call to an undefined environment: #{envname}" unless has_environment? envname
|
144
|
+
end
|
145
|
+
|
146
|
+
def check_template!(tplname)
|
147
|
+
raise UndefinedTemplateError.new "call to an undefined template: #{tplname}" unless has_template? tplname
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
data/lib/cindy/cli.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module Cindy
|
2
|
+
class CLI
|
3
|
+
ARGS_ALIASES = Hash.new do |h,k|
|
4
|
+
k
|
5
|
+
end.merge!({ 'tpl' => 'template', 'var' => 'variable', 'env' => 'environment' })
|
6
|
+
|
7
|
+
class InvalidArgumentError < ::ArgumentError
|
8
|
+
def initialize(given, expected)
|
9
|
+
super "invalid argument '#{given}'" + case expected
|
10
|
+
when String
|
11
|
+
", expecting '#{expected}'"
|
12
|
+
when Array
|
13
|
+
", expecting one of: '#{expected.join('\', \'')}'"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# class TooManyArgumentError < ::ArgumentError
|
19
|
+
# def initialize
|
20
|
+
# super "too many arguments"
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class TooFewArgumentError < ::ArgumentError
|
25
|
+
# def initialize
|
26
|
+
# super "too few arguments"
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@cindy = Cindy.load ENV['CINDY_CONF']
|
32
|
+
end
|
33
|
+
|
34
|
+
def environments
|
35
|
+
@cindy.environments.map { |v| v.name.to_s }
|
36
|
+
end
|
37
|
+
|
38
|
+
def templates
|
39
|
+
@cindy.templates.map { |v| v.alias.to_s }
|
40
|
+
end
|
41
|
+
|
42
|
+
# def check_args_count(given, expected, method = :"==")
|
43
|
+
# raise (given > expected ? TooManyArgumentError : TooFewArgumentError).new unless given.send(method, expected)
|
44
|
+
# end
|
45
|
+
|
46
|
+
def parse(args)
|
47
|
+
arg = args.shift
|
48
|
+
case ARGS_ALIASES[arg]
|
49
|
+
when 'reload'
|
50
|
+
# assert 0 == args.length
|
51
|
+
@cindy = Cindy.load ENV['CINDY_CONF']
|
52
|
+
when 'environment'
|
53
|
+
arg = args.shift
|
54
|
+
case arg
|
55
|
+
when 'list'
|
56
|
+
# assert 0 == args.length
|
57
|
+
@cindy.environments.each do |env|
|
58
|
+
puts "- #{env.name}: #{env.uri}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
when 'template'
|
62
|
+
arg = args.shift
|
63
|
+
case arg
|
64
|
+
when 'list'
|
65
|
+
# assert 0 == args.length
|
66
|
+
@cindy.templates.each do |tpl|
|
67
|
+
puts "> #{tpl.alias}: #{tpl.file}"
|
68
|
+
end
|
69
|
+
else
|
70
|
+
tplname = arg
|
71
|
+
arg = args.shift
|
72
|
+
case ARGS_ALIASES[arg]
|
73
|
+
when 'environment'
|
74
|
+
# assert args.length >= 2
|
75
|
+
envname = args.shift
|
76
|
+
arg = args.shift
|
77
|
+
case ARGS_ALIASES[arg]
|
78
|
+
when 'details'
|
79
|
+
# assert 0 == args.length
|
80
|
+
@cindy.template_environment_variables(envname, tplname)
|
81
|
+
when 'deploy', 'print'
|
82
|
+
# assert 0 == args.length
|
83
|
+
@cindy.send(:"template_environment_#{arg}", envname, tplname)
|
84
|
+
else
|
85
|
+
raise InvalidArgumentError.new arg, %w(details deploy print)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
raise InvalidArgumentError.new arg, %w(environment)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
else
|
92
|
+
raise InvalidArgumentError.new arg, %w(reload environment template)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Cindy
|
2
|
+
class Command
|
3
|
+
def initialize(command)
|
4
|
+
@command = command
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
@command
|
9
|
+
end
|
10
|
+
|
11
|
+
def inspect
|
12
|
+
"#{self.class.name}.new(#{@command.inspect})"
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(executor)
|
16
|
+
executor.exec(@command, nil, true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Cindy
|
2
|
+
class Environment
|
3
|
+
|
4
|
+
attr_reader :name, :uri
|
5
|
+
|
6
|
+
def initialize(name, uri)
|
7
|
+
@uri = uri
|
8
|
+
@name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"environment :#{@name}, #{@uri.inspect}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(attributes)
|
16
|
+
@uri = attributes['uri'] if attributes['uri']
|
17
|
+
@name = attributes['name'] if attributes['name']
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Cindy
|
4
|
+
module Executor
|
5
|
+
class Local
|
6
|
+
def exec(command, stdin_str = nil, status_only = false)
|
7
|
+
exit_status = 1
|
8
|
+
stdout_str = stderr_str = ''
|
9
|
+
Open3.popen3({ 'PATH' => "#{ENV['PATH']}:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin" }, command) do |stdin, stdout, stderr, wait_thr|
|
10
|
+
if stdin_str
|
11
|
+
stdin.write stdin_str
|
12
|
+
stdin.close
|
13
|
+
end
|
14
|
+
stdout_str = stdout.read
|
15
|
+
stderr_str = stderr.read
|
16
|
+
exit_status = wait_thr.value
|
17
|
+
end
|
18
|
+
# puts [ command, stderr_str, exit_status ].inspect
|
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
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Cindy
|
4
|
+
module Executor
|
5
|
+
class SSH
|
6
|
+
def initialize(cnx)
|
7
|
+
@cnx = cnx
|
8
|
+
end
|
9
|
+
|
10
|
+
def exec(command, stdin_str = nil, status_only = false)
|
11
|
+
exit_status = 1
|
12
|
+
# if stdin_str
|
13
|
+
stdout_str = stderr_str = ''
|
14
|
+
@cnx.open_channel do |channel|
|
15
|
+
channel.exec(command) do |ch, success|
|
16
|
+
channel.on_data do |ch, data|
|
17
|
+
stdout_str += data
|
18
|
+
end
|
19
|
+
channel.on_extended_data do |ch, type, data|
|
20
|
+
stderr_str += data
|
21
|
+
end
|
22
|
+
channel.on_request 'exit-status' do |ch, data|
|
23
|
+
exit_status = data.read_long
|
24
|
+
end
|
25
|
+
channel.send_data stdin_str if stdin_str
|
26
|
+
channel.eof!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@cnx.loop
|
30
|
+
# else
|
31
|
+
# result = @cnx.exec!(command)
|
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
|
36
|
+
end
|
37
|
+
|
38
|
+
def close
|
39
|
+
@cnx.close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'uri'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module Cindy
|
6
|
+
class Template
|
7
|
+
|
8
|
+
attr_reader :file, :alias, :paths, :defvars, :envvars
|
9
|
+
|
10
|
+
def initialize(file, name)
|
11
|
+
@file = file # local template filename
|
12
|
+
@alias = name
|
13
|
+
@paths = {} # remote filenames (<environment name> => <filename>)
|
14
|
+
@defvars = {} # default/global variables
|
15
|
+
@envvars = {} # environment specific variables
|
16
|
+
end
|
17
|
+
|
18
|
+
IDENT_STRING = ' ' * 4
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
ret = ["template :#{@alias}, #{@file.inspect} do"]
|
22
|
+
@defvars.each_pair do |k,v|
|
23
|
+
ret << "#{IDENT_STRING * 1}var :#{k}, #{v.inspect}"
|
24
|
+
end
|
25
|
+
@paths.each_pair do |ke,ve|
|
26
|
+
ret << "#{IDENT_STRING * 1}on :#{ke}, #{ve.inspect} do"
|
27
|
+
@envvars[ke].each_pair do |kv,vv|
|
28
|
+
ret << "#{IDENT_STRING * 2}var :#{kv}, #{vv.inspect}"
|
29
|
+
end
|
30
|
+
ret << "#{IDENT_STRING * 1}end"
|
31
|
+
end
|
32
|
+
ret << "end"
|
33
|
+
ret << ''
|
34
|
+
ret.join "\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
def print(env)
|
38
|
+
puts render(env)
|
39
|
+
end
|
40
|
+
|
41
|
+
def deploy(env)
|
42
|
+
executor = executor_for_env env
|
43
|
+
remote_filename = @paths[env.name]
|
44
|
+
sudo = ''
|
45
|
+
sudo = 'sudo ' unless 0 == executor.exec('id -u').to_i
|
46
|
+
suffix = executor.exec('date \'+%Y%m%d%H%M\'') # use remote - not local - time machine
|
47
|
+
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))
|
49
|
+
executor.exec("#{sudo} ln -snf \"#{remote_filename}.#{suffix}\" \"#{remote_filename}\"")
|
50
|
+
executor.close
|
51
|
+
end
|
52
|
+
|
53
|
+
# def variables
|
54
|
+
# (@defvars.keys + @envvars.collect { |v| v[1].keys }.flatten).uniq
|
55
|
+
# end
|
56
|
+
|
57
|
+
def list_variables(envname)
|
58
|
+
@defvars.merge(@envvars[envname]).each_pair do |k,v|
|
59
|
+
puts "- #{k}#{' (default)' unless @envvars[envname].key? k } = #{v} (#{v.class.name})"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# def unset_variable(varname)
|
64
|
+
# @defvars.delete varname
|
65
|
+
# @envvars.each_value do |h|
|
66
|
+
# h.delete varname
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
|
70
|
+
# def rename_variable(oldvarname, newvarname)
|
71
|
+
# @defvars[newvarname] = value if value = @defvars.delete(oldvarname)
|
72
|
+
# @envvars.each_value do |h|
|
73
|
+
# h[newvarname] = value if value = h.delete(oldvarname)
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
|
77
|
+
def set_variable(envname, varname, value)
|
78
|
+
envname = envname.intern if envname
|
79
|
+
varname = varname.intern
|
80
|
+
STDERR.puts "[ WARN ] non standard variable name found" unless varname =~ /\A[a-z][a-z0-9_]*\z/
|
81
|
+
if envname
|
82
|
+
@envvars[envname][varname] = value
|
83
|
+
else
|
84
|
+
@defvars[varname] = value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def set_path_for_environment(envname, path)
|
89
|
+
envname = envname.intern
|
90
|
+
@paths[envname] = path
|
91
|
+
@envvars[envname] ||= {}
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def executor_for_env(env)
|
97
|
+
uri = URI.parse(env.uri)
|
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
|
106
|
+
end
|
107
|
+
|
108
|
+
def render(env, executor = nil)
|
109
|
+
close_executor = executor.nil?
|
110
|
+
executor ||= executor_for_env(env)
|
111
|
+
# shell = executor.exec('ps -p $$ -ocomm=')
|
112
|
+
vars = Hash[
|
113
|
+
@defvars.merge(@envvars[env.name]).map do |k, v|
|
114
|
+
if v.respond_to? :call
|
115
|
+
[ k, v.call(executor) ]
|
116
|
+
else
|
117
|
+
[ k, v ]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
]
|
121
|
+
# ||= to not overwrite a previously user defined variable with the same name
|
122
|
+
vars['_install_file_'] ||= @paths[env.name]
|
123
|
+
vars['_install_dir_'] ||= File.dirname @paths[env.name]
|
124
|
+
erb = ERB.new(File.read(@file), 0, '-')
|
125
|
+
executor.close if close_executor
|
126
|
+
erb.result(OpenStruct.new(vars).instance_eval { binding })
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Cindy
|
2
|
+
class Variable
|
3
|
+
TAG_NAME = self.name.split('::').last.downcase
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def parse_boolean(string)
|
7
|
+
'true' == string
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse_string(string)
|
11
|
+
string
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse_command(string)
|
15
|
+
Command.new string
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse_int(string)
|
19
|
+
string.to_i
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
TYPES = public_methods.inject([]) do |ret,v|
|
24
|
+
ret << $' if v =~ /\Aparse_/
|
25
|
+
ret
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/test/test_cindy.rb
ADDED
data/test/test_dsl.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
class DSLTest < Minitest::Test
|
4
|
+
|
5
|
+
def test_on_without_block
|
6
|
+
cindy = Cindy::Cindy.from_string <<-EOS
|
7
|
+
environment :foo
|
8
|
+
|
9
|
+
template :bar, 'abc' do
|
10
|
+
on :foo, 'def'
|
11
|
+
end
|
12
|
+
EOS
|
13
|
+
|
14
|
+
tpl = cindy.templates[0]
|
15
|
+
assert_equal tpl.alias, :bar
|
16
|
+
assert_equal tpl.paths[:foo], 'def'
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_string_equivalent_to_symbol
|
20
|
+
cindy = Cindy::Cindy.from_string <<-EOS
|
21
|
+
environment 'foo'
|
22
|
+
|
23
|
+
template 'bar', 'abc' do
|
24
|
+
on 'foo', 'def' do
|
25
|
+
var 'x', 'y'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
EOS
|
29
|
+
|
30
|
+
assert cindy.has_environment? :foo
|
31
|
+
assert cindy.has_environment? 'foo'
|
32
|
+
assert cindy.has_template? :bar
|
33
|
+
assert cindy.has_template? 'bar'
|
34
|
+
tpl = cindy.templates[0]
|
35
|
+
assert tpl.envvars.key? :foo
|
36
|
+
# assert tpl.has_variable? :x
|
37
|
+
# assert tpl.has_variable? 'x'
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cindy-cm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- julp
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ssh
|
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: highline
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
executables:
|
72
|
+
- cindy
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- Changelog.md
|
77
|
+
- Gemfile
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- bin/cindy
|
81
|
+
- cindy.gemspec
|
82
|
+
- lib/cindy/all.rb
|
83
|
+
- lib/cindy/cindy.rb
|
84
|
+
- lib/cindy/cli.rb
|
85
|
+
- lib/cindy/command.rb
|
86
|
+
- lib/cindy/environment.rb
|
87
|
+
- lib/cindy/executor/local.rb
|
88
|
+
- lib/cindy/executor/ssh.rb
|
89
|
+
- lib/cindy/template.rb
|
90
|
+
- lib/cindy/variable.rb
|
91
|
+
- lib/cindy/version.rb
|
92
|
+
- test/minitest_helper.rb
|
93
|
+
- test/test_cindy.rb
|
94
|
+
- test/test_dsl.rb
|
95
|
+
homepage: https://github.com/julp/cindy
|
96
|
+
licenses:
|
97
|
+
- BSD
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: 2.0.0
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.4.6
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: Turn out your configuration files into ERB templates and deploy them
|
119
|
+
test_files: []
|