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,197 @@
1
+ # TODO: param validation
2
+ class Nodectl::Recipe
3
+ @manager = Nodectl::Manager.new(self)
4
+
5
+ SLOT_NAMES = [:install, :uninstall, :start, :version, :version_list, :version_set]
6
+
7
+ UnsupportedAction = Class.new(StandardError)
8
+ UnsupportedSlot = Class.new(StandardError)
9
+
10
+ LogNotFound = Class.new(Nodectl::NotFound)
11
+
12
+ attr_reader :name
13
+
14
+ def initialize(name, options = {})
15
+ @name = name
16
+ @actions = options[:actions] || {}
17
+ @slots = options[:slots] || {}
18
+ @triggers = options[:triggers] || {}
19
+ @events = options[:events] || {}
20
+ @logs = options[:logs] || {}
21
+ @params = options[:params] || {}
22
+
23
+ self.class[@name.to_s] = self
24
+ end
25
+
26
+ def bind(service)
27
+ Nodectl::Binding.new(service, self)
28
+ end
29
+
30
+ def run_slot(service, slot_name, run_params = nil)
31
+ run_params ||= {}
32
+
33
+ unless @slots.key?(slot_name)
34
+ raise UnsupportedSlot, "slot '#{slot_name}' in not supported by '#{name}' recipe"
35
+ end
36
+
37
+ if slot_name == :install
38
+ install_deps(service)
39
+ end
40
+
41
+ run_trigger(service, slot_name, :pre)
42
+
43
+ @params.each do |key, value|
44
+ if !run_params.key?(key) && value.key?(:default)
45
+ run_params[key] = value[:default]
46
+ end
47
+ end
48
+
49
+ run_params.keys.each do |key|
50
+ run_params.delete(key) unless @params.key?(key)
51
+ end
52
+
53
+ result = context(service).instance_exec run_params, &@slots[slot_name]
54
+ run_trigger(service, slot_name, :post)
55
+ result
56
+ end
57
+
58
+ def run_action(service, action_name, run_params = {})
59
+ unless @actions.key?(action_name)
60
+ raise UnsupportedAction, "action '#{action_name}' in not supported by '#{name}' recipe"
61
+ end
62
+
63
+ @params.each do |key, value|
64
+ if !run_params.key?(key) && value.key?(:default)
65
+ run_params[key] = value[:default]
66
+ end
67
+ end
68
+
69
+ Nodectl::Action.run(service, action_name) do
70
+ run_trigger(service, action_name, :pre)
71
+ result = context(service).instance_exec run_params, &@actions[action_name]
72
+ run_trigger(service, action_name, :post)
73
+ end
74
+ end
75
+
76
+ def run_trigger(service, action_name, trigger_type)
77
+ unless [:pre, :post].include? trigger_type
78
+ raise "incorrect trigger type: expected 'post' or 'pre'"
79
+ end
80
+
81
+ trigger = @triggers[:"#{trigger_type}_#{action_name}"]
82
+ context(service).instance_eval &trigger if trigger
83
+ end
84
+
85
+ def get_logfile(service, log_name)
86
+ log = @logs.fetch(log_name)
87
+ log.instance_path(service)
88
+ rescue KeyError
89
+ raise LogNotFound, "log '#{log_name}' for recipe '#{name}' not found (service '#{service.name}')"
90
+ end
91
+
92
+ def logs
93
+ @logs.keys
94
+ end
95
+
96
+ def actions
97
+ @actions.keys
98
+ end
99
+
100
+ def triggers
101
+ @triggers.keys
102
+ end
103
+
104
+ def supported_actions(service)
105
+ @actions.keys
106
+ end
107
+
108
+ def supported_slots(service)
109
+ @slots.keys
110
+ end
111
+
112
+ def params
113
+ @params
114
+ end
115
+
116
+ private
117
+
118
+ def context(service)
119
+ Nodectl::Context.new(self, service)
120
+ end
121
+
122
+ def install_deps(service)
123
+ service.deps.each do |service|
124
+ if service.status != :installed
125
+ service.recipe_binding.run_slot :install
126
+ end
127
+ end
128
+ end
129
+
130
+ class DSL
131
+ def initialize(name, &blk)
132
+ raise "Cannot create recipe without block" unless block_given?
133
+
134
+ @name = name
135
+
136
+ @actions = {}
137
+ @triggers = {}
138
+ @events = {}
139
+ @logs = {}
140
+ @params = {}
141
+ @slots = {}
142
+
143
+ instance_eval &blk
144
+ end
145
+
146
+ def create
147
+ Nodectl::Recipe.new(@name, actions: @actions,
148
+ triggers: @triggers,
149
+ events: @events,
150
+ logs: @logs,
151
+ params: @params,
152
+ slots: @slots)
153
+ end
154
+
155
+ def action(name, &blk)
156
+ raise "Cannot create action without block" unless block_given?
157
+ @actions[name] = blk
158
+ end
159
+
160
+ def slot(name, &blk)
161
+ raise "Cannot create slots without block" unless block_given?
162
+ raise "Unsupported slot name #{name}" unless Nodectl::Recipe::SLOT_NAMES.include?(name)
163
+ @slots[name] = blk
164
+ end
165
+
166
+ def event(name, &blk)
167
+ raise "Cannot create event without block" unless block_given?
168
+ @events[name] = blk
169
+ end
170
+
171
+ def trigger(name, &blk)
172
+ raise "Cannot create trigger without block" unless block_given?
173
+ @triggers[name] = blk
174
+ end
175
+
176
+ def param(name, options = {})
177
+ @params[name] = options
178
+ end
179
+
180
+ def log(name, path)
181
+ @logs[name.to_s] = Nodectl::Log.new(name, path)
182
+ end
183
+ end
184
+
185
+ # Extension for ruby Object class for creating recipes anywhere
186
+ module ObjectMethods
187
+ def declare_recipe(*args, &blk)
188
+ Nodectl::Recipe.declare(*args, &blk)
189
+ end
190
+ end
191
+
192
+ class << self
193
+ def declare(*args, &blk)
194
+ DSL.new(*args, &blk).create
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,80 @@
1
+ class Nodectl::Repository
2
+ VCSError = Class.new(StandardError)
3
+
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def current_version
9
+ ret, out = shell_git("log -1 --pretty=format:%H\\ %ci\\ %s")
10
+ parse_version(out)
11
+ end
12
+
13
+ def switch_version(version)
14
+ shell_git("checkout -f #{version}")
15
+ end
16
+
17
+ def versions
18
+ fetch
19
+
20
+ ret, out = shell_git("log --all --pretty=format:%H\\ %ci\\ %s")
21
+ result = []
22
+
23
+ out.each_line do |line|
24
+ result << parse_version(line)
25
+ end
26
+
27
+ result
28
+ end
29
+
30
+ def fetch
31
+ shell_git("fetch")
32
+ end
33
+
34
+ def pull
35
+ shell_git("checkout master")
36
+ shell_git("pull")
37
+ end
38
+
39
+ private
40
+
41
+ def shell_git(command, options = {})
42
+ source_path = options[:source_path] || @path
43
+ gitdir_path = options[:gitdir_path] || File.join(source_path, ".git")
44
+
45
+ self.class.shell_run("git --work-tree=#{source_path} --git-dir=#{gitdir_path} #{command} 2>&1", options)
46
+ end
47
+
48
+ def parse_version(line)
49
+ {
50
+ "id" => line[0...40],
51
+ "timestamp" => line[41...66],
52
+ "message" => line[67..-1]
53
+ }
54
+ end
55
+
56
+ class << self
57
+ def clone(from, to)
58
+ FileUtils.mkdir_p(to)
59
+ ret, out = shell_run("git clone #{from} #{to}")
60
+ new(to)
61
+ end
62
+
63
+ def shell_run(command, options = {})
64
+ command = "#{command} 2>&1"
65
+ Nodectl.logger.info "repository: run command '#{command}'"
66
+
67
+ process = IO.popen(command)
68
+
69
+ out = process.read
70
+ process.close
71
+ ret = $?
72
+
73
+ if ret != 0 && !options[:ignore_status]
74
+ raise VCSError, "exit status: #{ret.exitstatus}"
75
+ end
76
+
77
+ [ret, out]
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,103 @@
1
+ class Nodectl::Server
2
+ attr_reader :options
3
+
4
+ def initialize(options = {})
5
+ @options = self.class.default_options.merge(options)
6
+ end
7
+
8
+ def run
9
+ if @options["daemonize"]
10
+ if Process.fork
11
+ exit
12
+ else
13
+ Nodectl.shut_up!
14
+ end
15
+ end
16
+
17
+ if @options["pidfile"]
18
+ File.open(@options["pidfile"], "w") do |file|
19
+ file.write(Process.pid)
20
+ end
21
+
22
+ at_exit { File.unlink @options["pidfile"] }
23
+ end
24
+
25
+ Nodectl.logger.info "server with pid #{Process.pid} started"
26
+ at_exit { Nodectl.logger.info "server with pid #{Process.pid} shutting down" }
27
+
28
+ Nodectl::Service.load_register
29
+ Nodectl::Instance.load_register
30
+
31
+ EM.run do
32
+ setup_signals
33
+ rack_run
34
+ watchdog_run
35
+ stream_run
36
+ end
37
+ end
38
+
39
+ def stop
40
+ @thin.stop if @thin
41
+ EM.stop
42
+ end
43
+
44
+ def stop!
45
+ @thin.stop! if @thin
46
+ EM.stop
47
+ end
48
+
49
+ def self.default_options
50
+ @default_options ||= YAML.load_file(Nodectl.options[:config_dir].join('server.yml'))
51
+ end
52
+
53
+ private
54
+
55
+ def rack_run
56
+ options = @options
57
+
58
+ @thin = Thin::Server.new(@options["http_host"], @options["http_port"], signals: false) do
59
+ map('/instances') { run Nodectl::Webapp::Instances.new }
60
+ map('/services') { run Nodectl::Webapp::Services.new }
61
+ map('/actions') { run Nodectl::Webapp::Actions.new }
62
+ map('/versions') { run Nodectl::Webapp::Versions.new }
63
+ map('/settings') { run Nodectl::Webapp::Settings.new }
64
+
65
+ unless options["ui_disabled"]
66
+ map('/') { run Nodectl::Webapp::UI.new }
67
+ end
68
+ end
69
+
70
+ @thin.start!
71
+ end
72
+
73
+ def watchdog_run
74
+ @watchdog = Nodectl::Watchdog.new
75
+ end
76
+
77
+ def stream_run
78
+ @websocket = Nodectl::Stream::Websocket.new(@options["websocket_host"],
79
+ @options["websocket_port"])
80
+ end
81
+
82
+ def setup_signals
83
+ @signal_queue ||= []
84
+
85
+ %w( INT TERM ).each do |signal|
86
+ trap(signal) { @signal_queue << signal }
87
+ end
88
+
89
+ @signal_timer ||= EM.add_periodic_timer(1) { handle_signals }
90
+ end
91
+
92
+ def handle_signals
93
+ case @signal_queue.shift
94
+ when 'INT'
95
+ stop!
96
+ when 'TERM', 'QUIT'
97
+ stop
98
+ end
99
+
100
+ EM.next_tick { handle_signals } unless @signal_queue.empty?
101
+ end
102
+
103
+ end
@@ -0,0 +1,126 @@
1
+ class Nodectl::Service
2
+ @manager = Nodectl::Manager.new(self)
3
+
4
+ attr_reader :manifest
5
+
6
+ def initialize(manifest)
7
+ @manifest = manifest
8
+
9
+ Nodectl::Service[name] = self
10
+ end
11
+
12
+ def name; @manifest["name"]; end
13
+ def path; @manifest["path"]; end
14
+ def repo; @manifest["repo"]; end
15
+
16
+ def deps
17
+ (@manifest["deps"] || []).map do |service_name|
18
+ self.class.find!(service_name)
19
+ end
20
+ end
21
+
22
+ def recipe_name
23
+ @manifest["recipe"]
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class} name:#{name}>"
28
+ end
29
+
30
+ def source_path
31
+ Nodectl.options[:source_dir].join(path)
32
+ end
33
+
34
+ def exist?
35
+ # TODO: think of a better criterion for existing installation
36
+ source_path.exist?
37
+ end
38
+
39
+ def status
40
+ exist? ? :installed : :available
41
+ end
42
+
43
+ def recipe
44
+ @recipe ||= Nodectl::Recipe.find(recipe_name)
45
+ end
46
+
47
+ def recipe_binding
48
+ recipe.bind(self) if recipe
49
+ end
50
+
51
+ def instances
52
+ Nodectl::Instance.all.select { |i| i.service == self }
53
+ end
54
+
55
+ def supported_actions
56
+ binding = recipe_binding
57
+ if binding
58
+ binding.supported_actions
59
+ else
60
+ []
61
+ end
62
+ end
63
+
64
+ def supported_slots
65
+ binding = recipe_binding
66
+ if binding
67
+ binding.supported_slots
68
+ else
69
+ []
70
+ end
71
+ end
72
+
73
+ # TODO: Refactor this method (and may be whole log-related classes):
74
+ # service class should not know about log internals and streaming
75
+ # urls
76
+ def logs
77
+ unless @logs
78
+ if recipe
79
+ binding = recipe_binding
80
+ @logs = recipe.logs.map do |log|
81
+ {
82
+ "id" => "#{name}-#{log}",
83
+ "name" => log,
84
+ "path" => "/logs/#{name}/#{log}",
85
+ }
86
+ end
87
+ else
88
+ @logs = []
89
+ end
90
+ end
91
+
92
+ @logs
93
+ end
94
+
95
+ def as_json
96
+ json = {
97
+ "id" => name,
98
+ "status" => status,
99
+ "instance_ids" => instances.map(&:pid),
100
+ "recipe_name" => recipe_name,
101
+ "supported_actions" => supported_actions,
102
+ "supported_slots" => supported_slots,
103
+ "logs" => logs,
104
+ "params" => recipe.params
105
+ }
106
+
107
+ binding = recipe_binding
108
+
109
+ if status == :installed && binding.supported_slots.include?(:version)
110
+ version = binding.run_slot(:version)
111
+ json["version_id"] = version["id"]
112
+ json["version_timestamp"] = version["timestamp"]
113
+ json["version_message"] = version["message"]
114
+ end
115
+
116
+ json
117
+ end
118
+
119
+ class << self
120
+ def load_register
121
+ Nodectl.manifest["services"].each do |service_manifest|
122
+ new(service_manifest)
123
+ end
124
+ end
125
+ end
126
+ end