nodectl 0.2.4

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