propro 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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +339 -0
  5. data/README.md +134 -0
  6. data/Rakefile +9 -0
  7. data/bin/propro +6 -0
  8. data/examples/vagrant.propro +41 -0
  9. data/examples/vps_webserver.propro +51 -0
  10. data/ext/bash/app/nginx.sh +9 -0
  11. data/ext/bash/app/node.sh +5 -0
  12. data/ext/bash/app/pg.sh +5 -0
  13. data/ext/bash/app/puma/nginx.sh +58 -0
  14. data/ext/bash/app/puma.sh +64 -0
  15. data/ext/bash/app/rvm.sh +7 -0
  16. data/ext/bash/app/sidekiq.sh +69 -0
  17. data/ext/bash/app.sh +75 -0
  18. data/ext/bash/db/pg.sh +47 -0
  19. data/ext/bash/db/redis.sh +20 -0
  20. data/ext/bash/lib/extras.sh +11 -0
  21. data/ext/bash/lib/nginx.sh +233 -0
  22. data/ext/bash/lib/node.sh +28 -0
  23. data/ext/bash/lib/pg.sh +44 -0
  24. data/ext/bash/lib/propro.sh +104 -0
  25. data/ext/bash/lib/redis.sh +59 -0
  26. data/ext/bash/lib/rvm.sh +21 -0
  27. data/ext/bash/lib/system.sh +57 -0
  28. data/ext/bash/lib/ubuntu.sh +175 -0
  29. data/ext/bash/vagrant/nginx.sh +31 -0
  30. data/ext/bash/vagrant/node.sh +5 -0
  31. data/ext/bash/vagrant/pg.sh +12 -0
  32. data/ext/bash/vagrant/redis.sh +5 -0
  33. data/ext/bash/vagrant/rvm.sh +6 -0
  34. data/ext/bash/vagrant/system.sh +26 -0
  35. data/ext/bash/vagrant.sh +3 -0
  36. data/ext/bash/vps/system.sh +156 -0
  37. data/lib/propro/cli/templates/init.tt +21 -0
  38. data/lib/propro/cli.rb +125 -0
  39. data/lib/propro/command.rb +17 -0
  40. data/lib/propro/export.rb +119 -0
  41. data/lib/propro/option.rb +36 -0
  42. data/lib/propro/package.rb +68 -0
  43. data/lib/propro/script.rb +95 -0
  44. data/lib/propro/source.rb +86 -0
  45. data/lib/propro/version.rb +3 -0
  46. data/lib/propro.rb +57 -0
  47. data/propro.gemspec +27 -0
  48. data/test/export_spec.rb +88 -0
  49. data/test/minitest_helper.rb +6 -0
  50. data/test/option_spec.rb +34 -0
  51. metadata +167 -0
data/lib/propro/cli.rb ADDED
@@ -0,0 +1,125 @@
1
+ require 'thor'
2
+
3
+ module Propro
4
+ class CLI < Thor
5
+ include Thor::Actions
6
+
7
+ INIT_TEMPLATES = {
8
+ db: {
9
+ paths: %w[ vps db ],
10
+ desc: 'backend database server'
11
+ },
12
+ app: {
13
+ paths: %w[ vps app ],
14
+ desc: 'frontend application server'
15
+ },
16
+ web: {
17
+ paths: %w[ vps app db ],
18
+ desc: 'standalone web server'
19
+ },
20
+ vagrant: {
21
+ paths: %w[ vagrant ],
22
+ desc: 'standalone Vagrant development VM'
23
+ }
24
+ }
25
+
26
+ def self.source_root
27
+ File.join(File.dirname(__FILE__), 'cli/templates')
28
+ end
29
+
30
+ desc 'init NAME', 'Creates a Propro script with the file name NAME'
31
+ option :template, aliases: '-t', enum: %w[ db app web vagrant ], default: 'web'
32
+ def init(outname = nil)
33
+ key = options[:template].to_sym
34
+ outfile = absolute_path(outname || "#{key}.propro")
35
+ type = INIT_TEMPLATES[key]
36
+ @paths = type[:paths]
37
+ @desc = type[:desc]
38
+ @sources = Package.sources_for_paths('lib', *@paths)
39
+ template 'init.tt', outfile
40
+ end
41
+
42
+ desc 'build INPUT', 'Takes a Propro script INPUT and generates a Bash provisioner OUTPUT'
43
+ option :output, aliases: '-o', banner: '<output file name>'
44
+ def build(input)
45
+ infile = absolute_path(input)
46
+ script = Script.load(input).to_bash
47
+ if (output = options[:output])
48
+ File.write(absolute_path(output), script)
49
+ else
50
+ STDOUT << script
51
+ end
52
+ end
53
+
54
+ desc 'deploy SCRIPT', 'Builds a Propro script and then executes it remotely'
55
+ option :server, aliases: '-s', banner: '<server address>'
56
+ option :password, aliases: '-p', banner: '<server password>'
57
+ option :user, aliases: '-u', banner: '<server user>', default: 'root'
58
+ def deploy(script_path)
59
+ require 'net/ssh'
60
+ require 'net/scp'
61
+ require 'io/console'
62
+
63
+ puts Propro.color_banner
64
+ puts
65
+
66
+ script = Script.load(script_path)
67
+ address = (options[:server] || script.get_server)
68
+ password = (options[:password] || script.get_password || ask_password)
69
+ user = (options[:user] || script.get_user)
70
+ remote_home = (user == 'root' ? '/root' : "/home/#{user}")
71
+ remote_log_path = "#{remote_home}/provision.log"
72
+ remote_script_path = "#{remote_home}/provision.sh"
73
+ remote_script_url = address + remote_script_path
74
+
75
+ say_event 'build', script_path
76
+ script_data = StringIO.new(script.to_bash)
77
+
78
+ raise ArgumentError, 'no server address has been provided' if !address
79
+ raise ArgumentError, 'no server password has been provided' if !password
80
+
81
+ say_event 'connect', "#{user}@#{address}"
82
+ Net::SSH.start(address, user, password: password) do |session|
83
+ say_event 'upload', "#{script_path} -> #{remote_script_url}"
84
+ session.scp.upload!(script_data, remote_script_path)
85
+ session.exec!("chmod +x #{remote_script_path}")
86
+ session.exec!("touch #{remote_log_path}")
87
+ tail = session.exec("tail -f #{remote_log_path}") do |ch|
88
+ ch.on_data do |ch, data|
89
+ STDOUT.write(data)
90
+ STDOUT.flush
91
+ end
92
+ end
93
+
94
+ sleep 1 # ughhhhhh
95
+ say_event 'run', remote_script_url
96
+ puts
97
+ session.exec(remote_script_path)
98
+ end
99
+ rescue IOError # uggghhhhhhhhhh
100
+ say_event 'done', "#{address} is rebooting"
101
+ end
102
+
103
+ private
104
+
105
+ def say_event(event, msg)
106
+ pad = (7 - event.length)
107
+ label = "#{event.upcase}" + (" " * pad)
108
+ puts "\e[36m\e[1m#{label}\e[0m #{msg}"
109
+ end
110
+
111
+ def ask_password
112
+ STDIN.noecho do
113
+ ask 'password:'
114
+ end
115
+ end
116
+
117
+ def absolute_path(path)
118
+ if path[0] == '/'
119
+ path
120
+ else
121
+ File.join(Dir.pwd, path)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,17 @@
1
+ module Propro
2
+ class Command
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ @name = name.to_s
7
+ end
8
+
9
+ def function_name
10
+ @function_name ||= "provision-#{name.gsub(/\/|\_/, '-')}"
11
+ end
12
+
13
+ def to_bash
14
+ function_name
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,119 @@
1
+ module Propro
2
+ class Export
3
+ TAG_SPECIFY = '@specify'
4
+ TAG_REQUIRE = '@require'
5
+ EXPORT_BEGIN = 'export '
6
+ COMMENT_RANGE = /#(.*)\Z/
7
+ DQUO = '"'
8
+ SQUO = '\''
9
+ EQ = '='
10
+ ZERO_STRING = ''
11
+ INTEGER_RE = /\A\-{0,1}[0-9]+\Z/
12
+ DECIMAL_RE = /\A\-{0,1}[0-9]+\.[0-9]+\Z/
13
+ SPACE_RE = / /
14
+ YES = 'yes'
15
+ NO = 'no'
16
+
17
+ def self.parse(line)
18
+ is_literal = false
19
+ is_specified = false
20
+ is_required = false
21
+ comment = nil
22
+ line = line.sub(EXPORT_BEGIN, ZERO_STRING)
23
+ name, value = line.split(EQ, 2)
24
+
25
+ if value =~ COMMENT_RANGE
26
+ metacomment = $1
27
+ is_specified = true if metacomment.sub!(TAG_SPECIFY, ZERO_STRING)
28
+ is_required = true if metacomment.sub!(TAG_REQUIRE, ZERO_STRING)
29
+ metacomment.strip!
30
+
31
+ if metacomment != ZERO_STRING
32
+ comment = metacomment
33
+ end
34
+ end
35
+
36
+ value.sub!(COMMENT_RANGE, ZERO_STRING)
37
+ value.strip!
38
+
39
+ case value[0]
40
+ when DQUO
41
+ value[0] = ZERO_STRING
42
+ value[-1] = ZERO_STRING
43
+ when SQUO
44
+ is_literal = true
45
+ value[0] = ZERO_STRING
46
+ value[-1] = ZERO_STRING
47
+ end
48
+
49
+ new name,
50
+ default: value,
51
+ is_literal: is_literal,
52
+ is_specified: is_specified,
53
+ is_required: is_required,
54
+ comment: comment
55
+ end
56
+
57
+ def initialize(name, opts = {})
58
+ @name = name.to_s.upcase
59
+ @default = opts[:default]
60
+ @is_literal = opts[:is_literal]
61
+ @is_specified = opts[:is_specified]
62
+ @is_required = opts[:is_required]
63
+ @comment = opts[:comment]
64
+ end
65
+
66
+ def key
67
+ @key ||= @name.downcase.to_sym
68
+ end
69
+
70
+ def to_ruby
71
+ args = []
72
+ args << key.inspect
73
+ args << default.inspect
74
+ args << "lit: true" if @is_literal
75
+ if @comment
76
+ "set #{args.join(', ')} # #{@comment}"
77
+ else
78
+ "set #{args.join(', ')}"
79
+ end
80
+ end
81
+
82
+ def default
83
+ cast(@default)
84
+ end
85
+
86
+ def is_literal?
87
+ @is_literal
88
+ end
89
+
90
+ def is_specified?
91
+ @is_specified
92
+ end
93
+
94
+ def is_required?
95
+ @is_required
96
+ end
97
+
98
+ protected
99
+
100
+ def cast(val)
101
+ case val
102
+ when INTEGER_RE
103
+ val.to_i
104
+ when DECIMAL_RE
105
+ val.to_f
106
+ when ZERO_STRING
107
+ nil
108
+ when SPACE_RE
109
+ val.split(' ')
110
+ when YES
111
+ true
112
+ when NO
113
+ false
114
+ else
115
+ val
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,36 @@
1
+ module Propro
2
+ class Option
3
+ attr_reader :name
4
+
5
+ def initialize(key, value, opts = {})
6
+ @key = key.to_s.downcase.to_sym
7
+ @value = value
8
+ @is_literal = opts[:lit] ? true : false
9
+ end
10
+
11
+ def name
12
+ @name ||= @key.to_s.upcase
13
+ end
14
+
15
+ def value=(val)
16
+ @value = val
17
+ end
18
+
19
+ def value
20
+ case @value
21
+ when Array
22
+ %{"#{@value.join(' ')}"}
23
+ when true
24
+ %{"yes"}
25
+ when false
26
+ %{"no"}
27
+ else
28
+ @is_literal ? %{'#{@value}'} : %{"#{@value}"}
29
+ end
30
+ end
31
+
32
+ def to_bash
33
+ "#{name}=#{value}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ module Propro
2
+ module Package
3
+ EXTRACT_NAME_RE = %r{/ext/bash/([a-z0-9_\-/]+)\.sh}
4
+ SORTED_NAMES = %w[
5
+ lib/propro
6
+ lib/ubuntu
7
+ lib/system
8
+ lib/pg
9
+ lib/rvm
10
+ lib/nginx
11
+ lib/node
12
+ lib/redis
13
+ vps/system
14
+ app
15
+ app/rvm
16
+ app/pg
17
+ app/nginx
18
+ app/sidekiq
19
+ app/puma
20
+ app/puma/nginx
21
+ app/node
22
+ db/pg
23
+ db/redis
24
+ vagrant
25
+ vagrant/system
26
+ vagrant/pg
27
+ vagrant/redis
28
+ vagrant/rvm
29
+ vagrant/node
30
+ vagrant/nginx
31
+ lib/extras
32
+ ]
33
+
34
+ module_function
35
+
36
+ def root
37
+ File.join(Propro.root, 'ext/bash')
38
+ end
39
+
40
+ def source_files
41
+ @source_files ||= Dir[File.join(root, '**/*.sh')]
42
+ end
43
+
44
+ def sources
45
+ @sources ||= begin
46
+ names = SORTED_NAMES.dup
47
+ source_files.each do |file|
48
+ name = EXTRACT_NAME_RE.match(file)[1]
49
+ names.push(name) unless names.include?(name)
50
+ end
51
+ names.map { |name| Source.new(name) }
52
+ end
53
+ end
54
+
55
+ def sources_for_path(path)
56
+ resort! sources.select { |source| /\A#{path}/ =~ source.name }
57
+ end
58
+
59
+ def sources_for_paths(*paths)
60
+ resort! paths.flatten.map { |path| sources_for_path(path) }.flatten
61
+ end
62
+
63
+ def resort!(ary)
64
+ ary.sort_by! { |source| SORTED_NAMES.index(source.name) }
65
+ ary
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,95 @@
1
+ module Propro
2
+ class Script
3
+ def self.load(file)
4
+ script = new
5
+ script.load_file(file)
6
+ script
7
+ end
8
+
9
+ def initialize
10
+ @sources = []
11
+ @options = []
12
+ @commands = []
13
+ @server = nil
14
+ @password = nil
15
+ source :lib
16
+ end
17
+
18
+ def load_file(file)
19
+ @file = file
20
+ @file_name = File.basename(@file)
21
+ instance_eval(File.read(file))
22
+ end
23
+
24
+ def server(host, opts = {})
25
+ @server = host
26
+ @password = opts[:password]
27
+ @user = opts[:user] || 'root'
28
+ end
29
+
30
+ def get_server
31
+ @server
32
+ end
33
+
34
+ def get_password
35
+ @password
36
+ end
37
+
38
+ def get_user
39
+ @user
40
+ end
41
+
42
+ def source(src)
43
+ @sources.concat(Package.sources_for_path(src))
44
+ end
45
+
46
+ def set(key, value)
47
+ @options << Option.new(key, value)
48
+ end
49
+
50
+ def provision(*commands)
51
+ @commands.concat(commands.flatten.map { |c| Command.new(c) })
52
+ end
53
+
54
+ def to_bash
55
+ <<-SH
56
+ #!/usr/bin/env bash
57
+ #{Propro.comment_banner}
58
+ #
59
+ # Built from: #{@file_name}
60
+
61
+ unset UCF_FORCE_CONFFOLD
62
+ export UCF_FORCE_CONFFNEW="YES"
63
+ export DEBIAN_FRONTEND="noninteractive"
64
+
65
+ #{sources_bash}
66
+
67
+ # Options from: #{@file_name}
68
+ #{options_bash}
69
+
70
+ function main {
71
+ #{commands_bash}
72
+ finished
73
+ reboot-system
74
+ }
75
+
76
+ main
77
+
78
+ SH
79
+ end
80
+
81
+ private
82
+
83
+ def options_bash
84
+ @options.map(&:to_bash).join("\n").strip
85
+ end
86
+
87
+ def sources_bash
88
+ @sources.map(&:to_bash).join("\n").strip
89
+ end
90
+
91
+ def commands_bash
92
+ @commands.map(&:to_bash).join("\n ")
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,86 @@
1
+ module Propro
2
+ class Source
3
+ attr_reader :name, :provisioner
4
+
5
+ EXPORT_BEGIN = 'export '
6
+ COMMENT_BEGIN = '#'
7
+ IS_LIBRARY_BEGIN = 'lib/'
8
+ FUNC_PROVISION_BEGIN = 'function provision-'
9
+ FUNC_PROVISION_NAME_RANGE = /\Afunction provision\-([a-z\-]+)/
10
+
11
+ def initialize(name)
12
+ @name = name.to_s
13
+ @exports = []
14
+ @can_provision = false
15
+ @is_library = name.start_with?(IS_LIBRARY_BEGIN)
16
+ @src = ''
17
+ load
18
+ end
19
+
20
+ def file_name
21
+ "#{@name}.sh"
22
+ end
23
+
24
+ def file_path
25
+ File.join(Propro::Package.root, file_name)
26
+ end
27
+
28
+ def load
29
+ File.open(file_path) do |file|
30
+ file.each_line { |line| load_line(line) }
31
+ end
32
+ end
33
+
34
+ def can_provision?
35
+ @can_provision
36
+ end
37
+
38
+ def is_library?
39
+ @is_library
40
+ end
41
+
42
+ def specified_exports
43
+ @specified_exports ||= begin
44
+ exports.select { |e| e.is_required? || e.is_specified? }
45
+ end
46
+ end
47
+
48
+ def exports
49
+ @exports.sort { |a, b|
50
+ case
51
+ when b.is_required? then 1
52
+ when b.is_specified? then 0
53
+ else -1
54
+ end
55
+ }
56
+ end
57
+
58
+ def to_bash
59
+ <<-SH
60
+ # Propro package: #{file_name}
61
+ #{@src}
62
+ SH
63
+ end
64
+
65
+ protected
66
+
67
+ def load_line(line)
68
+ case
69
+ when line.start_with?(COMMENT_BEGIN)
70
+ # skip comments
71
+ when line.start_with?(EXPORT_BEGIN)
72
+ # collect exported variables from bash modules
73
+ @exports << Export.parse(line)
74
+ @src << line.sub(EXPORT_BEGIN, '')
75
+ when line.start_with?(FUNC_PROVISION_BEGIN)
76
+ @can_provision = true
77
+ path = line.match(FUNC_PROVISION_NAME_RANGE)[1]
78
+ @provisioner = path.gsub('-', '/')
79
+ @src << line
80
+ else
81
+ # pass-through
82
+ @src << line
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ module Propro
2
+ VERSION = '0.1.0'
3
+ end
data/lib/propro.rb ADDED
@@ -0,0 +1,57 @@
1
+ require 'propro/version'
2
+ require 'propro/package'
3
+ require 'propro/export'
4
+ require 'propro/source'
5
+ require 'propro/script'
6
+ require 'propro/command'
7
+ require 'propro/option'
8
+
9
+ module Propro
10
+ class Error < StandardError; end
11
+
12
+ BANNER = <<'DOC'.chomp
13
+ ____ _________ ____ _________
14
+ / __ \/ ___/ __ \/ __ \/ ___/ __ \
15
+ / /_/ / / / /_/ / /_/ / / / /_/ /
16
+ / .___/_/ \____/ .___/_/ \____/
17
+ /_/ /_/
18
+ DOC
19
+
20
+ module_function
21
+
22
+ def banner
23
+ BANNER
24
+ end
25
+
26
+ # <3 <3 <3 Lifted from Minitest::Pride <3 <3 <3
27
+ # https://github.com/seattlerb/minitest/blob/master/lib/minitest/pride_plugin.rb
28
+ def color_banner
29
+ @color_banner ||= begin
30
+ if /^xterm|-256color$/ =~ ENV['TERM']
31
+ pi3 = Math::PI / 3
32
+ colors = (0...(6 * 7)).map { |n|
33
+ n *= 1.0 / 6
34
+ r = (3 * Math.sin(n ) + 3).to_i
35
+ g = (3 * Math.sin(n + 2 * pi3) + 3).to_i
36
+ b = (3 * Math.sin(n + 4 * pi3) + 3).to_i
37
+ 36 * r + 6 * g + b + 16
38
+ }
39
+ banner.each_line.map { |line|
40
+ line.each_char.with_index.map { |chr, i|
41
+ "\e[38;5;#{colors[i]}m#{chr}\e[0m"
42
+ }.join
43
+ }.join
44
+ else
45
+ "\e[2m#{banner}\e[0m"
46
+ end
47
+ end
48
+ end
49
+
50
+ def comment_banner
51
+ @comment_banner ||= banner.each_line.map { |l| '# ' + l }.join
52
+ end
53
+
54
+ def root
55
+ File.expand_path(File.join(File.dirname(__FILE__), '..'))
56
+ end
57
+ end
data/propro.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'propro/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'propro'
8
+ spec.version = Propro::VERSION
9
+ spec.authors = ['Carsten Nielsen']
10
+ spec.email = ['heycarsten@gmail.com']
11
+ spec.summary = 'A standalone server provisioning tool'
12
+ spec.description = 'Propro is a tool for provisioning remote servers.'
13
+ spec.homepage = 'http://github.com/heycarsten/propro'
14
+ spec.license = 'GNU v2'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^test\//)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'thor', '~> 0.18'
22
+ spec.add_dependency 'net-scp', '~> 1.1'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.5'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'minitest'
27
+ end