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,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