summaryrefslogtreecommitdiff
path: root/console.rb
diff options
context:
space:
mode:
Diffstat (limited to 'console.rb')
-rwxr-xr-xconsole.rb474
1 files changed, 474 insertions, 0 deletions
diff --git a/console.rb b/console.rb
new file mode 100755
index 0000000..6589b8a
--- /dev/null
+++ b/console.rb
@@ -0,0 +1,474 @@
+#!/usr/bin/env ruby
+#
+# console.rb: Console GUI component.
+#
+# Copyright (C) 2011 VMware, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# Apologies in advance for the state of this code, my Ruby/Tk-fu is really not
+# up to scratch. The liberal use of $global_variables is to get things working
+# correctly from Tk callbacks, if you know a better way to make this work
+# please let me know.
+
+require 'tk'
+require 'ffi-rzmq'
+require 'thread'
+
+# Convenience method to raise exception on ffi-rzmq error
+def raise_if_error(rc)
+ unless ZMQ::Util.resultcode_ok?(rc)
+ raise "ZMQ Error: #{ZMQ::Util.error_string}"
+ end
+end
+
+# Callable on any Tk widget, adds get/set methods for mytag and mytype,
+# allowing us to keep track of what is what when we get callback events.
+def make_taggable(widget)
+ widget.instance_eval do
+ @mytag = nil
+ @mytype = nil
+ def mytag
+ @mytag
+ end
+ def mytag=(tag)
+ @mytag = tag
+ end
+ def mytype
+ @mytype
+ end
+ def mytype=(type)
+ @mytype = type
+ end
+ end
+end
+
+# Build out the GUI.
+def build_gui
+ $root = TkRoot.new { title "Console" }
+ $workspace = TkFrame.new($root) {
+ relief 'sunken'
+ borderwidth '2'
+ }
+ $workspace.grid :column => 0, :row => 0, :columnspan => 2, :sticky => 'nsew'
+ $buttons = TkFrame.new($root)
+ $buttons.grid :column => 0, :row => 1, :columnspan => 2, :sticky => 'nsew'
+ $start = TkButton.new($buttons) {
+ text 'Start'
+ state :disabled
+ command proc {
+ host = $selected_item.mytag
+ start_service_dialog(host)
+ }
+ }
+ $start.pack :padx => 10, :side => :left
+ $stop = TkButton.new($buttons) {
+ text 'Stop'
+ state :disabled
+ command proc {
+ service = $selected_item.mytag
+ host = $selected_item.winfo_parent.mytag
+ result = Tk::messageBox :type => :yesno,
+ :message => "Are you sure you wish to stop the service " + \
+ "'#{service}' on host '#{host}'?",
+ :icon => :question,
+ :title => "Stop service"
+ stop_service(service, host) if result == 'yes'
+ }
+ }
+ $stop.pack :padx => 10, :side => :left
+ $restart = TkButton.new($buttons) {
+ text 'Restart'
+ state :disabled
+ command proc {
+ service = $selected_item.mytag
+ host = $selected_item.winfo_parent.mytag
+ restart_service_dialog(host, service)
+ }
+ }
+ $restart.pack :padx => 10, :side => :left
+ $dns = TkButton.new($buttons) {
+ text 'Update DNS'
+ command proc {
+ update_dns_dialog()
+ }
+ }
+ $dns.pack :padx => 10, :side => :left
+ $exit = TkButton.new($buttons) {
+ text 'Exit'
+ command proc {
+ result = Tk::messageBox :type => :yesno,
+ :message => "Are you sure you want to exit?",
+ :icon => :question,
+ :title => "Confirm exit"
+ exit if result == 'yes'
+ }
+ }
+ $exit.pack :side => :left
+ $console = TkListbox.new($root) {
+ height 10
+ width 60
+ yscrollcommand proc { |*args| $console_sb.set(*args) }
+ }
+ $console.grid :column => 0, :row => 2, :sticky => 'nwes'
+ $console_sb = TkScrollbar.new($root) {
+ orient :vertical
+ command proc { |*args| $console.yview(*args) }
+ }
+ $console_sb.grid :column => 1, :row => 2, :sticky => 'ns'
+ TkGrid.columnconfigure $root, 0, :weight => 1, :minsize => 600
+ TkGrid.rowconfigure $root, 0, :weight => 1, :minsize => 400
+end
+
+# DNS Update dialog GUI
+def update_dns_dialog
+ w = TkToplevel.new { title "Update DNS record" }
+ w.wm_transient($root)
+ l = TkLabel.new(w) { text "Record" }
+ r = TkEntry.new(w) { width 15 }
+ r.focus
+ l2 = TkLabel.new(w) { text "Value" }
+ v = TkEntry.new(w) { width 15 }
+ ok = TkButton.new(w) {
+ text "OK"
+ command proc {
+ if r.get.length > 0 && v.get.length > 0
+ result = `./nsupdate-script #{r.get} #{v.get}`
+ result.chomp
+ console_add("Updating DNS record '#{r.get}' to value '#{v.get}': #{result}")
+ w.destroy
+ end
+ }
+ default :active
+ }
+ cancel = TkButton.new(w) { text "Cancel"; command proc { w.destroy } }
+ w.bind('Return') { ok.invoke }
+ w.bind('Escape') { cancel.invoke }
+ l.grid :column => 0, :row => 0, :padx => 10, :pady => 10
+ r.grid :column => 1, :row => 0, :padx => 10, :pady => 10
+ l2.grid :column => 0, :row => 1, :padx => 10, :pady => 10
+ v.grid :column => 1, :row => 1, :padx => 10, :pady => 10
+ ok.grid :column => 0, :row => 2, :padx => 10, :pady => 10
+ cancel.grid :column => 1, :row => 2, :padx => 10, :pady => 10
+ w.grab
+ w.tkwait
+end
+
+# Start Service dialog GUI
+def start_service_dialog(host)
+ w = TkToplevel.new { title "Start service on #{host}" }
+ w.wm_transient($root)
+ l = TkLabel.new(w) { text "Command" }
+ e = TkEntry.new(w) { width 40 }
+ e.focus
+ ok = TkButton.new(w) {
+ text "OK"
+ command proc {
+ if e.get.length > 0
+ start_service(host, e.get)
+ w.destroy
+ end
+ }
+ default :active
+ }
+ cancel = TkButton.new(w) { text "Cancel"; command proc { w.destroy } }
+ w.bind('Return') { ok.invoke }
+ w.bind('Escape') { cancel.invoke }
+ l.grid :column => 0, :row => 0, :padx => 10, :pady => 10
+ e.grid :column => 1, :row => 0, :padx => 10, :pady => 10
+ ok.grid :column => 0, :row => 1, :padx => 10, :pady => 10
+ cancel.grid :column => 1, :row => 1, :padx => 10, :pady => 10
+ w.grab
+ w.tkwait
+end
+
+# Restart service dialog GUI
+def restart_service_dialog(host, service)
+ w = TkToplevel.new { title "Restart service #{service} on #{host}" }
+ w.wm_transient($root)
+ l = TkLabel.new(w) { text "Command" }
+ e = TkEntry.new(w) { width 40 }
+ e.set($hosts[host][:services][service][:command])
+ e.focus
+ ok = TkButton.new(w) {
+ text "OK"
+ command proc {
+ if e.get.length > 0
+ stop_service(service, host)
+ start_service(host, e.get)
+ w.destroy
+ end
+ }
+ default :active
+ }
+ cancel = TkButton.new(w) { text "Cancel"; command proc { w.destroy } }
+ w.bind('Return') { ok.invoke }
+ w.bind('Escape') { cancel.invoke }
+ l.grid :column => 0, :row => 0, :padx => 10, :pady => 10
+ e.grid :column => 1, :row => 0, :padx => 10, :pady => 10
+ ok.grid :column => 0, :row => 1, :padx => 10, :pady => 10
+ cancel.grid :column => 1, :row => 1, :padx => 10, :pady => 10
+ w.grab
+ w.tkwait
+end
+
+# Start service 'command' on 'host'
+def start_service(host, command)
+ console_add("Starting command '#{command}' on host #{host}");
+ s = $ctx.socket(ZMQ::PUSH)
+ rc = s.connect("tcp://#{host}:7772")
+ raise_if_error(rc)
+ rc = s.send_string("START #{command}")
+ raise_if_error(rc)
+ rc = s.close
+ raise_if_error(rc)
+end
+
+# Stop service 'service' on 'host'
+def stop_service(service, host)
+ console_add("Stopping service: #{service} on host: #{host}")
+ s = $ctx.socket(ZMQ::PUSH)
+ rc = s.connect("tcp://#{host}:7772")
+ raise_if_error(rc)
+ rc = s.send_string("STOP #{service}")
+ raise_if_error(rc)
+ rc = s.close
+ raise_if_error(rc)
+end
+
+# Log text to console widget
+def console_add(msg)
+ $console.insert 'end', Time.now.to_s + ' ' + msg
+ $console.yview 'end'
+end
+
+# Select an item in the console, highlights it and updates available
+# action buttons
+def select_item(new)
+ old = $selected_item
+ if old
+ target = old.labelwidget if old.mytype == :host
+ target = old if old.mytype == :service
+ target.background('#d9d9d9')
+ end
+ if new.mytype == :host
+ new.labelwidget.background('green')
+ $start.state(:normal)
+ $stop.state(:disabled)
+ $restart.state(:disabled)
+ elsif new.mytype == :service
+ new.background('green')
+ $start.state(:disabled)
+ $stop.state(:normal)
+ $restart.state(:normal)
+ end
+ $selected_item = new
+end
+
+# Run periodically from Tk Timer to pull interesting messages from notifier
+# and update the GUI accordingly.
+def update
+ begin
+ msg = ''
+ rc = 0
+ loop do
+ $semaphore.synchronize do
+ rc = $notifier_sock.recv_string(msg, ZMQ::DONTWAIT)
+ end
+ if rc < 0
+ break if ZMQ::Util.errno == ZMQ::EAGAIN
+ raise_if_error(rc)
+ end
+ if msg =~ /^WATCHDOG (\w+) (\w+) ([\w\.]+) ?(.*)?$/
+ verb = $1
+ host = $2
+ timestamp = $3.to_f
+ command = $4
+ if verb == 'ALIVE'
+ if $hosts.has_key?(host)
+ $hosts[host][:last_seen] = timestamp
+ else
+ label = TkLabel.new($workspace) { text host }
+ make_taggable(label)
+ label.mytag = host
+ label.mytype = :host
+ host_widget = TkLabelFrame.new($workspace) {
+ labelwidget label
+ width '4c'
+ height '4c'
+ relief 'raised'
+ padx 10
+ pady 10
+ }
+ make_taggable(host_widget)
+ host_widget.mytag = host
+ host_widget.mytype = :host
+ host_widget.pack :side => :left,:anchor => 'nw',
+ :padx => 10, :pady => 10
+ host_widget.cursor("hand2")
+ label.cursor("hand2")
+ label.bind("1", proc { |event|
+ host = event.widget.mytag
+ real_widget = $hosts[host][:widget]
+ select_item(real_widget)
+ console_add("Selected host: #{host}")
+ })
+ host_widget.bind("1", proc { |event|
+ select_item(event.widget)
+ console_add("Selected host: #{event.widget.mytag}")
+ })
+ $hosts[host] = {}
+ $hosts[host][:widget] = host_widget
+ $hosts[host][:services] = {}
+ $hosts[host][:last_seen] = timestamp
+ console_add("Registered host: #{host}")
+ end
+ elsif verb == 'RUNNING'
+ service = command.split()[0]
+ svcs = $hosts[host][:services]
+ if svcs.has_key?(service)
+ svcs[service][:last_seen] = timestamp
+ else
+ svcs[service] = {}
+ svcs[service][:command] = command
+ svcs[service][:last_seen] = timestamp
+ host_widget = $hosts[host][:widget]
+ service_widget = TkLabel.new(host_widget) {
+ text service
+ padx '0.25c'
+ pady '0.25c'
+ }
+ make_taggable(service_widget)
+ service_widget.mytype = :service
+ service_widget.mytag = service
+ service_widget.pack { fill 'both' }
+ service_widget.cursor("hand2")
+ service_widget.bind("1", proc { |event|
+ select_item(event.widget)
+ console_add("Selected service: #{event.widget.mytag}")
+ })
+ svcs[service][:widget] = service_widget
+ console_add("Registered service: #{service} on host: #{host}")
+ end
+ end
+ elsif msg =~ /^VALUE (\w+) (\w+) (.*)$/
+ host = $1
+ service = $2
+ value = $3
+ if $hosts.has_key?(host) && $hosts[host][:services].has_key?(service)
+ widget = $hosts[host][:services][service][:widget]
+ widget.text(widget.mytag + "\n" + value)
+ end
+ end
+ end
+ rescue => exception
+ print exception, "\n"
+ print exception.backtrace.join("\n\t from "), "\n"
+ Process.abort
+ end
+end
+
+# Run periodically from Tk Timer to reap dead hosts/services from GUI.
+def reaper
+ begin
+ current_time = Time.now.to_f
+ $hosts.each do |key, host|
+ if current_time - host[:last_seen] > 2
+ host[:services].each do |key, service|
+ if $selected_item == service[:widget]
+ $start.state(:disabled)
+ $stop.state(:disabled)
+ $restart.state(:disabled)
+ $selected_item = nil
+ end
+ service[:widget].destroy
+ host[:services].delete(key)
+ end
+ if $selected_item == host[:widget]
+ $start.state(:disabled)
+ $stop.state(:disabled)
+ $restart.state(:disabled)
+ $selected_item = nil
+ end
+ host[:widget].destroy
+ $hosts.delete(key)
+ console_add("Removed host: #{key}")
+ else
+ host[:services].each do |key, service|
+ if current_time - service[:last_seen] > 2
+ if $selected_item == service[:widget]
+ $start.state(:disabled)
+ $stop.state(:disabled)
+ $restart.state(:disabled)
+ $selected_item = nil
+ end
+ service[:widget].destroy
+ host[:services].delete(key)
+ console_add("Removed service: #{key}")
+ end
+ end
+ end
+ end
+ rescue => exception
+ print exception, "\n"
+ print exception.backtrace.join("\n\t from "), "\n"
+ Process.abort
+ end
+end
+
+#
+# Main program
+#
+
+# Globals used by GUI
+# ZMQ context
+$ctx = ZMQ::Context.new()
+# Currently selected item (host/service)
+$selected_item = nil
+# Map of hosts and their services
+$hosts = {}
+
+# Parse arguments
+unless ARGV[0]
+ print "usage: console.rb <notifier-host>\n"
+ exit 1
+end
+notifier_ep = "tcp://#{ARGV[0]}:7771"
+
+# Create and connect to notifier socket. Synchronized to allow migration to
+# GUI callback thread.
+$semaphore = Mutex.new
+$semaphore.synchronize do
+ $notifier_sock = $ctx.socket(ZMQ::SUB)
+ rc = $notifier_sock.setsockopt(ZMQ::SUBSCRIBE, '')
+ raise_if_error(rc)
+ rc = $notifier_sock.connect(notifier_ep)
+ raise_if_error(rc)
+end
+
+# Build and update GUI immediately on startup
+build_gui
+update
+
+# Setup timers and go into Tk mainloop
+# Update GUI/reap dead objects every 500ms
+TkTimer.new(500, -1, proc { update }).start
+TkTimer.new(500, -1, proc { reaper }).start
+Tk.mainloop
+