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,44 @@
1
+ class Nodectl::Stream::Buffer
2
+ def initialize
3
+ @buffer = ""
4
+ @valid = ""
5
+ end
6
+
7
+ def <<(string)
8
+ string = @buffer + string
9
+ @buffer = ""
10
+ @buffer.force_encoding(Encoding::ASCII_8BIT)
11
+
12
+ string.force_encoding(Encoding::UTF_8)
13
+
14
+ 16.times do
15
+ if string.valid_encoding?
16
+ break
17
+ end
18
+
19
+ string, byte = byte_rotate(string)
20
+ @buffer << byte
21
+ end
22
+
23
+ if string.valid_encoding?
24
+ @valid << string
25
+ else
26
+ Nodectl.logger.error("buffer: cannot convert string to valid UTF-8, ignore it")
27
+ end
28
+ end
29
+
30
+ def read
31
+ ret = @valid
32
+ @valid = ""
33
+ ret
34
+ end
35
+
36
+ private
37
+
38
+ def byte_rotate(string)
39
+ byte = string.byteslice(-1)
40
+ string = string.byteslice(0...-1)
41
+ byte.force_encoding(Encoding::ASCII_8BIT)
42
+ [string, byte]
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ class Nodectl::Stream::EventsSession
2
+ @manager = Nodectl::Manager.new(self)
3
+
4
+ attr_reader :websocket
5
+ attr_reader :file
6
+
7
+ def initialize(websocket, handshake)
8
+ @websocket = websocket
9
+ @handshake = handshake
10
+
11
+ self.class[self.class.next_id] = self
12
+ end
13
+
14
+ def publish(resource, id, event_name, payload = {})
15
+ message = MultiJson.dump({resource: resource,
16
+ id: id,
17
+ event_name: event_name,
18
+ payload: payload})
19
+
20
+ @websocket.send(message)
21
+
22
+ Nodectl.logger.debug("events_session: sent 'message'")
23
+ end
24
+
25
+ def close
26
+ self.class.delete(self)
27
+ @websocket.close unless @websocket.closed?
28
+ end
29
+
30
+ def onclose(&block)
31
+ @websocket.onclose(&block)
32
+ end
33
+
34
+ class << self
35
+ def publish(resource, id, event_name, payload = {})
36
+ all.each { |session| session.publish(resource, id, event_name, payload) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,69 @@
1
+ class Nodectl::Stream::File
2
+ def initialize(file, options = {})
3
+ @onread = []
4
+ @onclose = []
5
+
6
+ if file.respond_to? :read_nonblock
7
+ @file = file
8
+ else
9
+ begin
10
+ @file = File.new(file)
11
+ rescue Errno::ENOENT
12
+ raise Nodectl::NotFound, "file not found"
13
+ end
14
+ end
15
+
16
+ @options = options
17
+
18
+ if @options[:end_offset]
19
+ offset = @file.size - @options[:end_offset]
20
+ if offset > 0
21
+ @file.seek(offset)
22
+ @options[:end_offset].times do
23
+ break if @file.readchar == "\n"
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def onread(&blk)
30
+ @onread << blk
31
+ end
32
+
33
+ def onclose(&blk)
34
+ @onclose << blk
35
+ end
36
+
37
+ def read_chunk
38
+ chunk = @file.read_nonblock(4096)
39
+
40
+ if chunk.size > 0
41
+ onread_call(chunk)
42
+ end
43
+ rescue Errno::EAGAIN, EOFError
44
+ # Nothing to do
45
+ end
46
+
47
+ def close
48
+ onclose_call
49
+ @file.close
50
+ end
51
+
52
+ def closed?
53
+ @file.closed?
54
+ end
55
+
56
+ private
57
+
58
+ def file
59
+ @file
60
+ end
61
+
62
+ def onread_call(chunk)
63
+ @onread.each { |blk| blk.call(chunk) }
64
+ end
65
+
66
+ def onclose_call
67
+ @onclose.each { |blk| blk.call }
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ class Nodectl::Stream::FileSession
2
+ @manager = Nodectl::Manager.new(self)
3
+
4
+ attr_reader :websocket
5
+ attr_reader :file
6
+
7
+ def initialize(websocket, handshake, file)
8
+ @websocket = websocket
9
+ @handshake = handshake
10
+ @file = file
11
+ @buffer = Nodectl::Stream::Buffer.new
12
+
13
+ @file.onread do |chunk|
14
+ @buffer << chunk
15
+ publish(@buffer.read)
16
+ end
17
+
18
+ self.class[self.class.next_id] = self
19
+ end
20
+
21
+ def publish(message)
22
+ @websocket.send(message)
23
+ Nodectl.logger.debug("file_session: sent #{message.bytesize} bytes")
24
+ end
25
+
26
+ def close
27
+ self.class.delete(self)
28
+ @file.close
29
+ @websocket.close unless @websocket.closed?
30
+ end
31
+
32
+ def onclose(&block)
33
+ @websocket.onclose(&block)
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ class Nodectl::Stream::FileWithMemory < Nodectl::Stream::File
2
+ def initialize(file, options = {})
3
+ super(file, options)
4
+
5
+ @memory = ""
6
+ end
7
+
8
+ def onread(&blk)
9
+ blk.call(@memory)
10
+ @onread << blk
11
+ end
12
+
13
+ def onread_call(chunk)
14
+ @memory << chunk
15
+ super(chunk)
16
+ end
17
+ end
@@ -0,0 +1,90 @@
1
+ class Nodectl::Stream::Websocket
2
+ URI_LOG = /\A\/logs\/(?<service_name>[\w_-]+)\/(?<log_name>\w+)\Z/
3
+ URI_ACTION = /\A\/actions\/(?<action_id>\w+)\Z/
4
+ URI_EVENTS = /\A\/events\Z/
5
+
6
+ def initialize(host, port)
7
+ EM::WebSocket.run(host: host, port: port) do |ws|
8
+ Nodectl.logger.info("websocket: socket created")
9
+ ws.onopen { |handshake| connection_open(ws, handshake) }
10
+ end
11
+
12
+ callbacks_init
13
+ end
14
+
15
+ def connection_open(ws, handshake)
16
+ Nodectl.logger.info("websocket: connection opened with url '#{handshake.path}'")
17
+
18
+ case handshake.path
19
+ when URI_LOG
20
+ create_log_session(ws, handshake)
21
+ when URI_ACTION
22
+ create_action_session(ws, handshake)
23
+ when URI_EVENTS
24
+ create_events_session(ws, handshake)
25
+ else
26
+ ws.close
27
+ end
28
+ end
29
+
30
+ def callbacks_init
31
+ EM.add_periodic_timer(0.5, method(:tick))
32
+
33
+ Nodectl::Instance.onadd do |instance|
34
+ Nodectl::Stream::EventsSession.publish('instance', instance.pid, 'started')
35
+ end
36
+
37
+ Nodectl::Instance.ondelete do |instance, options|
38
+ Nodectl::Stream::EventsSession.publish('instance', instance.pid, 'stopped', options)
39
+ end
40
+
41
+ Nodectl::Action.onadd do |action|
42
+ Nodectl::Stream::EventsSession.publish('action', action.id, 'started')
43
+ end
44
+
45
+ Nodectl::Action.onkill do |action, options|
46
+ Nodectl::Stream::EventsSession.publish('action', action.id, 'stopped', options)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def create_log_session(ws, handshake)
53
+ match = URI_LOG.match(handshake.path)
54
+ service = Nodectl::Service.find!(match[:service_name])
55
+ logfile = service.recipe_binding.get_logfile(match[:log_name])
56
+ file = Nodectl::PromisedFile.new(logfile, Nodectl::Stream::File, end_offset: 5000, infinity: true)
57
+ session = Nodectl::Stream::FileSession.new(ws, handshake, file)
58
+
59
+ Nodectl.logger.info("websocket: log session open succeed: #{logfile}")
60
+ rescue StandardError => e
61
+ Nodectl.logger.info("websocket: log session open failed: #{e.message}")
62
+ ws.close
63
+ end
64
+
65
+ def create_action_session(ws, handshake)
66
+ match = URI_ACTION.match(handshake.path)
67
+ action = Nodectl::Action.find!(match[:action_id].to_i)
68
+ session = Nodectl::Stream::FileSession.new(ws, handshake, action.output)
69
+
70
+ Nodectl.logger.info("websocket: action session open succeed: action ##{action.id}")
71
+ rescue StandardError => e
72
+ Nodectl.logger.info("websocket: action session open failed: #{e.message}")
73
+ ws.close
74
+ end
75
+
76
+ def create_events_session(ws, handshake)
77
+ session = Nodectl::Stream::EventsSession.new(ws, handshake)
78
+
79
+ Nodectl.logger.info("websocket: events session open succeed")
80
+ rescue StandardError => e
81
+ Nodectl.logger.info("websocket: events session open failed: #{e.message}")
82
+ ws.close
83
+ end
84
+
85
+ def tick
86
+ Nodectl::Stream::FileSession.all.each do |session|
87
+ session.file.read_chunk
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module Nodectl
2
+ VERSION = "0.2.4"
3
+ end
@@ -0,0 +1,17 @@
1
+ class Nodectl::Watchdog
2
+ TICK_INTERVAL = 2
3
+
4
+ def initialize
5
+ @timer = EM.add_periodic_timer(TICK_INTERVAL, method(:tick))
6
+ end
7
+
8
+ def tick
9
+ Nodectl::Instance.all.each do |instance|
10
+ instance.alive?
11
+ end
12
+
13
+ Nodectl::Process.all.each do |process|
14
+ process.alive?
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ class Nodectl::Webapp::Actions < Nodectl::Webapp::Base
2
+ get '/' do
3
+ json actions: Nodectl::Action.all.map(&:as_json)
4
+ end
5
+
6
+ get '/:id' do
7
+ json action: Nodectl::Action.find!(params[:id]).as_json
8
+ end
9
+
10
+ post '/' do
11
+ service = Nodectl::Service.find(params[:action][:service_id])
12
+
13
+ if service
14
+ action = service.recipe_binding.run_action(params[:action][:name].to_sym, params[:action][:params] || {})
15
+ json action: action.as_json
16
+ else
17
+ json errors: {service_id: "service not found"}
18
+ status 422
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ class Nodectl::Webapp::Base < Sinatra::Base
2
+ helpers Sinatra::JSON
3
+
4
+ before do
5
+ params.merge! json_body_params
6
+ end
7
+
8
+ helpers do
9
+ def json_body_params
10
+ @json_body_params ||= begin
11
+ MultiJson.load(request.body.read.to_s, symbolize_keys: true)
12
+ rescue MultiJson::LoadError
13
+ {}
14
+ end
15
+ end
16
+ end
17
+
18
+ configure do
19
+ set :threaded, false
20
+ set :show_exceptions, :after_handler
21
+ enable :logging
22
+ end
23
+
24
+ error StandardError do
25
+ exception = env['sinatra.error']
26
+ backtrace = [exception.inspect] + exception.backtrace
27
+ Nodectl.logger.error('webapp error - ' + backtrace.join("\n from"))
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ class Nodectl::Webapp::Instances < Nodectl::Webapp::Base
2
+ get '/' do
3
+ if params[:ids]
4
+ select_id = params[:ids].map(&:to_i)
5
+ instances = Nodectl::Instance.all.select { |i| select_id.include?(i.pid) }
6
+ else
7
+ instances = Nodectl::Instance.all
8
+ end
9
+
10
+ json instances: instances.map(&:as_json)
11
+ end
12
+
13
+ get '/:pid' do
14
+ instance = Nodectl::Instance.find!(params[:pid].to_i)
15
+ json instance: instance.as_json
16
+ end
17
+
18
+ post '/' do
19
+ service = Nodectl::Service.find(params[:instance][:service_id])
20
+
21
+ if service
22
+ instance = service.recipe_binding.run_slot :start, params[:instance][:options]
23
+ status 201
24
+ json instance: instance.as_json
25
+ else
26
+ json errors: {service_name: "service not found"}
27
+ status 422
28
+ end
29
+ end
30
+
31
+ put '/:pid' do
32
+ instance = Nodectl::Instance.find!(params[:pid].to_i)
33
+
34
+ if params[:instance][:status] == "stopping"
35
+ if params[:instance][:force_stop]
36
+ instance.stop(force: true)
37
+ else
38
+ instance.stop
39
+ end
40
+ elsif params[:instance][:status] == "restarting"
41
+ instance.restart(force: true)
42
+ end
43
+
44
+ json instance: instance.as_json
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ class Nodectl::Webapp::Services < Nodectl::Webapp::Base
2
+ get '/' do
3
+ json services: Nodectl::Service.all.map(&:as_json)
4
+ end
5
+
6
+ get '/:name' do
7
+ service = Nodectl::Service.find!(params[:name])
8
+ json service: service.as_json
9
+ end
10
+
11
+ put '/:name' do
12
+ service = Nodectl::Service.find!(params[:name])
13
+
14
+ if params[:service][:slot]
15
+ service.recipe_binding.run_slot params[:service][:slot].to_sym, params[:service][:slot_params]
16
+ end
17
+
18
+ binding = service.recipe_binding
19
+
20
+ if !params[:service][:version_id].nil? &&
21
+ binding.supported_slots.include?(:version_set) &&
22
+ binding.supported_slots.include?(:version)
23
+ current = binding.run_slot(:version)["id"]
24
+ target = params[:service][:version_id]
25
+
26
+ if current != target
27
+ binding.run_slot(:version_set, {id: target})
28
+ end
29
+ end
30
+
31
+ json service: service.as_json
32
+ end
33
+
34
+ private
35
+
36
+ def service_params
37
+ params[:service] || {}
38
+ end
39
+ end