nodectl 0.2.4

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 (111) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +4 -0
  6. data/README.md +3 -0
  7. data/Rakefile +1 -0
  8. data/TODO.md +7 -0
  9. data/assets/javascript/application.coffee +22 -0
  10. data/assets/javascript/bootstrap.js +2006 -0
  11. data/assets/javascript/controllers/application_controller.coffee +73 -0
  12. data/assets/javascript/controllers/service_controller.coffee +62 -0
  13. data/assets/javascript/event_handler.coffee +51 -0
  14. data/assets/javascript/models/action.coffee +28 -0
  15. data/assets/javascript/models/instance.coffee +7 -0
  16. data/assets/javascript/models/service.coffee +75 -0
  17. data/assets/javascript/models/version.coffee +3 -0
  18. data/assets/javascript/notifier.coffee +64 -0
  19. data/assets/javascript/router.coffee +3 -0
  20. data/assets/javascript/routes/application.coffee +6 -0
  21. data/assets/javascript/routes/log.coffee +4 -0
  22. data/assets/javascript/routes/service.coffee +3 -0
  23. data/assets/javascript/store.coffee +32 -0
  24. data/assets/javascript/templates/application.hbs +82 -0
  25. data/assets/javascript/templates/index.hbs +1 -0
  26. data/assets/javascript/templates/log.hbs +1 -0
  27. data/assets/javascript/templates/service.hbs +110 -0
  28. data/assets/javascript/templates/views/action.hbs +20 -0
  29. data/assets/javascript/templates/views/actions_list.hbs +26 -0
  30. data/assets/javascript/templates/views/environments_list.hbs +9 -0
  31. data/assets/javascript/templates/views/install_button.hbs +1 -0
  32. data/assets/javascript/templates/views/mass_run.hbs +30 -0
  33. data/assets/javascript/templates/views/notifier_control.hbs +7 -0
  34. data/assets/javascript/templates/views/param.hbs +6 -0
  35. data/assets/javascript/templates/views/terminal.hbs +8 -0
  36. data/assets/javascript/ui.coffee +24 -0
  37. data/assets/javascript/vendor/ansiparse.js +186 -0
  38. data/assets/javascript/vendor/audio.min.js +24 -0
  39. data/assets/javascript/vendor/audiojs.swf +0 -0
  40. data/assets/javascript/vendor/ember-data.js +10528 -0
  41. data/assets/javascript/vendor/ember.js +40583 -0
  42. data/assets/javascript/vendor/handlebars.js +2746 -0
  43. data/assets/javascript/vendor/jquery.js +8829 -0
  44. data/assets/javascript/vendor/ladda.js +157 -0
  45. data/assets/javascript/vendor/moment.min.js +6 -0
  46. data/assets/javascript/vendor/notify-osd.js +319 -0
  47. data/assets/javascript/vendor/player-graphics.gif +0 -0
  48. data/assets/javascript/vendor/spin.js +218 -0
  49. data/assets/javascript/views/action_view.coffee +18 -0
  50. data/assets/javascript/views/actions_list_view.coffee +2 -0
  51. data/assets/javascript/views/environments_list_view.coffee +2 -0
  52. data/assets/javascript/views/install_button_view.coffee +34 -0
  53. data/assets/javascript/views/mass_run_view.coffee +107 -0
  54. data/assets/javascript/views/notifier_control_view.coffee +29 -0
  55. data/assets/javascript/views/terminal_view.coffee +56 -0
  56. data/assets/stylesheets/application.css +137 -0
  57. data/assets/stylesheets/bootstrap-theme.css +347 -0
  58. data/assets/stylesheets/bootstrap.css +5785 -0
  59. data/assets/stylesheets/fonts/glyphicons-halflings-regular.eot +0 -0
  60. data/assets/stylesheets/fonts/glyphicons-halflings-regular.svg +229 -0
  61. data/assets/stylesheets/fonts/glyphicons-halflings-regular.ttf +0 -0
  62. data/assets/stylesheets/fonts/glyphicons-halflings-regular.woff +0 -0
  63. data/assets/stylesheets/ladda-themeless.css +330 -0
  64. data/assets/stylesheets/ladda.css +392 -0
  65. data/assets/stylesheets/notify-osd.css +49 -0
  66. data/assets/stylesheets/terminal.css +12 -0
  67. data/bin/nodectl +4 -0
  68. data/lib/nodectl.rb +137 -0
  69. data/lib/nodectl/action.rb +80 -0
  70. data/lib/nodectl/binding.rb +13 -0
  71. data/lib/nodectl/cli.rb +123 -0
  72. data/lib/nodectl/context.rb +34 -0
  73. data/lib/nodectl/database.rb +55 -0
  74. data/lib/nodectl/generators/init.rb +22 -0
  75. data/lib/nodectl/generators/templates/init/.gitignore +5 -0
  76. data/lib/nodectl/generators/templates/init/config/manifest.yml +1 -0
  77. data/lib/nodectl/generators/templates/init/config/server.yml +7 -0
  78. data/lib/nodectl/instance.rb +100 -0
  79. data/lib/nodectl/log.rb +13 -0
  80. data/lib/nodectl/manager.rb +71 -0
  81. data/lib/nodectl/multi_io.rb +16 -0
  82. data/lib/nodectl/options.rb +47 -0
  83. data/lib/nodectl/process.rb +145 -0
  84. data/lib/nodectl/promised_file.rb +37 -0
  85. data/lib/nodectl/recipe.rb +197 -0
  86. data/lib/nodectl/repository.rb +80 -0
  87. data/lib/nodectl/server.rb +103 -0
  88. data/lib/nodectl/service.rb +126 -0
  89. data/lib/nodectl/stream/buffer.rb +44 -0
  90. data/lib/nodectl/stream/events_session.rb +39 -0
  91. data/lib/nodectl/stream/file.rb +69 -0
  92. data/lib/nodectl/stream/file_session.rb +35 -0
  93. data/lib/nodectl/stream/file_with_memory.rb +17 -0
  94. data/lib/nodectl/stream/websocket.rb +90 -0
  95. data/lib/nodectl/version.rb +3 -0
  96. data/lib/nodectl/watchdog.rb +17 -0
  97. data/lib/nodectl/webapp/actions.rb +21 -0
  98. data/lib/nodectl/webapp/base.rb +29 -0
  99. data/lib/nodectl/webapp/instances.rb +46 -0
  100. data/lib/nodectl/webapp/public/sounds/alert.mp3 +0 -0
  101. data/lib/nodectl/webapp/public/sounds/error.mp3 +0 -0
  102. data/lib/nodectl/webapp/public/sounds/question.mp3 +0 -0
  103. data/lib/nodectl/webapp/services.rb +39 -0
  104. data/lib/nodectl/webapp/settings.rb +7 -0
  105. data/lib/nodectl/webapp/ui.rb +19 -0
  106. data/lib/nodectl/webapp/versions.rb +12 -0
  107. data/lib/nodectl/webapp/views/ui.haml +20 -0
  108. data/nodectl.gemspec +35 -0
  109. data/spec/spec_helper.rb +12 -0
  110. data/spec/stream/buffer_spec.rb +33 -0
  111. metadata +365 -0
@@ -0,0 +1,22 @@
1
+ class Nodectl::Generators::Init < Thor::Group
2
+ include Thor::Actions
3
+
4
+ argument :host_path
5
+
6
+ def self.source_root
7
+ File.join(File.dirname(__FILE__), "templates", "init")
8
+ end
9
+
10
+ def self.destination_root
11
+ host_path
12
+ end
13
+
14
+ def copy_template
15
+ directory "./", host_path
16
+ end
17
+
18
+ def database
19
+ db = Nodectl::Database.new(File.join(host_path, "nodectl.db"))
20
+ db.reset
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ src/
2
+ log/
3
+ tmp/
4
+ nodectl.db.dir
5
+ nodectl.db.pag
@@ -0,0 +1 @@
1
+ # Write service definitions here
@@ -0,0 +1,7 @@
1
+ http_host:
2
+ http_port: 8080
3
+ websocket_port: 8090
4
+ websocket_host: 0.0.0.0
5
+
6
+ ui_default_env: development
7
+ ui_disabled: false
@@ -0,0 +1,100 @@
1
+ class Nodectl::Instance < Nodectl::Process
2
+ @manager = Nodectl::Manager.new(self)
3
+
4
+ attr_reader :service
5
+ attr_reader :options
6
+ attr_reader :command
7
+
8
+ def initialize(service, command, options = {}, &blk)
9
+ super(options) do
10
+ blk.call if blk
11
+ Process.exec command
12
+ end
13
+
14
+ @service = service
15
+ @options = options
16
+ @command = command
17
+
18
+ if @options[:started]
19
+ @status = :running
20
+ @pid = @options[:pid]
21
+ @detach = Process.detach(@pid)
22
+
23
+ Nodectl::Instance[@pid] = self
24
+ Nodectl.logger.info("#{@service.name} [#{@pid}] found")
25
+ end
26
+
27
+ onkill do
28
+ self.class.delete(pid, reason: @status)
29
+ end
30
+ end
31
+
32
+ def run
33
+ super
34
+
35
+ Nodectl::Instance[@pid] = self
36
+ Nodectl.logger.info("#{@service.name} [#{@pid}] started")
37
+ end
38
+
39
+ def as_json
40
+ {
41
+ "id" => pid,
42
+ "service_id" => service.name,
43
+ "status" => status,
44
+ "options" => options
45
+ }
46
+ end
47
+
48
+ def dump
49
+ {
50
+ "service_name" => service.name,
51
+ "pid" => pid,
52
+ "options" => options,
53
+ "command" => command
54
+ }
55
+ end
56
+
57
+ def pre_fork_block
58
+ Nodectl.shut_up!
59
+ end
60
+
61
+ class << self
62
+ def load(dump)
63
+ service = Nodectl::Service.find(dump["service_name"])
64
+
65
+ if service
66
+ puts "Service found: #{dump['service_name']}"
67
+ new(service, dump["command"], dump["options"].merge(started: true, pid: dump["pid"]))
68
+ else
69
+ puts "Service not found"
70
+ raise "service not found"
71
+ end
72
+ end
73
+
74
+ def push(pid, instance)
75
+ super(pid, instance)
76
+ save_register
77
+ end
78
+
79
+ alias :[]= :push
80
+
81
+ def delete(pid, options = {})
82
+ super(pid, options)
83
+ save_register
84
+ end
85
+
86
+ def save_register
87
+ Nodectl.database["instances"] = all.map(&:dump)
88
+ end
89
+
90
+ def load_register
91
+ database = Nodectl.database["instances"] || []
92
+
93
+ database.each do |dump|
94
+ load(dump) rescue RuntimeError
95
+ end
96
+
97
+ save_register
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,13 @@
1
+ class Nodectl::Log
2
+ attr_reader :name
3
+ attr_reader :path
4
+
5
+ def initialize(name, path)
6
+ @name = name
7
+ @path = path
8
+ end
9
+
10
+ def instance_path(service)
11
+ service.source_path.join(path)
12
+ end
13
+ end
@@ -0,0 +1,71 @@
1
+ class Nodectl::Manager
2
+ MANAGER_METHODS = [:all, :find, :find!, :push, :delete, :[], :[]=, :next_id, :onadd, :ondelete]
3
+
4
+ def initialize(target_class)
5
+ @objects = {}
6
+ @target_class = target_class
7
+
8
+ @onadd = []
9
+ @ondelete = []
10
+
11
+
12
+ MANAGER_METHODS.each do |method|
13
+ @target_class.define_singleton_method method do |*args, &blk|
14
+ @manager.send(method, *args, &blk)
15
+ end
16
+ end
17
+ end
18
+
19
+ def find(key)
20
+ @objects[key]
21
+ end
22
+
23
+ alias :[] :find
24
+
25
+ def find!(key)
26
+ object = find(key)
27
+
28
+ unless object
29
+ raise Nodectl::NotFound, "#{@target_class.name} with key '#{key}' not found"
30
+ end
31
+
32
+ object
33
+ end
34
+
35
+ def all
36
+ @objects.values
37
+ end
38
+
39
+ def push(key, object)
40
+ @objects[key] = object
41
+ onadd_call(object)
42
+ end
43
+
44
+ alias :[]= :push
45
+
46
+ def delete(key, options = {})
47
+ object = @objects.delete(key)
48
+ ondelete_call(object, options)
49
+ end
50
+
51
+ def next_id
52
+ @next_id ||= 0
53
+ @next_id += 1
54
+ end
55
+
56
+ def onadd(&blk)
57
+ @onadd << blk
58
+ end
59
+
60
+ def ondelete(&blk)
61
+ @ondelete << blk
62
+ end
63
+
64
+ def onadd_call(object)
65
+ @onadd.each { |b| b.call(object) }
66
+ end
67
+
68
+ def ondelete_call(object, options)
69
+ @ondelete.each { |b| b.call(object, options) }
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ # Redirect `write` calls to multiple targets, used with standard ruby
2
+ # logger instance for writing messages to log file and stdout at the
3
+ # same time
4
+ class Nodectl::MultiIO
5
+ def initialize(*targets)
6
+ @targets = targets
7
+ end
8
+
9
+ def write(*args)
10
+ @targets.each {|t| t.write(*args)}
11
+ end
12
+
13
+ def close
14
+ @targets.each(&:close)
15
+ end
16
+ end
@@ -0,0 +1,47 @@
1
+ # Boot options container, keep info about node directory structure etc
2
+ class Nodectl::Options
3
+ def initialize(overrides = {})
4
+ @options = default_options.merge(overrides)
5
+ @options = calculate_options(@options)
6
+ end
7
+
8
+ def [](option_name)
9
+ @options[option_name.to_s]
10
+ end
11
+
12
+ def inspect
13
+ @options.inspect
14
+ end
15
+
16
+ def to_s
17
+ @options.to_s
18
+ end
19
+
20
+ def to_hash
21
+ @options
22
+ end
23
+
24
+ private
25
+
26
+ def default_options
27
+ {
28
+ "root" => Dir.pwd,
29
+ "log_level" => "INFO"
30
+ }
31
+ end
32
+
33
+ def calculate_options(source_options)
34
+ options = source_options.dup
35
+
36
+ options["root"] = Pathname.new(options["root"])
37
+
38
+ options["config_dir"] ||= options["root"].join("config")
39
+ options["source_dir"] ||= options["root"].join("src")
40
+ options["logger_dir"] ||= options["root"].join("log")
41
+ options["recipe_dir"] ||= options["root"].join("recipes")
42
+ options["database"] ||= options["root"].join("nodectl.db")
43
+ options["manifest"] ||= options["config_dir"].join("manifest.yml")
44
+
45
+ options
46
+ end
47
+ end
@@ -0,0 +1,145 @@
1
+ class Nodectl::Process
2
+ ENV_BLACKLIST = %w(BUNDLE_BIN_PATH BUNDLE_GEMFILE USE_BUNDLER _ORIGINAL_GEM_PATH _ RUBYOPT)
3
+ TIMEOUT = 10
4
+
5
+ @manager = Nodectl::Manager.new(self)
6
+
7
+ attr_reader :pid
8
+ attr_reader :status
9
+ attr_reader :exit_status
10
+
11
+ def initialize(options = {}, &blk)
12
+ raise "cannot create process without block" unless block_given?
13
+
14
+ @status = :pending
15
+ @stopping = false
16
+ @options = options
17
+ @blk = blk
18
+ @oncrash = []
19
+ @onstop = []
20
+ @onkill = []
21
+ end
22
+
23
+ def run
24
+ if @status != :pending
25
+ raise "Cannot run one process twice"
26
+ end
27
+
28
+ @pid = fork do
29
+ ENV_BLACKLIST.each do |name|
30
+ ENV.delete(name)
31
+ end
32
+
33
+ args = @options[:args] || []
34
+ @blk.call(*args)
35
+
36
+ EM.stop
37
+ end
38
+
39
+ @detach = Process.detach(@pid)
40
+
41
+ @status = :running
42
+ Nodectl::Process[pid] = self
43
+ end
44
+
45
+ def alive?
46
+ Process.getpgid(pid)
47
+ true
48
+ rescue Errno::ESRCH
49
+ dead!
50
+ false
51
+ end
52
+
53
+ def stop(options = {})
54
+ @status = :stopping
55
+ signal = options[:force] ? "KILL" : "TERM"
56
+
57
+ Nodectl.logger.info("process: send #{signal} to #{pid}")
58
+
59
+ Process.kill(signal, pid)
60
+ rescue Errno::ESRCH, Errno::EPERM
61
+ dead!
62
+ end
63
+
64
+ def restart(options = {})
65
+ stop(options)
66
+ TIMEOUT.times do
67
+ unless alive?
68
+ @status = :pending
69
+ run
70
+ break
71
+ end
72
+
73
+ sleep 1
74
+ end
75
+ end
76
+
77
+ def dead!(exit_status = nil)
78
+ @detach.join
79
+
80
+ if @detach.value
81
+ @exit_status = @detach.value.exitstatus
82
+ end
83
+
84
+ if [:running, :stopping].include?(@status)
85
+ case @status
86
+ when :stopping
87
+ @status = :stopped
88
+ Nodectl.logger.info("process: [#{pid}] stopped")
89
+ onstop_call
90
+ when :running
91
+ @status = :crashed
92
+ Nodectl.logger.info("process: [#{pid}] crashed")
93
+ oncrash_call
94
+ end
95
+
96
+ onkill_call
97
+ Nodectl::Process.delete(pid)
98
+ end
99
+ end
100
+
101
+ # When process crashed
102
+ def oncrash(&blk)
103
+ @oncrash << blk
104
+ end
105
+
106
+ # When process gracefully stoped
107
+ def onstop(&blk)
108
+ @onstop << blk
109
+ end
110
+
111
+ # When process is dead, regardless of reason
112
+ def onkill(&blk)
113
+ @onkill << blk
114
+ end
115
+
116
+ private
117
+
118
+ def oncrash_call
119
+ @oncrash.each { |b| b.call(self) }
120
+ end
121
+
122
+ def onkill_call
123
+ @onkill.each { |b| b.call(self) }
124
+ end
125
+
126
+ def onstop_call
127
+ @onstop.each { |b| b.call(self) }
128
+ end
129
+
130
+ def pre_fork_block
131
+ # Override it to customize process behaviour
132
+ end
133
+
134
+ def post_fork_block
135
+ # Override it to customize process behaviour
136
+ end
137
+
138
+ def fork(&blk)
139
+ Process.fork do
140
+ pre_fork_block
141
+ blk.call
142
+ post_fork_block
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,37 @@
1
+ # Promised file is file which looks like an empty file while it does not exists
2
+ class Nodectl::PromisedFile
3
+ def initialize(path, file_class, options = {})
4
+ @path = path
5
+ @file_class = file_class
6
+ @options = options
7
+ @fake_file = Nodectl::Stream::File.new(StringIO.new)
8
+ end
9
+
10
+ def close
11
+ @closed = true
12
+ current_file.close
13
+ end
14
+
15
+ # Hook Nodectl::Stream::File#onread for call it when real
16
+ # file was created
17
+ def onread(&blk)
18
+ @onread = blk
19
+ end
20
+
21
+ def method_missing(name, *args, &blk)
22
+ current_file.public_send(name, *args, &blk)
23
+ end
24
+
25
+ private
26
+
27
+ def current_file
28
+ if !@real_file && !@closed && File.exist?(@path)
29
+ @real_file = @file_class.new(@path, @options)
30
+ @real_file.onread(&@onread) if @onread
31
+ end
32
+
33
+ @real_file || @fake_file
34
+ end
35
+
36
+
37
+ end