cindy-cm 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|