propro 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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