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