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,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "nodectl"
4
+ Nodectl::CLI::start(ARGV)
@@ -0,0 +1,137 @@
1
+ require "nodectl/version"
2
+
3
+ require "thor"
4
+ require "thin"
5
+ require "eventmachine"
6
+ require "em-websocket"
7
+ require 'sinatra/base'
8
+ require 'sinatra/json'
9
+ require "logger"
10
+ require 'stringio'
11
+ require "yaml"
12
+ require "time"
13
+ require "dbm"
14
+ require 'multi_json'
15
+
16
+ module Nodectl
17
+ autoload :Action, "nodectl/action.rb"
18
+ autoload :Binding, "nodectl/binding.rb"
19
+ autoload :CLI, "nodectl/cli.rb"
20
+ autoload :Context, "nodectl/context.rb"
21
+ autoload :Database, "nodectl/database.rb"
22
+ autoload :Log, "nodectl/log.rb"
23
+ autoload :Manager, "nodectl/manager.rb"
24
+ autoload :MultiIO, "nodectl/multi_io.rb"
25
+ autoload :Instance, "nodectl/instance.rb"
26
+ autoload :Options, "nodectl/options.rb"
27
+ autoload :PromisedFile, "nodectl/promised_file.rb"
28
+ autoload :Repository, "nodectl/repository.rb"
29
+ autoload :Recipe, "nodectl/recipe.rb"
30
+ autoload :Process, "nodectl/process.rb"
31
+ autoload :Server, "nodectl/server.rb"
32
+ autoload :Service, "nodectl/service.rb"
33
+ autoload :Watchdog, "nodectl/watchdog.rb"
34
+
35
+ module Generators
36
+ autoload :Init, "nodectl/generators/init.rb"
37
+ end
38
+
39
+ module Webapp
40
+ autoload :Actions, "nodectl/webapp/actions.rb"
41
+ autoload :Base, "nodectl/webapp/base.rb"
42
+ autoload :Instances, "nodectl/webapp/instances.rb"
43
+ autoload :Services, "nodectl/webapp/services.rb"
44
+ autoload :Settings, "nodectl/webapp/settings.rb"
45
+ autoload :UI, "nodectl/webapp/ui.rb"
46
+ autoload :Versions, "nodectl/webapp/versions.rb"
47
+ end
48
+
49
+ module Stream
50
+ autoload :Buffer, "nodectl/stream/buffer.rb"
51
+ autoload :EventsSession, "nodectl/stream/events_session.rb"
52
+ autoload :File, "nodectl/stream/file.rb"
53
+ autoload :FileSession, "nodectl/stream/file_session.rb"
54
+ autoload :FileWithMemory, "nodectl/stream/file_with_memory.rb"
55
+ autoload :Websocket, "nodectl/stream/websocket.rb"
56
+ end
57
+
58
+ NotFound = Class.new(Sinatra::NotFound)
59
+
60
+ class << self
61
+ attr_reader :options
62
+
63
+ # Node initialization: here default global options could be overriden
64
+ #
65
+ # @param options [Hash] overrides for default options
66
+ def boot(options = {})
67
+ @options = Options.new(options)
68
+
69
+ # Routing targets
70
+ Dir["#{@options[:recipe_dir]}/**/*.rb"].each do |path|
71
+ require path
72
+ end
73
+ end
74
+
75
+ # Key-value store for node
76
+ #
77
+ # @return [Nodectl::Database]
78
+ def database
79
+ @database ||= Nodectl::Database.new(options[:database])
80
+ end
81
+
82
+ def server
83
+ @server ||= Nodectl::Server.new(options)
84
+ end
85
+
86
+ # Get node manifest: list of hashes with service definitions. It looks like:
87
+ #
88
+ # [
89
+ # {
90
+ # "name" => "hub",
91
+ # "repo" => "git://example.com:service.git",
92
+ # "path" => "hub"
93
+ # },
94
+ # ...
95
+ # ]
96
+ #
97
+ # @return [Array] node manifest
98
+ def manifest
99
+ @manifest ||= YAML.load_file(@options[:manifest]) || []
100
+ rescue SystemCallError => e
101
+ $stderr.puts "Manifest loading error: #{e.message}"
102
+ abort
103
+ end
104
+
105
+ # Get logger instance
106
+ #
107
+ # @return [Logger] logger instance
108
+ def logger
109
+ unless defined? @logger
110
+ file = File.open(options[:logger_dir].join("nodectl.log"), "a")
111
+ file.sync = true
112
+ io = MultiIO.new(STDOUT, file)
113
+
114
+ @logger ||= Logger.new(io)
115
+ @logger.level = Logger.const_get(options[:log_level])
116
+ end
117
+
118
+ @logger
119
+ end
120
+
121
+ # Redirect all standard streams to null device
122
+ def shut_up!
123
+ $stdout.reopen(File.open(File::NULL, "w"))
124
+ $stderr.reopen(File.open(File::NULL, "w"))
125
+ $stdin.reopen(File.open(File::NULL, "r"))
126
+ end
127
+
128
+ # Base path of assets folder
129
+ #
130
+ # @return [String]
131
+ def assets_path
132
+ File.absolute_path(File.join(__FILE__, "../../assets"))
133
+ end
134
+ end
135
+ end
136
+
137
+ include Nodectl::Recipe::ObjectMethods
@@ -0,0 +1,80 @@
1
+ class Nodectl::Action < Nodectl::Process
2
+ @manager = Nodectl::Manager.new(self)
3
+
4
+ attr_reader :id
5
+ attr_reader :service
6
+ attr_reader :name
7
+ attr_reader :output
8
+
9
+ def initialize(service, name, options = {}, &blk)
10
+ super(options, &blk)
11
+
12
+ @id = Nodectl::Action.next_id
13
+ @service = service
14
+ @name = name
15
+
16
+ onkill do
17
+ result = if @exit_status == 0
18
+ "succeed"
19
+ else
20
+ "failed"
21
+ end
22
+
23
+ self.class.onkill_call(self, reason: result)
24
+ end
25
+ end
26
+
27
+ def run
28
+ rout, @wout = IO.pipe
29
+
30
+ @output = Nodectl::Stream::FileWithMemory.new(rout)
31
+
32
+ super
33
+
34
+ @started_at = Time.now
35
+
36
+ Nodectl::Action[@id] = self
37
+ Nodectl.logger.info("action: [#{pid}] started: #{service.name}##{name}")
38
+ end
39
+
40
+ def as_json
41
+ {
42
+ "id" => @id,
43
+ "pid" => @pid,
44
+ "name" => @name,
45
+ "service_id" => @service.name,
46
+ "ws" => "/actions/#{id}",
47
+ "exit_status" => @exit_status,
48
+ "started_at" => @started_at.iso8601
49
+ }
50
+ end
51
+
52
+ private
53
+
54
+ def pre_fork_block
55
+ $stdout.reopen(@wout)
56
+ $stderr.reopen(@wout)
57
+
58
+ $stdout.sync = true
59
+ $stderr.sync = true
60
+
61
+ @wout.close
62
+ end
63
+
64
+ class << self
65
+ def onkill(&blk)
66
+ @onkill ||= []
67
+ @onkill << blk
68
+ end
69
+
70
+ def onkill_call(action, options = {})
71
+ @onkill.each { |b| b.call(action, options) }
72
+ end
73
+
74
+ def run(service, name, *args, &blk)
75
+ action = new(service, name, *args, &blk)
76
+ action.run
77
+ action
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ class Nodectl::Binding
2
+ attr_reader :service
3
+ attr_reader :recipe
4
+
5
+ def initialize(service, recipe)
6
+ @service = service
7
+ @recipe = recipe
8
+ end
9
+
10
+ def method_missing(name, *args, &blk)
11
+ @recipe.public_send(name, @service, *args, &blk)
12
+ end
13
+ end
@@ -0,0 +1,123 @@
1
+ class Nodectl::CLI < Thor
2
+ class_option :root, desc: "Root directory path"
3
+ class_option :config_dir, desc: "Config directory path"
4
+ class_option :source_dir, desc: "Source directory path"
5
+ class_option :logger_dir, desc: "Logger directory path"
6
+ class_option :manifest, desc: "Manifest file path"
7
+ class_option :database, desc: "Nodectl database path"
8
+ class_option :log_level, desc: "Log level", enum: ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]
9
+
10
+ desc "init [HOST_PATH]", "Initialize new node"
11
+ def init(host_path = "./")
12
+ Nodectl::Generators::Init.start [host_path]
13
+ end
14
+
15
+ desc "recipes", "Show available recipes list"
16
+ def recipes
17
+ boot
18
+
19
+ Nodectl::Recipe.all.each do |recipe|
20
+ puts recipe.name
21
+ end
22
+ end
23
+
24
+ desc "recipe RECIPE_NAME", "Show recipe info"
25
+ def recipe(recipe_name)
26
+ boot
27
+
28
+ recipe = Nodectl::Recipe.find!(recipe_name)
29
+
30
+ print "Logs: "
31
+ puts recipe.logs.map(&:to_s).join(", ")
32
+
33
+ print "Actions: "
34
+ puts recipe.actions.map(&:to_s).join(", ")
35
+
36
+ print "Triggers: "
37
+ puts recipe.triggers.map(&:to_s).join(", ")
38
+
39
+ rescue Nodectl::NotFound
40
+ $stderr.puts "recipe '#{recipe_name}' not found"
41
+ abort
42
+ end
43
+
44
+ desc "services", "Show services list"
45
+ def services
46
+ boot
47
+
48
+ Nodectl::Service.load_register
49
+
50
+ Nodectl::Service.all.each do |service|
51
+ puts "#{service.name} #{service.status}"
52
+ end
53
+ end
54
+
55
+ desc "install SERVICE_NAME", "Install specified service"
56
+ def install(service_name)
57
+ boot
58
+
59
+ service = find_service!(service_name)
60
+ recipe = find_recipe!(service)
61
+
62
+ recipe.run_action :install
63
+ end
64
+
65
+ option :ui_disabled, desc: "Disable UI and only API", type: :boolean
66
+ option :daemonize, desc: "Daemonize server", type: :boolean, default: false
67
+ option :pidfile, desc: "File to save server PID"
68
+
69
+ option :http_host, desc: "Host to bind webserver"
70
+ option :http_port, desc: "Port to bind webserver"
71
+
72
+ option :websocket_host, desc: "Host to bind websocket server"
73
+ option :websocket_port, desc: "Port to bind websocket server"
74
+
75
+ # AMPQ support is not implemented yet
76
+ #
77
+ # option :amqp_host, desc: "Host of AMQP broker"
78
+ # option :amqp_port, desc: "Port of AMQP broker"
79
+ # option :amqp_username, desc: "AMQP username"
80
+ # option :amqp_password, desc: "AMQP password"
81
+ # option :amqp_vhost, desc: "AMQP vhost"
82
+
83
+ desc 'server', 'Run node'
84
+ def server
85
+ boot
86
+
87
+ Nodectl::server.run
88
+ end
89
+
90
+ desc 'version', 'Show nodectl version'
91
+ def version
92
+ puts "Nodectl #{Nodectl::VERSION}"
93
+ end
94
+
95
+ private
96
+
97
+ def boot
98
+ Nodectl.boot(options)
99
+ end
100
+
101
+ def find_service!(service_name)
102
+ service = Nodectl::Service.find(service_name)
103
+
104
+ unless service
105
+ $stderr.puts "service '#{service_name}' not found"
106
+ abort
107
+ end
108
+
109
+ service
110
+ end
111
+
112
+ def find_recipe!(service)
113
+ recipe = service.recipe_binding
114
+
115
+ unless recipe
116
+ $stderr.puts "recipe '#{service.recipe_name}' for service '#{service.name}' not found"
117
+ abort
118
+ end
119
+
120
+ recipe
121
+ end
122
+
123
+ end
@@ -0,0 +1,34 @@
1
+ class Nodectl::Context
2
+ attr_reader :recipe
3
+ attr_reader :service
4
+
5
+ def initialize(recipe, service, methods = nil)
6
+ @recipe = recipe
7
+ @service = service
8
+
9
+ if methods
10
+ extend methods
11
+ end
12
+ end
13
+
14
+ def exec(*args)
15
+ Process.exec(*args)
16
+ end
17
+
18
+ def chdir(dir)
19
+ Dir.chdir(dir)
20
+ end
21
+
22
+ def run(command, options = {}, &blk)
23
+ instance = Nodectl::Instance.new(service, command, options) do
24
+ instance_eval &blk if blk
25
+ end
26
+
27
+ instance.run
28
+ instance
29
+ end
30
+
31
+ def run_action(action_name, options = {})
32
+ service.recipe_binding.run_action(action_name, options)
33
+ end
34
+ end
@@ -0,0 +1,55 @@
1
+ class Nodectl::Database
2
+ LoadError = Class.new(StandardError)
3
+ DumpError = Class.new(StandardError)
4
+
5
+ def initialize(file_path)
6
+ @db = DBM.open(file_path, 0666, DBM::WRCREAT)
7
+ end
8
+
9
+ def reset
10
+ @db.clear
11
+ end
12
+
13
+ def fetch(key, default = nil)
14
+ blob = @db[key]
15
+
16
+ if blob
17
+ load(blob)
18
+ elsif default
19
+ default
20
+ else
21
+ raise LoadError, "key not found: '#{key}'"
22
+ end
23
+ end
24
+
25
+ def [](key)
26
+ blob = @db[key]
27
+
28
+ if blob
29
+ load(blob)
30
+ else
31
+ nil
32
+ end
33
+ end
34
+
35
+ def []=(key, value)
36
+ @db[key] = Marshal.dump(value)
37
+ rescue TypeError => e
38
+ raise LoadError, "marshal dump error for key '#{key}' and value '#{value}': #{e.message}"
39
+ end
40
+
41
+ private
42
+
43
+ def dump(value)
44
+ Marshal.dump(value)
45
+ rescue TypeError => e
46
+ raise LoadError, "marshal dump error for key '#{key}' and value '#{value}': #{e.message}"
47
+ end
48
+
49
+ def load(value)
50
+ Marshal.load(value)
51
+ rescue TypeError => e
52
+ raise LoadError, "marshal load error for key '#{key}': #{e.message}"
53
+ end
54
+
55
+ end