├── test ├── core_package │ ├── stuff │ └── bin │ │ ├── launcher │ │ ├── xndeploy │ │ └── control ├── property_data │ ├── foo-bar.properties │ └── a │ │ ├── b │ │ ├── c │ │ │ ├── test_simple.properties │ │ │ └── d │ │ │ │ └── test_override.properties │ │ ├── test_override.properties │ │ └── xncore.properties │ │ ├── build.properties │ │ └── test_comments_ignored.properties ├── bad_core_package │ ├── stuff │ └── bin │ │ ├── xndeploy │ │ ├── launcher │ │ └── control ├── performance │ ├── lib │ │ ├── httpcore-4.0.1.jar │ │ ├── httpmime-4.0.1.jar │ │ ├── httpclient-4.0.1.jar │ │ ├── commons-logging-1.1.1.jar │ │ └── one-jar-ant-task-0.96.jar │ ├── build.xml │ └── src │ │ └── LoadTest.java ├── test_db.rb ├── test_host.rb ├── test_parallelize.rb ├── test_propbuilder.rb ├── test_repository.rb ├── test_announcements.rb ├── test_fetcher.rb ├── test_controller.rb ├── test_console.rb ├── test_config.rb ├── test_temp.rb ├── test_logger_collector.rb ├── test_transport.rb ├── test_client.rb ├── test_event.rb ├── test_agent.rb ├── test_deployer.rb ├── test_logger.rb ├── helper.rb ├── test_filter.rb └── test_commands.rb ├── lib └── galaxy │ ├── version.rb │ ├── agent_utils.rb │ ├── commands │ ├── cleanup.rb │ ├── show_console.rb │ ├── stop.rb │ ├── start.rb │ ├── restart.rb │ ├── clear.rb │ ├── rollback.rb │ ├── show_agent.rb │ ├── show_core.rb │ ├── ssh.rb │ ├── reap.rb │ ├── show.rb │ ├── perform.rb │ ├── update.rb │ ├── assign.rb │ └── update_config.rb │ ├── versioning.rb │ ├── repository.rb │ ├── db.rb │ ├── client.rb │ ├── fetcher.rb │ ├── software.rb │ ├── temp.rb │ ├── filter.rb │ ├── parallelize.rb │ ├── properties.rb │ ├── starter.rb │ ├── controller.rb │ ├── deployer.rb │ ├── command.rb │ ├── proxy_console.rb │ ├── log.rb │ ├── transport.rb │ ├── daemon.rb │ ├── report.rb │ ├── host.rb │ ├── console.rb │ ├── events.rb │ ├── announcements.rb │ ├── agent.rb │ └── config.rb ├── .gitignore ├── assembly.xml ├── pom.xml ├── bin ├── galaxy-console ├── galaxy-agent └── galaxy ├── Rakefile └── LICENSE-2.0.txt /test/core_package/stuff: -------------------------------------------------------------------------------- 1 | This is some stuff 2 | -------------------------------------------------------------------------------- /test/property_data/foo-bar.properties: -------------------------------------------------------------------------------- 1 | helo=g'bye -------------------------------------------------------------------------------- /test/bad_core_package/stuff: -------------------------------------------------------------------------------- 1 | This is some stuff 2 | -------------------------------------------------------------------------------- /test/property_data/a/b/c/test_simple.properties: -------------------------------------------------------------------------------- 1 | chris = green 2 | -------------------------------------------------------------------------------- /test/property_data/a/b/c/d/test_override.properties: -------------------------------------------------------------------------------- 1 | oscar = purple 2 | -------------------------------------------------------------------------------- /test/property_data/a/b/test_override.properties: -------------------------------------------------------------------------------- 1 | oscar = blue 2 | sam = red -------------------------------------------------------------------------------- /test/property_data/a/build.properties: -------------------------------------------------------------------------------- 1 | type=test 2 | build=1.0-12345 3 | -------------------------------------------------------------------------------- /test/property_data/a/b/xncore.properties: -------------------------------------------------------------------------------- 1 | oscar = green 2 | core = hello 3 | -------------------------------------------------------------------------------- /test/property_data/a/test_comments_ignored.properties: -------------------------------------------------------------------------------- 1 | #hello = 7 2 | red = fuschia -------------------------------------------------------------------------------- /test/performance/lib/httpcore-4.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ning/galaxy/HEAD/test/performance/lib/httpcore-4.0.1.jar -------------------------------------------------------------------------------- /test/performance/lib/httpmime-4.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ning/galaxy/HEAD/test/performance/lib/httpmime-4.0.1.jar -------------------------------------------------------------------------------- /test/performance/lib/httpclient-4.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ning/galaxy/HEAD/test/performance/lib/httpclient-4.0.1.jar -------------------------------------------------------------------------------- /test/performance/lib/commons-logging-1.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ning/galaxy/HEAD/test/performance/lib/commons-logging-1.1.1.jar -------------------------------------------------------------------------------- /test/performance/lib/one-jar-ant-task-0.96.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ning/galaxy/HEAD/test/performance/lib/one-jar-ant-task-0.96.jar -------------------------------------------------------------------------------- /lib/galaxy/version.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | # Don't forget to also update the version in build/sun/pkginfo 3 | Version = "3.0.0" 4 | end 5 | -------------------------------------------------------------------------------- /test/bad_core_package/bin/xndeploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'fileutils' 5 | 6 | raise "obviously a major malfunction" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | *.o 3 | *.lo 4 | *.pc 5 | *.log 6 | *.status 7 | .deps/ 8 | *.iml 9 | *.ipr 10 | *.iws 11 | coverage/ 12 | target/ 13 | .rakeTasks 14 | -------------------------------------------------------------------------------- /test/bad_core_package/bin/launcher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'fileutils' 4 | 5 | File.open("/tmp/galaxy_test_#{Process.ppid}", "w") do |f| 6 | f.puts ARGV[0] 7 | end -------------------------------------------------------------------------------- /test/core_package/bin/launcher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'fileutils' 4 | 5 | File.open("/tmp/galaxy_test_#{Process.ppid}", "w") do |f| 6 | f.puts ARGV[0] 7 | end -------------------------------------------------------------------------------- /lib/galaxy/agent_utils.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | 3 | module Galaxy 4 | module AgentUtils 5 | def ping_agent agent 6 | Timeout::timeout(5) do 7 | agent.proxy.status 8 | end 9 | end 10 | 11 | module_function :ping_agent 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | tar.gz 4 | 5 | false 6 | 7 | 8 | 9 | lib/** 10 | 11 | / 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/galaxy/commands/cleanup.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class CleanupCommand < Command 4 | register_command "cleanup" 5 | 6 | def execute_for_agent agent 7 | agent.proxy.cleanup! 8 | end 9 | 10 | def self.help 11 | return <<-HELP 12 | #{name} 13 | 14 | Remove all deployments up to the current and last one, for rollback. 15 | HELP 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/galaxy/versioning.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Versioning 3 | class StrictVersioningPolicy 4 | def self.assignment_allowed? current_config, requested_config 5 | if current_config.environment == requested_config.environment and current_config.type == requested_config.type 6 | return current_config.version != requested_config.version 7 | end 8 | true 9 | end 10 | end 11 | 12 | class RelaxedVersioningPolicy 13 | def self.assignment_allowed? current_config, requested_config 14 | true 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/galaxy/commands/show_console.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class ShowConsoleCommand < Command 4 | register_command "show-console" 5 | 6 | def execute agents 7 | report.start 8 | report.record_result @options[:console] 9 | report.finish 10 | end 11 | 12 | def report_class 13 | Galaxy::Client::ConsoleStatusReport 14 | end 15 | 16 | def self.help 17 | return <<-HELP 18 | #{name} 19 | 20 | Show metadata about the selected Galaxy console 21 | HELP 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/galaxy/commands/stop.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class StopCommand < Command 4 | register_command "stop" 5 | changes_agent_state 6 | 7 | def normalize_filter filter 8 | filter = super 9 | filter[:set] = :taken if filter[:set] == :all 10 | filter 11 | end 12 | 13 | def execute_for_agent agent 14 | agent.proxy.stop! 15 | end 16 | 17 | def self.help 18 | return <<-HELP 19 | #{name} 20 | 21 | Stop the deployed software on the selected hosts 22 | HELP 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/galaxy/commands/start.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class StartCommand < Command 4 | register_command "start" 5 | changes_agent_state 6 | 7 | def normalize_filter filter 8 | filter = super 9 | filter[:set] = :taken if filter[:set] == :all 10 | filter 11 | end 12 | 13 | def execute_for_agent agent 14 | agent.proxy.start! 15 | end 16 | 17 | def self.help 18 | return <<-HELP 19 | #{name} 20 | 21 | Start the deployed software on the selected hosts 22 | HELP 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/galaxy/commands/restart.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class RestartCommand < Command 4 | register_command "restart" 5 | changes_agent_state 6 | 7 | def normalize_filter filter 8 | filter = super 9 | filter[:set] = :taken if filter[:set] == :all 10 | filter 11 | end 12 | 13 | def execute_for_agent agent 14 | agent.proxy.restart! 15 | end 16 | 17 | def self.help 18 | return <<-HELP 19 | #{name} 20 | 21 | Restart the deployed software on the selected hosts 22 | HELP 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/galaxy/commands/clear.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class ClearCommand < Command 4 | register_command "clear" 5 | changes_agent_state 6 | 7 | def normalize_filter filter 8 | filter = super 9 | filter[:set] = :taken if filter[:set] == :all 10 | filter 11 | end 12 | 13 | def execute_for_agent agent 14 | agent.proxy.clear! 15 | end 16 | 17 | def self.help 18 | return <<-HELP 19 | #{name} 20 | 21 | Stop and clear the active software deployment on the selected hosts 22 | HELP 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/galaxy/commands/rollback.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class RollbackCommand < Command 4 | register_command "rollback" 5 | changes_agent_state 6 | 7 | def normalize_filter filter 8 | filter = super 9 | filter[:set] = :taken if filter[:set] == :all 10 | filter 11 | end 12 | 13 | def execute_for_agent agent 14 | agent.proxy.rollback! 15 | end 16 | 17 | def self.help 18 | return <<-HELP 19 | #{name} 20 | 21 | Stop and rollback software to the previously deployed version 22 | HELP 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/test_db.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/db' 6 | require 'helper' 7 | 8 | class TestDB < Test::Unit::TestCase 9 | 10 | def setup 11 | @db = Galaxy::DB.new "#{Helper.mk_tmpdir}/galaxy.db" 12 | end 13 | 14 | def test_silly 15 | @db["k"] = "v" 16 | assert_equal "v", @db["k"] 17 | end 18 | 19 | def test_in_child 20 | @db["name"] = "Fred" 21 | pid = fork do 22 | if @db["name"] == "Fred" 23 | exit 0 24 | else 25 | exit 1 26 | end 27 | end 28 | _, status = Process.waitpid2 pid 29 | assert_equal 0, status.exitstatus 30 | end 31 | 32 | 33 | 34 | end 35 | -------------------------------------------------------------------------------- /test/core_package/bin/xndeploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'fileutils' 5 | 6 | OptionParser.new do |opts| 7 | opts.on("--base BASE") { |arg| DeployBase = arg } 8 | opts.on("--binaries BINARIES") { |arg| BinariesBase = arg } 9 | opts.on("--config-path PATH") { |arg| ConfigPath = arg } 10 | opts.on("--repository URL") { |arg| Repository = arg } 11 | end.parse! ARGV 12 | 13 | # for test_xndeploy_invoked_on_deploy 14 | begin 15 | dump = { 16 | :deploy_base => DeployBase, 17 | :config_path => ConfigPath, 18 | :repository => Repository, 19 | :binaries_base => BinariesBase, 20 | } 21 | File.open(File.join(DeployBase, "xndeploy_touched_me"), "w") do |file| 22 | Marshal.dump(dump, file) 23 | end 24 | rescue TypeError 25 | end 26 | -------------------------------------------------------------------------------- /lib/galaxy/commands/show_agent.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class ShowAgentCommand < Command 4 | register_command "show-agent" 5 | 6 | def execute agents 7 | report.start 8 | agents.sort_by { |agent| agent.host }.each do |agent| 9 | report.record_result agent 10 | end 11 | report.finish 12 | end 13 | 14 | def report_class 15 | Galaxy::Client::AgentStatusReport 16 | end 17 | 18 | def self.help 19 | return <<-HELP 20 | #{name} 21 | 22 | Show metadata about the selected Galaxy agents 23 | HELP 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/bad_core_package/bin/control: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | 5 | rest = OptionParser.new do |opts| 6 | opts.on("--base BASE") { |arg| @core_base = arg } 7 | opts.on("--config-path PATH") { |arg| @config_path = arg } 8 | opts.on("--repository URL") { |arg| @repository_base = arg } 9 | end.parse! ARGV 10 | 11 | command = rest.shift 12 | 13 | case command 14 | when 'test-success' 15 | exit! 0 16 | when 'test-failure' 17 | exit! 1 18 | when 'test-arguments' 19 | if (@core_base and 20 | File.join(@core_base, 'bin', 'control') == File.expand_path(__FILE__) and 21 | @config_path == '/test/config/path' and 22 | @repository_base == 'http://repository/base') 23 | exit! 0 24 | else 25 | exit! 1 26 | end 27 | else 28 | exit! 2 29 | end 30 | -------------------------------------------------------------------------------- /test/test_host.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | 3 | require "fileutils" 4 | require "test/unit" 5 | require "galaxy/host" 6 | 7 | class TestHost < Test::Unit::TestCase 8 | 9 | def test_tar_executable_was_found 10 | assert_not_nil Galaxy::HostUtils.tar 11 | end 12 | 13 | def test_system_success 14 | assert_nothing_raised do 15 | Galaxy::HostUtils.system 'true' 16 | end 17 | end 18 | 19 | def test_system_failure 20 | assert_raise Galaxy::HostUtils::CommandFailedError do 21 | Galaxy::HostUtils.system 'false' 22 | end 23 | end 24 | 25 | def test_system_failure_output 26 | begin 27 | Galaxy::HostUtils.system 'ls /gorple/fez' 28 | rescue Exception => e 29 | assert_match(/No such file or directory/, e.message) 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/galaxy/commands/show_core.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class ShowCoreCommand < Command 4 | register_command "show-core" 5 | 6 | def execute agents 7 | report.start 8 | agents.sort_by { |agent| agent.host }.each do |agent| 9 | report.record_result agent 10 | end 11 | report.finish 12 | end 13 | 14 | def report_class 15 | Galaxy::Client::CoreStatusReport 16 | end 17 | 18 | def self.help 19 | return <<-HELP 20 | #{name} 21 | 22 | Show core status (last start time, ...) on the selected hosts 23 | See "galaxy show -h" for help and examples on flags usage 24 | HELP 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/galaxy/commands/ssh.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class SSHCommand < Command 4 | register_command "ssh" 5 | 6 | def execute agents 7 | agent = agents.first 8 | command = ENV['GALAXY_SSH_COMMAND'] || "ssh" 9 | Kernel.system "#{command} #{agent.host}" if agent 10 | end 11 | 12 | def self.help 13 | return <<-HELP 14 | #{name} 15 | 16 | Connect via ssh to the first host matching the selection criteria 17 | 18 | The GALAXY_SSH_COMMAND environment variable can be set to specify options for ssh. 19 | 20 | For example, this instructs galaxy to login as user 'foo': 21 | 22 | export GALAXY_SSH_COMMAND="ssh -l foo" 23 | HELP 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_parallelize.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'galaxy/parallelize' 3 | 4 | class TestParallelize < Test::Unit::TestCase 5 | def test_parallelize_with_thread_count_of_1 6 | array = (1..10).entries 7 | start = Time.new 8 | array.parallelize(1) { |i| sleep 1 } 9 | stop = Time.new 10 | assert stop - start >= 10 11 | assert stop - start < 11 12 | end 13 | 14 | def test_parallelize_with_thread_count_of_10 15 | array = (1..100).entries 16 | start = Time.new 17 | array.parallelize(10) { |i| sleep 1 } 18 | stop = Time.new 19 | assert stop - start >= 10 20 | assert stop - start < 11 21 | end 22 | 23 | def test_parallelize_with_thread_count_of_100 24 | array = (1..1000).entries 25 | start = Time.new 26 | array.parallelize(100) { |i| sleep 1 } 27 | stop = Time.new 28 | assert stop - start >= 10 29 | assert stop - start < 11 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/galaxy/commands/reap.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class ReapCommand < Command 4 | register_command "reap" 5 | changes_console_state 6 | 7 | def execute agents 8 | report.start 9 | agents.sort_by { |agent| agent.host }.each do |agent| 10 | reaped = @options[:console].reap(agent.host) 11 | report.record_result("#{agent.host} - reap #{reaped.nil? ? 'failed' : 'succeeded'}") 12 | end 13 | [report.finish, nil] 14 | end 15 | 16 | def report_class 17 | Galaxy::Client::Report 18 | end 19 | 20 | def self.help 21 | return <<-HELP 22 | #{name} 23 | 24 | Delete stale announcements (from the console) for the selected hosts, without affecting agents 25 | HELP 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/galaxy/repository.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'logger' 3 | 4 | module Galaxy 5 | class Repository 6 | def initialize base, log=Logger.new(STDOUT) 7 | @base = base 8 | end 9 | 10 | def walk hierarchy, file_name 11 | result = [] 12 | hierarchy.split(/\//).inject([]) do |history, part| 13 | history << part 14 | begin 15 | path = "#{history.join("/")}/#{file_name}" 16 | url = "#{@base}#{path}" 17 | open(url) do |io| 18 | data = io.read 19 | if block_given? 20 | yield path, data 21 | end 22 | result << data 23 | end 24 | rescue 25 | end 26 | history 27 | end 28 | 29 | result 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_propbuilder.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | 3 | require 'test/unit' 4 | require "galaxy/properties" 5 | require 'logger' 6 | 7 | class TestPropertyBuilder < Test::Unit::TestCase 8 | 9 | PropertyBase = File.dirname(__FILE__) + "/property_data" 10 | 11 | def setup 12 | @builder = Galaxy::Properties::Builder.new PropertyBase, Logger.new("/dev/null") 13 | end 14 | 15 | def test_simple 16 | props = @builder.build "/a/b/c/d", "test_simple.properties" 17 | assert_equal "green", props['chris'] 18 | end 19 | 20 | def test_override 21 | props = @builder.build "/a/b/c/d", "test_override.properties" 22 | assert_equal "purple", props['oscar'] 23 | assert_equal "red", props['sam'] 24 | end 25 | 26 | def test_comments_ignored 27 | props = @builder.build "/a/b/c/d", "test_comments_ignored.properties" 28 | 29 | assert_nil props['hello'] 30 | assert_nil props['#hello'] 31 | assert_equal "fuschia", props['red'] 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /test/test_repository.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | 3 | require 'test/unit' 4 | require "galaxy/repository" 5 | 6 | class TestRepository < Test::Unit::TestCase 7 | 8 | PropertyBase = File.dirname(__FILE__) + "/property_data" 9 | 10 | def setup 11 | @builder = Galaxy::Repository.new PropertyBase 12 | end 13 | 14 | def test_simple 15 | @builder.walk "/a/b/c/d", "test_simple.properties" do |path, content| 16 | assert_equal "/a/b/c/test_simple.properties", path 17 | end 18 | end 19 | 20 | def test_multiple 21 | paths = [] 22 | @builder.walk "/a/b/c/d", "test_override.properties" do |path, content| 23 | paths << path 24 | end 25 | 26 | assert_equal ["/a/b/test_override.properties", "/a/b/c/d/test_override.properties"], paths 27 | end 28 | 29 | def test_empty 30 | paths = [] 31 | @builder.walk "/a/b/c/d", "empty.properties" do |path, content| 32 | paths << path 33 | end 34 | 35 | assert_equal [], paths 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/galaxy/commands/show.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class ShowCommand < Command 4 | register_command "show" 5 | 6 | def execute agents 7 | report.start 8 | agents.sort_by { |agent| agent.host }.each do |agent| 9 | report.record_result agent 10 | end 11 | report.finish 12 | end 13 | 14 | def self.help 15 | return <<-HELP 16 | #{name} 17 | 18 | Show software deployments on the selected hosts 19 | 20 | Examples: 21 | 22 | - Show all hosts: 23 | galaxy show 24 | 25 | - Show unassigned hosts: 26 | galaxy -s empty show 27 | 28 | - Show assigned hosts: 29 | galaxy -s taken show 30 | 31 | - Show a specific host: 32 | galaxy -i foo.bar.com show 33 | 34 | - Show all widgets: 35 | galaxy -t widget show 36 | HELP 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/test_announcements.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/announcements' 6 | 7 | class TestAnnouncements < Test::Unit::TestCase 8 | 9 | # example callback for action upon receiving an announcement 10 | def on_announcement(ann) 11 | assert "bar" == ann.foo # these are not Test::Unit asserts, but $received won't be set if any are false 12 | assert ann.rand >= 0 13 | assert ann.rand < 10 14 | assert "eggs" == ann.item 15 | @@received = true 16 | end 17 | 18 | def test_server 19 | # url = "http://localhost:8000" # 4442 for announcements in production, but can be anything for test 20 | # # server 21 | # Galaxy::HTTPAnnouncementReceiver.new(url, lambda{|a| on_announcement(a)}) 22 | # 23 | # # sender 24 | # announcer = HTTPAnnouncementSender.new(url) 25 | # @@received = false 26 | # announcer.announce(OpenStruct.new(:foo=>"bar", :rand => rand(10), :item => "eggs")) 27 | # assert_equal true, @@received 28 | end 29 | end -------------------------------------------------------------------------------- /lib/galaxy/db.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | module Galaxy 4 | class DB 5 | def initialize path 6 | @lock = Mutex.new 7 | @path = path 8 | Dir.mkdir @path rescue nil 9 | end 10 | 11 | def delete_at key 12 | @lock.synchronize do 13 | FileUtils.rm_f file_for(key) 14 | end 15 | end 16 | 17 | def []= key, value 18 | @lock.synchronize do 19 | File.open file_for(key), "w" do |f| 20 | f.write(value) 21 | end 22 | end 23 | end 24 | 25 | def [] key 26 | @lock.synchronize do 27 | result = nil 28 | begin 29 | File.open file_for(key), "r" do |f| 30 | result = f.read 31 | end 32 | rescue Errno::ENOENT 33 | end 34 | 35 | return result 36 | end 37 | end 38 | 39 | def file_for key 40 | File.join(@path, key) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/galaxy/client.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'etc' 3 | require 'resolv' 4 | 5 | class CommandLineError < Exception; 6 | end 7 | 8 | def prompt_and_wait_for_user_confirmation prompt 9 | confirmed = false 10 | loop do 11 | $stderr.print prompt 12 | $stderr.flush 13 | case $stdin.gets.chomp.downcase 14 | when "y" 15 | confirmed = true 16 | break 17 | when "n" 18 | break 19 | else 20 | $stderr.puts "Please enter 'y' or 'n'" 21 | end 22 | end 23 | confirmed 24 | end 25 | 26 | # Expand the supplied console_url (which may just consist of hostname) to a full URL, assuming DRb as the default transport 27 | def normalize_console_url console_url 28 | console_url = "druby://#{console_url}" unless console_url.match(/^\w+:\/\//) 29 | console_url ="#{console_url}:4440" unless console_url.match(/:\d+$/) 30 | console_url 31 | end 32 | 33 | # Expand short hostnames to their fully-qualified names 34 | # 35 | # This implementation depends on the client and agents sharing a common naming service (hosts files, NIS, DNS, etc). 36 | def canonical_hostname hostname 37 | Resolv.getname(Resolv.getaddress(hostname)) 38 | end 39 | -------------------------------------------------------------------------------- /lib/galaxy/commands/perform.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class PerformCommand < Command 4 | register_command "perform" 5 | changes_agent_state 6 | 7 | def initialize args, options 8 | super 9 | 10 | @command = args.shift 11 | raise CommandLineError.new(" is missing") unless @command 12 | @args = args 13 | end 14 | 15 | def normalize_filter filter 16 | filter = super 17 | filter[:set] = :taken if filter[:set] == :all 18 | filter 19 | end 20 | 21 | def execute_for_agent agent 22 | agent.proxy.perform! @command, @args.join(' ') 23 | end 24 | 25 | def report_class 26 | Galaxy::Client::CommandOutputReport 27 | end 28 | 29 | def self.help 30 | return <<-HELP 31 | #{name} 32 | 33 | galaxy perform [args] 34 | 35 | Launch the control script (bin/control) with the indicated command on the selected hosts, optionally passing the provided arguments 36 | HELP 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/galaxy/fetcher.rb: -------------------------------------------------------------------------------- 1 | require 'galaxy/temp' 2 | require 'galaxy/host' 3 | 4 | module Galaxy 5 | class Fetcher 6 | def initialize base_url, log 7 | @base, @log = base_url, log 8 | end 9 | 10 | # return path on filesystem to the binary 11 | def fetch type, version, extension="tar.gz" 12 | core_url = "#{@base}/#{type}-#{version}.#{extension}" 13 | tmp = Galaxy::Temp.mk_auto_file "galaxy-download" 14 | @log.info "Fetching #{core_url} into #{tmp}" 15 | if @base =~ /^http:/ 16 | begin 17 | output = Galaxy::HostUtils.system("curl -D - #{core_url} -o #{tmp} -s") 18 | rescue Galaxy::HostUtils::CommandFailedError => e 19 | raise "Failed to download archive #{core_url}: #{e.message}" 20 | end 21 | status = output.first 22 | (protocol, response_code, response_message) = status.split 23 | unless response_code == '200' 24 | raise "Failed to download archive #{core_url}: #{status}" 25 | end 26 | else 27 | open(core_url) do |io| 28 | File.open(tmp, "w") { |f| f.write(io.read) } 29 | end 30 | end 31 | return tmp 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/galaxy/software.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | class SoftwareExecutable 3 | attr_accessor :type, :version 4 | 5 | def initalize type, version 6 | @type = type 7 | @version = version 8 | end 9 | end 10 | 11 | class SoftwareConfiguration 12 | attr_accessor :environment, :version, :type 13 | 14 | def initialize environment, version, type 15 | @environment = environment 16 | @version = version 17 | @type = type 18 | end 19 | 20 | def config_path 21 | "/#{environment}/#{version}/#{type}" 22 | end 23 | 24 | def self.new_from_config_path config_path 25 | # Using ! as regex delimiter since the config path contains / characters 26 | unless components = %r!^/([^/]+)/([^/]+)/(.*)$!.match(config_path) 27 | raise "Illegal config path '#{config_path}'" 28 | end 29 | environment, version, type = components[1], components[2], components[3] 30 | new environment, version, type 31 | end 32 | end 33 | 34 | class SoftwareDeployment 35 | attr_accessor :executable, :config, :running_state 36 | 37 | def initialize executable, config, running_state 38 | @executable = executable 39 | @config = config 40 | @running_state = running_state 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/galaxy/temp.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'thread' 3 | require 'tmpdir' 4 | 5 | module Galaxy 6 | module Temp 7 | Mutex = Mutex.new 8 | 9 | def Temp.auto_delete path 10 | Kernel.at_exit do 11 | begin 12 | FileUtils.rm_r(path) if File.exist? path 13 | rescue => e 14 | puts "Failed to delete #{path}: #{e}" 15 | end 16 | end 17 | path 18 | end 19 | 20 | def Temp.mk_auto_file component="galaxy" 21 | auto_delete mk_file(component) 22 | end 23 | 24 | def Temp.mk_auto_dir component="galaxy" 25 | auto_delete mk_dir(component) 26 | end 27 | 28 | def Temp.mk_file component="galaxy" 29 | return * FileUtils.touch(next_name(component)) 30 | end 31 | 32 | def Temp.mk_dir component="galaxy" 33 | return * FileUtils.mkdir(next_name(component)) 34 | end 35 | 36 | private 37 | 38 | def Temp.next_name component 39 | Mutex.synchronize do 40 | @@id ||= 0 41 | name = ""; 42 | loop do 43 | @@id += 1 44 | name = File.join Dir::tmpdir, "#{component}.#{Process.pid}.#{@@id}" 45 | return name unless File.exists? name 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/test_fetcher.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/fetcher' 6 | require 'helper' 7 | require 'fileutils' 8 | require 'logger' 9 | require 'webrick' 10 | include WEBrick 11 | 12 | class TestFetcher < Test::Unit::TestCase 13 | 14 | def setup 15 | @local_fetcher = Galaxy::Fetcher.new(File.join(File.dirname(__FILE__), "property_data"), Logger.new("/dev/null")) 16 | @http_fetcher = Galaxy::Fetcher.new("http://localhost:7777", Logger.new("/dev/null")) 17 | 18 | webrick_logger = Logger.new(STDOUT) 19 | webrick_logger.level = Logger::WARN 20 | @server = HTTPServer.new(:Port => 7777, :Logger => webrick_logger) 21 | @server.mount("/", HTTPServlet::FileHandler, File.join(File.dirname(__FILE__), "property_data"), true) 22 | Thread.start do 23 | @server.start 24 | end 25 | end 26 | 27 | def teardown 28 | @server.shutdown 29 | end 30 | 31 | def test_local_fetch 32 | path = @local_fetcher.fetch "foo", "bar", "properties" 33 | assert File.exists?(path) 34 | end 35 | 36 | def test_http_fetch 37 | path = @http_fetcher.fetch "foo", "bar", "properties" 38 | assert File.exists?(path) 39 | end 40 | 41 | def test_http_fetch_not_found 42 | assert_raise RuntimeError do 43 | @server.logger.level = Logger::FATAL 44 | path = @http_fetcher.fetch "gorple", "fez", "properties" 45 | @server.logger.level = Logger::WARN 46 | end 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/galaxy/commands/update.rb: -------------------------------------------------------------------------------- 1 | require 'galaxy/software' 2 | 3 | module Galaxy 4 | module Commands 5 | class UpdateCommand < Command 6 | register_command "update" 7 | changes_agent_state 8 | 9 | def initialize args, options 10 | super 11 | @requested_version = args.first 12 | raise CommandLineError.new("Must specify version") unless @requested_version 13 | @versioning_policy = options[:versioning_policy] 14 | end 15 | 16 | def normalize_filter filter 17 | filter = super 18 | filter[:set] = :taken if filter[:set] == :all 19 | filter 20 | end 21 | 22 | def execute_for_agent agent 23 | if agent.config_path.nil? or agent.config_path.empty? 24 | raise "Cannot update unassigned agent" 25 | end 26 | current_config = Galaxy::SoftwareConfiguration.new_from_config_path(agent.config_path) # TODO - this should already be tracked 27 | requested_config = current_config.dup 28 | requested_config.version = @requested_version 29 | agent.proxy.become!(requested_config.config_path, @versioning_policy) 30 | end 31 | 32 | def self.help 33 | return <<-HELP 34 | #{name} 35 | 36 | Stop and update the software on the selected hosts to the specified version 37 | HELP 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/galaxy/commands/assign.rb: -------------------------------------------------------------------------------- 1 | require 'galaxy/report' 2 | 3 | module Galaxy 4 | module Commands 5 | class AssignCommand < Command 6 | register_command "assign" 7 | changes_agent_state 8 | 9 | def initialize args, options 10 | super 11 | 12 | env, version, type = * args 13 | 14 | raise CommandLineError.new(" is missing") unless env 15 | raise CommandLineError.new(" is missing") unless version 16 | raise CommandLineError.new(" is missing") unless type 17 | 18 | @config_path = "/#{env}/#{version}/#{type}" 19 | @versioning_policy = options[:versioning_policy] 20 | end 21 | 22 | def default_filter 23 | {:set => :empty} 24 | end 25 | 26 | def execute_for_agent agent 27 | agent.proxy.become!(@config_path, @versioning_policy) 28 | end 29 | 30 | def self.help 31 | return <<-HELP 32 | #{name} 33 | 34 | Deploy software to the selected hosts 35 | 36 | Parameters: 37 | env The environment 38 | version The software version 39 | type The software type 40 | 41 | These three parameters together define the configuration path (relative to the repository base): 42 | 43 | /// 44 | HELP 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/test_controller.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/controller' 6 | require 'galaxy/deployer' 7 | require 'galaxy/host' 8 | require 'helper' 9 | require 'fileutils' 10 | require 'logger' 11 | 12 | class TestController < Test::Unit::TestCase 13 | 14 | def setup 15 | @core_package = Tempfile.new("package.tgz").path 16 | system %{ 17 | #{Galaxy::HostUtils.tar} -C #{File.join(File.dirname(__FILE__), "core_package")} -czf #{@core_package} . 18 | } 19 | @path = Helper.mk_tmpdir 20 | @deployer = Galaxy::Deployer.new @path, Logger.new("/dev/null") 21 | @core_base = @deployer.deploy "1", @core_package, "/config", "/repository", "/binaries" 22 | @controller = Galaxy::Controller.new @core_base, '/config/path', 'http://repository/base', 'http://binaries/base', Logger.new("/dev/null") 23 | end 24 | 25 | def test_perform_success 26 | output = @controller.perform!('test-success') 27 | assert_equal "gorple\n", output 28 | end 29 | 30 | def test_perform_failure 31 | assert_raise Galaxy::Controller::CommandFailedException do 32 | @controller.perform!('test-failure') 33 | end 34 | end 35 | 36 | def test_perform_unrecognized 37 | assert_raise Galaxy::Controller::UnrecognizedCommandException do 38 | @controller.perform!('unrecognized') 39 | end 40 | end 41 | 42 | def test_controller_arguments 43 | assert_nothing_raised do 44 | @controller.perform!('test-arguments') 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/galaxy/commands/update_config.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Commands 3 | class UpdateConfigCommand < Command 4 | register_command "update-config" 5 | changes_agent_state 6 | 7 | def initialize args, options 8 | super 9 | 10 | @requested_version = args.first 11 | raise CommandLineError.new("Must specify version") unless @requested_version 12 | 13 | @versioning_policy = options[:versioning_policy] 14 | end 15 | 16 | def normalize_filter filter 17 | filter = super 18 | filter[:set] = :taken if filter[:set] == :all 19 | filter 20 | end 21 | 22 | def execute_for_agent agent 23 | if agent.config_path.nil? or agent.config_path.empty? 24 | raise "Cannot update configuration of unassigned agent" 25 | end 26 | current_config = Galaxy::SoftwareConfiguration.new_from_config_path(agent.config_path) # TODO - this should already be tracked 27 | agent.proxy.update_config!(@requested_version, @versioning_policy) 28 | end 29 | 30 | def self.help 31 | return <<-HELP 32 | #{name} 33 | 34 | Update the software configuration on the selected hosts to the specified version 35 | 36 | This does NOT redeploy or restart the software. If a restart is desired to activate the new configuration, it must be done separately. 37 | HELP 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/galaxy/filter.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Filter 3 | def self.new args 4 | filters = [] 5 | 6 | case args[:set] 7 | when :all, "all" 8 | filters << lambda { true } 9 | when :empty, "empty" 10 | filters << lambda { |a| a.config_path.nil? } 11 | when :taken, "taken" 12 | filters << lambda { |a| a.config_path } 13 | end 14 | 15 | if args[:env] || args[:version] || args[:type] 16 | env = args[:env] || "[^/]+" 17 | version = args[:version] || "[^/]+" 18 | type = args[:type] || ".+" 19 | 20 | filters << lambda { |a| a.config_path =~ %r!^/#{env}/#{version}/#{type}$! } 21 | end 22 | 23 | if args[:host] 24 | filters << lambda { |a| a.host == args[:host] } 25 | end 26 | 27 | if args[:ip] 28 | filters << lambda { |a| a.ip == args[:ip] } 29 | end 30 | 31 | if args[:machine] 32 | filters << lambda { |a| a.machine == args[:machine] } 33 | end 34 | 35 | if args[:state] 36 | filters << lambda { |a| a.status == args[:state] } 37 | end 38 | 39 | if args[:agent_state] 40 | p args[:agent_state] 41 | filters << lambda { |a| p a.agent_status; a.agent_status == args[:agent_state] } 42 | end 43 | 44 | lambda do |a| 45 | filters.inject(false) { |result, filter| result || filter.call(a) } 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/galaxy/parallelize.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | class CountingSemaphore 4 | 5 | def initialize(initvalue = 0) 6 | @counter = initvalue 7 | @waiting_list = [] 8 | end 9 | 10 | def wait 11 | Thread.critical = true 12 | if (@counter -= 1) < 0 13 | @waiting_list.push(Thread.current) 14 | Thread.stop 15 | end 16 | self 17 | ensure 18 | Thread.critical = false 19 | end 20 | 21 | def signal 22 | Thread.critical = true 23 | begin 24 | if (@counter += 1) <= 0 25 | t = @waiting_list.shift 26 | t.wakeup if t 27 | end 28 | rescue ThreadError 29 | retry 30 | end 31 | self 32 | ensure 33 | Thread.critical = false 34 | end 35 | 36 | def exclusive 37 | wait 38 | yield 39 | ensure 40 | signal 41 | end 42 | 43 | end 44 | 45 | class ThreadGroup 46 | 47 | def join 48 | list.each { |t| t.join } 49 | end 50 | 51 | def << thread 52 | add thread 53 | end 54 | 55 | def kill 56 | list.each { |t| t.kill } 57 | end 58 | 59 | end 60 | 61 | # execute in parallel with up to thread_count threads at once 62 | class Array 63 | def parallelize thread_count=100 64 | sem = CountingSemaphore.new thread_count 65 | results = [] 66 | threads = ThreadGroup.new 67 | lock = Mutex.new 68 | each_with_index do |item, i| 69 | sem.wait 70 | threads << Thread.new do 71 | begin 72 | yield item 73 | ensure 74 | sem.signal 75 | end 76 | end 77 | end 78 | 79 | threads.join 80 | 81 | results 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/core_package/bin/control: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | 5 | rest = OptionParser.new do |opts| 6 | opts.on("--base BASE") { |arg| @core_base = arg } 7 | opts.on("--binaries BINARIES") { |arg| @binaries_base = arg } 8 | opts.on("--config-path PATH") { |arg| @config_path = arg } 9 | opts.on("--repository URL") { |arg| @repository_base = arg } 10 | end.parse! ARGV 11 | 12 | command = rest.shift 13 | 14 | case command 15 | when 'test-success' 16 | puts "gorple" 17 | exit 0 18 | when 'test-failure' 19 | STDERR.puts "fmep" 20 | exit 1 21 | when 'test-multiline' 22 | puts <<-EOM 23 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sit amet arcu a risus pulvinar facilisis. 24 | Proin sed sapien nec magna mattis blandit. Phasellus porta hendrerit eros. Vestibulum ante ipsum primis in 25 | faucibus orci luctus et ultrices posuere cubilia Curae; Integer consequat, ante vitae tempus consequat, nisi 26 | purus facilisis orci, et euismod ligula purus quis magna. Vestibulum diam ante, vestibulum non, adipiscing mollis, 27 | eleifend sed, neque. Cras magna. Fusce non felis et libero posuere facilisis. Cras porttitor tempor orci. 28 | Suspendisse placerat, tortor vel vehicula tempor, felis lorem tincidunt enim, eget cursus lorem tellus non mi. 29 | Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum vitae 30 | risus. Praesent rutrum lectus quis dolor. Aliquam arcu. Sed vulputate mauris. 31 | EOM 32 | exit 0 33 | when 'test-arguments' 34 | if (@core_base and 35 | File.join(@core_base, 'bin', 'control') == File.expand_path(__FILE__) and 36 | @config_path == '/config/path' and 37 | @repository_base == 'http://repository/base' and 38 | @binaries_base == 'http://binaries/base') 39 | exit 0 40 | else 41 | exit 1 42 | end 43 | else 44 | exit 2 45 | end 46 | -------------------------------------------------------------------------------- /test/test_console.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/transport' 6 | require 'galaxy/config' 7 | require 'galaxy/console' 8 | require 'thread' 9 | require 'timeout' 10 | require 'helper' 11 | 12 | class TestConsole < Test::Unit::TestCase 13 | 14 | def setup 15 | @foo = OpenStruct.new({ 16 | :host => 'foo', 17 | :ip => '10.0.0.1', 18 | :machine => 'foomanchu', 19 | :config_path => '/alpha/1.0/bloo', 20 | :status => 'running' 21 | }) 22 | 23 | @bar = OpenStruct.new({ 24 | :host => 'bar', 25 | :ip => '10.0.0.2', 26 | :machine => 'barmanchu', 27 | :config_path => '/beta/2.0/blar', 28 | :status => 'stopped' 29 | }) 30 | 31 | @baz = OpenStruct.new({ 32 | :host => 'baz', 33 | :ip => '10.0.0.3', 34 | :machine => 'bazmanchu', 35 | :config_path => '/gamma/3.0/blaz', 36 | :status => 'dead' 37 | }) 38 | 39 | @blee = OpenStruct.new({ 40 | :host => 'blee', 41 | :ip => '10.0.0.4', 42 | :machine => 'bleemanchu' 43 | }) 44 | 45 | @console = Galaxy::Console.start({:host => "localhost", :url => "druby://localhost:4449"}) 46 | end 47 | 48 | def teardown 49 | @console.shutdown 50 | end 51 | 52 | def test_updates_last_announced_on_announce 53 | assert_nil @console.db["foo"] 54 | 55 | @console.send("announce", @foo) 56 | first = @console.db["foo"].timestamp 57 | @console.send("announce", @foo) 58 | second = @console.db["foo"].timestamp 59 | 60 | assert second > first 61 | end 62 | 63 | def test_list_agents 64 | @console.send("announce", @foo) 65 | @console.send("announce", @bar) 66 | @console.send("announce", @baz) 67 | 68 | agents = @console.agents 69 | assert_equal 3, agents.length 70 | 71 | assert agents.include?(@foo) 72 | assert agents.include?(@bar) 73 | assert agents.include?(@baz) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/galaxy/properties.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'open-uri' 3 | require 'logger' 4 | 5 | module Galaxy 6 | module Properties 7 | 8 | def Properties.parse_props io, props={} 9 | io.each_line do |line| 10 | if line =~ /^(\s)*#/ 11 | # comment, ignore 12 | elsif line =~ /^([^=]+)\s*=(.*)$/ 13 | props[$1.strip] = $2.strip 14 | end 15 | 16 | end 17 | props 18 | end 19 | 20 | class Builder 21 | def initialize base, log=Logger.new(STDOUT) 22 | @base = base 23 | @log = log 24 | end 25 | 26 | def build hierarchy, file_name 27 | props = {} 28 | hierarchy.split(/\//).inject([]) do |history, part| 29 | history << part 30 | begin 31 | url = "#{@base}#{history.join("/")}/#{file_name}" 32 | @log.debug "Fetching #{url}" 33 | open(url) do |io| 34 | Properties.parse_props io, props 35 | end 36 | rescue => e 37 | @log.debug e.message 38 | end 39 | history 40 | end 41 | @log.debug props.inspect 42 | props 43 | end 44 | 45 | def replace_tokens properties, tokens 46 | # replace special tokens 47 | # syntax is #{TOKEN} 48 | # (old syntax of $TOKEN is deprecated) 49 | properties.inject({}) do |hash, pair| 50 | key, value = pair 51 | hash[key] = value 52 | tokens.each { |find, replace| hash[key] = hash[key].gsub("$#{find}", replace).gsub("\#{#{find}}", replace) } 53 | hash 54 | end 55 | end 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /lib/galaxy/starter.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'galaxy/host' 3 | require 'logger' 4 | 5 | module Galaxy 6 | class Starter 7 | def initialize log 8 | @log = log 9 | end 10 | 11 | [:start!, :restart!, :stop!, :status].each do |action| 12 | define_method action.to_s do |path| 13 | return "unknown" if path.nil? 14 | launcher_path = xnctl_path(path) 15 | 16 | command = "#{launcher_path} #{action.to_s.chomp('!')}" 17 | @log.debug "Running #{command}" 18 | begin 19 | output = Galaxy::HostUtils.system command 20 | @log.debug "#{command} returned: #{output}" 21 | # Command returned 0, return status of the app 22 | case action 23 | when :start! 24 | when :restart! 25 | return "running" 26 | when :stop! 27 | when :status 28 | return "stopped" 29 | else 30 | return "unknown" 31 | end 32 | rescue Galaxy::HostUtils::CommandFailedError => e 33 | # status is special 34 | if action == :status 35 | if e.exitstatus == 1 36 | return "running" 37 | else 38 | return "unknown" 39 | end 40 | end 41 | 42 | @log.warn "Unable to #{action}: #{e.message}" 43 | raise e 44 | end 45 | end 46 | end 47 | 48 | private 49 | 50 | def xnctl_path path 51 | xnctl = File.join(path, "bin", "launcher") 52 | xnctl = "/bin/sh #{xnctl}" unless FileTest.executable? xnctl 53 | xnctl 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/test_config.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/config' 6 | require 'galaxy/log' 7 | require 'helper' 8 | require 'ostruct' 9 | require 'stringio' 10 | require 'logger' 11 | 12 | class TestConfig < Test::Unit::TestCase 13 | 14 | def setup 15 | @s = OpenStruct.new 16 | @c = Galaxy::AgentConfigurator.new @s 17 | @c2 = Galaxy::ConsoleConfigurator.new @s 18 | end 19 | 20 | def test_host_defaults_to_hostname 21 | assert_equal `hostname`.strip, @c.host 22 | end 23 | 24 | def test_logging 25 | Galaxy::HostUtils.logger("fred").info "boo!" 26 | Galaxy::HostUtils.logger("fred").info "warn!" 27 | end 28 | 29 | def test_data_dir 30 | assert_equal "#{Galaxy::HostUtils.avail_path}/galaxy-agent/data" , @c.data_dir 31 | end 32 | 33 | def test_deploy_dir 34 | assert_equal "#{Galaxy::HostUtils.avail_path}/galaxy-agent/deploy" , @c.deploy_dir 35 | end 36 | 37 | def test_deploy_dir_specced 38 | @s.deploy_dir = "/tmp/plop" 39 | assert_equal "/tmp/plop", @c.deploy_dir 40 | end 41 | 42 | def test_data_dir_specced 43 | @s.data_dir = "/tmp/plop" 44 | assert_equal "/tmp/plop", @c.data_dir 45 | end 46 | 47 | def test_verbose 48 | @s.verbose = true 49 | assert @c.configure[:verbose] 50 | end 51 | 52 | def test_log_level_debug 53 | @s.log_level = "DEBUG" 54 | assert_equal Logger::DEBUG, @c.configure[:log_level] 55 | end 56 | 57 | def test_log_level_info 58 | @s.log_level = "INFO" 59 | assert_equal Logger::INFO, @c.configure[:log_level] 60 | end 61 | 62 | def test_log_level_warn 63 | @s.log_level = "WARN" 64 | assert_equal Logger::WARN, @c.configure[:log_level] 65 | end 66 | 67 | def test_log_level_error 68 | @s.log_level = "ERROR" 69 | assert_equal Logger::ERROR, @c.configure[:log_level] 70 | end 71 | 72 | def test_log_level_other 73 | @s.log_level = "---UNK" 74 | assert_equal nil, @c.configure[:log_level] 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/performance/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | -------------------------------------------------------------------------------- /test/test_temp.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | 3 | require "fileutils" 4 | require "test/unit" 5 | require "galaxy/temp" 6 | 7 | class TestTemp < Test::Unit::TestCase 8 | 9 | def test_simple 10 | begin 11 | file = Galaxy::Temp.mk_file 12 | dir = Galaxy::Temp.mk_dir 13 | assert File.exists?(file) 14 | assert File.exists?(dir) 15 | ObjectSpace.garbage_collect 16 | assert File.exists?(file) 17 | assert File.exists?(dir) 18 | ensure 19 | FileUtils.rm file if File.exists? file 20 | FileUtils.rmdir dir if File.exists? dir 21 | end 22 | end 23 | 24 | def test_repeated 25 | used_files = [] 26 | used_dirs = [] 27 | begin 28 | 100.times do 29 | file = Galaxy::Temp.mk_file 30 | assert !used_files.include?(file) 31 | assert !used_dirs.include?(file) 32 | used_files.push file 33 | dir = Galaxy::Temp.mk_dir 34 | assert !used_files.include?(dir) 35 | assert !used_dirs.include?(dir) 36 | used_dirs.push dir 37 | assert File.exists?(file) 38 | assert File.exists?(dir) 39 | ObjectSpace.garbage_collect 40 | assert File.exists?(file) 41 | assert File.exists?(dir) 42 | end 43 | ensure 44 | used_files.each { |file| FileUtils.rm file if File.exists? file } 45 | end 46 | end 47 | 48 | def test_auto 49 | rd, wr = IO.pipe 50 | if fork 51 | wr.close 52 | file, dir = rd.read.split "\t" 53 | rd.close 54 | Process.wait 55 | begin 56 | assert !File.exists?(file) 57 | assert !File.exists?(dir) 58 | ensure 59 | FileUtils.rm file if File.exists? file 60 | FileUtils.rmdir dir if File.exists? dir 61 | end 62 | else 63 | rd.close 64 | file = Galaxy::Temp.mk_auto_file 65 | dir = Galaxy::Temp.mk_auto_dir 66 | assert File.exists?(file) 67 | assert File.exists?(dir) 68 | ObjectSpace.garbage_collect 69 | assert File.exists?(file) 70 | assert File.exists?(dir) 71 | wr.write "#{file}\t#{dir}" 72 | wr.close 73 | exit 0 74 | end 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /test/test_logger_collector.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/events' 6 | require 'galaxy/host' 7 | require 'galaxy/log' 8 | 9 | class TestLoggerCollector < Test::Unit::TestCase 10 | 11 | # Set your collector hostname here to run tests. 12 | # See http://github.com/ning/collector 13 | COLLECTOR_HOST = nil 14 | 15 | def test_collectors 16 | unless COLLECTOR_HOST.nil? 17 | send_event_via_event_dispatcher 18 | send_encoded_event_via_event_dispatcher 19 | else 20 | assert true 21 | end 22 | end 23 | 24 | def send_event_via_event_dispatcher 25 | glogger = Galaxy::Log::Glogger.new "/tmp/galaxy_unit_test.log", COLLECTOR_HOST, "http://gonsole.test.company.com:1242", "10.15.12.14" 26 | assert_kind_of Galaxy::GalaxyLogEventSender, glogger.event_dispatcher 27 | assert_kind_of Logger, glogger.log 28 | 29 | assert glogger.event_dispatcher.dispatch_debug_log("debug hello from unit test") 30 | assert glogger.event_dispatcher.dispatch_info_log("info hello from unit test") 31 | assert glogger.event_dispatcher.dispatch_warn_log("warn hello from unit test") 32 | assert glogger.event_dispatcher.dispatch_error_log("error hello from unit test") 33 | assert glogger.event_dispatcher.dispatch_fatal_log("fatal hello from unit test") 34 | end 35 | 36 | def send_encoded_event_via_event_dispatcher 37 | glogger = Galaxy::Log::Glogger.new "/tmp/galaxy_unit_test.log", COLLECTOR_HOST, "http://gonsole.test.company.com:1242", "10.15.12.14" 38 | assert_kind_of Galaxy::GalaxyLogEventSender, glogger.event_dispatcher 39 | assert_kind_of Logger, glogger.log 40 | 41 | assert glogger.event_dispatcher.dispatch_error_log("i love spaces") 42 | assert glogger.event_dispatcher.dispatch_error_log("drb://slashespowaa.com") 43 | assert glogger.event_dispatcher.dispatch_error_log("Embedded Thrift: ,sMyThrift,412") 44 | assert glogger.event_dispatcher.dispatch_error_log("$rr0r haZ !@#\$%^&*()_+{}:<>?/.,';#][\/~-`") 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/test_transport.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/transport' 6 | require 'galaxy/console' 7 | 8 | class TestTransport < Test::Unit::TestCase 9 | def test_handler_for 10 | assert Galaxy::Transport.handler_for("druby://xxxx:444").kind_of?(Galaxy::DRbTransport) 11 | assert Galaxy::Transport.handler_for("local://xxxx:444").kind_of?(Galaxy::LocalTransport) 12 | assert Galaxy::Transport.handler_for("http://xxxx:444").kind_of?(Galaxy::HttpTransport) 13 | end 14 | 15 | def test_handler_not_found 16 | assert_raises RuntimeError do 17 | Galaxy::Transport.handler_for("invalid://xxxx:444") 18 | end 19 | end 20 | 21 | def test_drb_publish 22 | url = "druby://localhost:4444" 23 | console = Galaxy::Transport.publish url, "hello" 24 | 25 | obj = Galaxy::Transport.locate url 26 | 27 | assert_equal "hello", obj.to_s 28 | console.stop_service 29 | end 30 | 31 | def test_drb_pool_size 32 | assert_equal 0, DRb::DRbConn::POOL_SIZE 33 | end 34 | 35 | def test_http_publish 36 | console = Galaxy::Console.start({ :host => 'localhost', :log_level => Logger::WARN }) 37 | url = "http://localhost:4441" 38 | 39 | assert_raises TypeError do 40 | Galaxy::Transport.publish url, nil 41 | end 42 | 43 | console_logger = Logger.new(STDOUT) 44 | console_logger.level = Logger::WARN 45 | Galaxy::Transport.publish url, console, console_logger 46 | 47 | announcer = Galaxy::Transport.locate url 48 | o = OpenStruct.new(:host => "localhost", :url => url, :status => "running") 49 | assert_equal Galaxy::ReceiveAnnouncement::ANNOUNCEMENT_RESPONSE_TEXT, announcer.announce(o) 50 | 51 | Galaxy::Transport.unpublish url 52 | console.shutdown 53 | end 54 | 55 | def foo(a) 56 | $foo_called = true 57 | end 58 | 59 | # def test_http_publish_with_callback 60 | # url = "http://localhost:4442" 61 | # Galaxy::Transport.publish url, lambda{|a| foo(a) } 62 | # 63 | # ann = Galaxy::Transport.locate url 64 | # $foo_called = false 65 | # ann.announce("announcement") 66 | # assert $foo_called == true 67 | # 68 | # Galaxy::Transport.unpublish url 69 | # end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | com.ning 6 | galaxy 7 | pom 8 | 3.0.0-SNAPSHOT 9 | galaxy 10 | Galaxy, a lightweight software deployment and management tool 11 | http://github.com/ning/galaxy 12 | 13 | 14 | Apache License 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.html 16 | repo 17 | 18 | 19 | 20 | scm:git:git://github.com/ning/galaxy.git 21 | scm:git:git://github.com/ning/galaxy.git 22 | http://github.com/ning/galaxy/tree/master 23 | 24 | 25 | UTF-8 26 | 27 | 28 | 29 | 30 | org.apache.maven.plugins 31 | maven-assembly-plugin 32 | 33 | 34 | Create Galaxy distribution 35 | 36 | single 37 | 38 | package 39 | 40 | 41 | assembly.xml 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-release-plugin 50 | 2.0-beta-9 51 | 52 | forked-path 53 | 54 | 55 | 56 | 57 | 58 | 59 | pierre 60 | Pierre-Alexandre Meyer 61 | pierre@mouraf.org 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/test_client.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/agent' 6 | require 'galaxy/command' 7 | require 'galaxy/console' 8 | require 'logger' 9 | 10 | class TestClient < Test::Unit::TestCase 11 | 12 | ENV.delete 'GALAXY_CONSOLE' 13 | GALAXY = "ruby -Ilib bin/galaxy" 14 | 15 | Galaxy::Commands.each do |command| 16 | define_method "test_#{command}_usage" do 17 | output = `#{GALAXY} -h #{command} 2>&1` 18 | assert_match("Usage for '#{command}':", output) 19 | end 20 | end 21 | 22 | def test_usage_with_no_arguments 23 | output = `#{GALAXY} 2>&1` 24 | assert_match("Error: Missing command", output) 25 | end 26 | 27 | def test_show_with_no_console 28 | output = `#{GALAXY} show 2>&1` 29 | assert_match("Error: Cannot determine console host", output) 30 | end 31 | 32 | def test_show_console 33 | console = Galaxy::Console.start({ :host => 'localhost' }) 34 | output = `#{GALAXY} show-console -c localhost 2>&1` 35 | assert_match("druby://localhost:4440\thttp://localhost:4442\tlocalhost\t-\t5\n", output) 36 | console.shutdown 37 | end 38 | 39 | def test_show_with_console_from_environment 40 | # console = Galaxy::Console.start({ :host => 'localhost' }) 41 | # output = `GALAXY_CONSOLE=localhost #{GALAXY} show 2>&1` 42 | # assert_match("No agents matching the provided filter(s) were available for show", output) 43 | # console.shutdown 44 | end 45 | 46 | def test_show_with_console_from_command_line 47 | # console = Galaxy::Console.start({ :host => 'localhost' }) 48 | # output = `#{GALAXY} -c localhost show 2>&1` 49 | # assert_match("No agents matching the provided filter(s) were available for show", output) 50 | # console.shutdown 51 | end 52 | 53 | def test_show_with_bad_console 54 | output = `#{GALAXY} -c non-existent-host show 2>&1` 55 | # On Linux, this will be: 56 | # Error: druby://non-existent-host:4440 - #" 57 | #assert_match("Error: druby://non-existent-host:4440 - # 'localhost', :log_level => Logger::WARN }) 63 | agent = Galaxy::Agent.start({ :host => 'localhost', :console => 'localhost', :log_level => Logger::WARN }) 64 | output = `#{GALAXY} -c localhost show 2>&1`.split("\n") 65 | assert_equal(1, output.length) 66 | assert_match("No agents matching the provided filter(s) were available for show", output[0]) 67 | agent.shutdown 68 | console.shutdown 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/test_event.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/events' 6 | require 'logger' 7 | 8 | class TestEvent < Test::Unit::TestCase 9 | 10 | # Set your collector hostname here to run tests. 11 | # See http://github.com/ning/collector 12 | COLLECTOR_HOST = nil 13 | 14 | def test_collectors 15 | unless COLLECTOR_HOST.nil? 16 | send_log 17 | send_raw_event 18 | send_success_event 19 | send_error_event 20 | build_number_string 21 | else 22 | assert true 23 | end 24 | end 25 | 26 | def setup 27 | logger = Logger.new(STDOUT) 28 | logger.level = Logger::WARN 29 | 30 | @galaxy_sender = Galaxy::GalaxyEventSender.new(COLLECTOR_HOST, "http://gonsole.testing.company.net:1242", "127.0.0.1", logger) 31 | @log_sender = Galaxy::GalaxyLogEventSender.new(COLLECTOR_HOST, "http://gonsole.testing.company.net:1242", "127.0.0.1", logger) 32 | 33 | @event = OpenStruct.new( 34 | :host => "prod1.company.com", 35 | :ip => "192.168.12.42", 36 | :url => "drb://goofabr.company.pouet", 37 | :os => "Linux", 38 | :machine => "foobar", 39 | :core_type => "tester", 40 | :config_path => "conf/bar/baz", 41 | :build => "124212", 42 | :status => "running", 43 | :agent_status => "online", 44 | :galaxy_version => "2.5.1", 45 | :user => "John Doe", 46 | :gonsole_url => "http://gonsole.qa.company.net:4442" 47 | ) 48 | end 49 | 50 | # More tests in test_logger.rb 51 | def send_log 52 | assert @log_sender.dispatch_error_log("Hello world!", "program_test") 53 | end 54 | 55 | def send_raw_event 56 | assert @galaxy_sender.send_event(@event) 57 | end 58 | 59 | def send_success_event 60 | assert @galaxy_sender.dispatch_announce_success_event(@event) 61 | end 62 | 63 | def send_error_event 64 | assert @galaxy_sender.dispatch_perform_error_event(@event) 65 | end 66 | 67 | def build_number_string 68 | event = OpenStruct.new( 69 | :agent_status => "online", 70 | :os => "solaris", 71 | :host => "prod1.company.com", 72 | :galaxy_version => "2.6.0.5", 73 | :core_type => "apache", 74 | :machine => "localhost", 75 | :status => "stopped", 76 | :url => "drb://prod2.company.com:4441", 77 | :config_path => "alpha/DEP-1/apache", 78 | :build => "6.1.10", 79 | :ip => "0.1.9.1" 80 | ) 81 | assert @galaxy_sender.dispatch_announce_success_event(event) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/test_agent.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/transport' 6 | require 'galaxy/agent' 7 | require 'galaxy/host' 8 | require 'webrick' 9 | require 'thread' 10 | require 'timeout' 11 | require 'helper' 12 | require 'fileutils' 13 | require 'logger' 14 | 15 | class TestAgent < Test::Unit::TestCase 16 | 17 | def setup 18 | @tempdir = Helper.mk_tmpdir 19 | 20 | @data_dir = File.join(@tempdir, 'data') 21 | @deploy_dir = File.join(@tempdir, 'deploy') 22 | @binaries_base = File.join(@tempdir, 'binaries') 23 | 24 | FileUtils.mkdir_p @data_dir 25 | FileUtils.mkdir_p @deploy_dir 26 | FileUtils.mkdir_p @binaries_base 27 | 28 | system "#{Galaxy::HostUtils.tar} -C #{File.join(File.dirname(__FILE__), "core_package")} -czf #{@binaries_base}/test-1.0-12345.tar.gz ." 29 | 30 | webrick_logger = Logger.new(STDOUT) 31 | webrick_logger.level = Logger::WARN 32 | @server = WEBrick::HTTPServer.new(:Port => 8000, :Logger => webrick_logger) 33 | 34 | # Replies on POST from agent 35 | @server.mount_proc("/") do |request, response| 36 | status, content_type, body = 200, "text/plain", "pong" 37 | response.status = status 38 | response['Content-Type'] = content_type 39 | response.body = body 40 | end 41 | @server.mount("/config", WEBrick::HTTPServlet::FileHandler, File.join(File.dirname(__FILE__), "property_data"), true) 42 | @server.mount("/binaries", WEBrick::HTTPServlet::FileHandler, @binaries_base, true) 43 | 44 | Thread.start do 45 | @server.start 46 | end 47 | 48 | # Note: force 127.0.0.1 not to rely on `hostname` and localhost 49 | @agent = Galaxy::Agent.start({:repository => File.dirname(__FILE__) + "/property_data", 50 | :binaries => @binaries_base, 51 | :data_dir => @data_dir, 52 | :deploy_dir => @deploy_dir, 53 | :log_level => Logger::WARN, 54 | :host => "druby://127.0.0.1:4441", 55 | :console => "http://127.0.0.1:8000" 56 | }) 57 | end 58 | 59 | def teardown 60 | @agent.shutdown 61 | @server.shutdown 62 | FileUtils.rm_rf @tempdir 63 | end 64 | 65 | def test_agent_assign 66 | @agent.become! '/a/b/c' 67 | assert File.exist?(File.join(@deploy_dir, 'current', 'bin')) 68 | end 69 | 70 | def test_agent_perform 71 | @agent.become! '/a/b/c' 72 | assert_nothing_raised do 73 | @agent.perform! 'test-success' 74 | end 75 | end 76 | 77 | def test_agent_perform_failure 78 | @agent.become! '/a/b/c' 79 | assert_raise RuntimeError do 80 | @agent.logger.log.level = Logger::FATAL 81 | # The failure will spit a stacktrace in the log (ERROR) 82 | @agent.perform! 'test-failure' 83 | @agent.logger.log.level = Logger::WARN 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/galaxy/controller.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Galaxy 4 | class Controller 5 | def initialize core_base, config_path, repository_base, binaries_base, log 6 | @core_base = core_base 7 | @config_path = config_path 8 | @repository_base = repository_base 9 | @binaries_base = binaries_base 10 | script = File.join(@core_base, "bin", "control") 11 | if File.exists? script 12 | @script = File.executable?(script) ? script : "/bin/sh #{script}" 13 | else 14 | raise ControllerNotFoundException.new 15 | end 16 | @log = log 17 | end 18 | 19 | def perform! command, args = '' 20 | @log.info "Invoking control script: #{@script} #{command} #{args}" 21 | 22 | begin 23 | output = `#{@script} --base #{@core_base} --binaries #{@binaries_base} --config-path #{@config_path} --repository #{@repository_base} #{command} #{args} 2>&1` 24 | rescue Exception => e 25 | raise ControllerFailureException.new(command, e) 26 | end 27 | 28 | rv = $?.exitstatus 29 | 30 | case rv 31 | when 0 32 | output 33 | when 1 34 | raise CommandFailedException.new(command, output) 35 | when 2 36 | raise UnrecognizedCommandException.new(command, output) 37 | else 38 | raise UnrecognizedResponseCodeException.new(rv, command, output) 39 | end 40 | end 41 | 42 | class ControllerException < RuntimeError; 43 | end 44 | 45 | class ControllerNotFoundException < ControllerException 46 | def initialize 47 | super "No control script available" 48 | end 49 | end 50 | 51 | class ControllerFailureException < ControllerException 52 | def initialize command, exception 53 | super "Unexpected exception executing command '#{command}': #{exception}" 54 | end 55 | end 56 | 57 | class CommandFailedException < ControllerException 58 | def initialize command, output 59 | message = "Command failed: #{command}" 60 | message += ": #{output}" unless output.empty? 61 | super message 62 | end 63 | end 64 | 65 | class UnrecognizedCommandException < ControllerException 66 | def initialize command, output 67 | message = "Unrecognized command: #{command}" 68 | message += ": #{output}" unless output.empty? 69 | super message 70 | end 71 | end 72 | 73 | class UnrecognizedResponseCodeException < ControllerException 74 | def initialize code, command, output 75 | message = "Unrecognized response code #{code} for command #{command}" 76 | message += ": #{output}" unless output.empty? 77 | super message 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/galaxy/deployer.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'tempfile' 3 | require 'logger' 4 | require 'galaxy/host' 5 | 6 | module Galaxy 7 | class Deployer 8 | attr_reader :log 9 | 10 | def initialize deploy_dir, log 11 | @base, @log = deploy_dir, log 12 | end 13 | 14 | # number is the deployment number for this agent 15 | # archive is the path to the binary archive to deploy 16 | # props are the properties (configuration) for the core 17 | def deploy number, archive, config_path, repository_base, binaries_base 18 | core_base = File.join(@base, number.to_s); 19 | FileUtils.mkdir_p core_base 20 | 21 | log.info "deploying #{archive} to #{core_base} with config path #{config_path}" 22 | 23 | command = "#{Galaxy::HostUtils.tar} -C #{core_base} -zxf #{archive}" 24 | begin 25 | Galaxy::HostUtils.system command 26 | rescue Galaxy::HostUtils::CommandFailedError => e 27 | raise "Unable to extract archive: #{e.message}" 28 | end 29 | 30 | xndeploy = "#{core_base}/bin/xndeploy" 31 | unless FileTest.executable? xndeploy 32 | xndeploy = "/bin/sh #{xndeploy}" 33 | end 34 | 35 | command = "#{xndeploy} --base #{core_base} --binaries #{binaries_base} --config-path #{config_path} --repository #{repository_base}" 36 | begin 37 | Galaxy::HostUtils.system command 38 | rescue Galaxy::HostUtils::CommandFailedError => e 39 | raise "Deploy script failed: #{e.message}" 40 | end 41 | return core_base 42 | end 43 | 44 | def activate number 45 | core_base = File.join(@base, number.to_s); 46 | current = File.join(@base, "current") 47 | if File.exists? current 48 | File.unlink(current) 49 | end 50 | FileUtils.ln_sf core_base, current 51 | return core_base 52 | end 53 | 54 | def deactivate number 55 | current = File.join(@base, "current") 56 | if File.exists? current 57 | File.unlink(current) 58 | end 59 | end 60 | 61 | def rollback number 62 | current = File.join(@base, "current") 63 | 64 | if File.exists? current 65 | File.unlink(current) 66 | end 67 | 68 | FileUtils.rm_rf File.join(@base, number.to_s) 69 | 70 | core_base = File.join(@base, (number - 1).to_s) 71 | FileUtils.ln_sf core_base, current 72 | 73 | return core_base 74 | end 75 | 76 | def cleanup_up_to_previous current, db 77 | # Keep the current and last one (for rollback) 78 | (1..(current - 2)).each do |number| 79 | key = number.to_s 80 | 81 | # Remove deployed bits 82 | FileUtils.rm_rf File.join(@base, key) 83 | 84 | # Cleanup the database 85 | db.delete_at key 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/test_deployer.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/deployer' 6 | require 'galaxy/host' 7 | require 'helper' 8 | require 'fileutils' 9 | require 'logger' 10 | 11 | class TestDeployer < Test::Unit::TestCase 12 | 13 | def setup 14 | @core_package = Tempfile.new("package.tgz").path 15 | @bad_core_package = Tempfile.new("bad-package.tgz").path 16 | system %{ 17 | #{Galaxy::HostUtils.tar} -C #{File.join(File.dirname(__FILE__), "core_package")} -czf #{@core_package} . 18 | } 19 | system %{ 20 | #{Galaxy::HostUtils.tar} -C #{File.join(File.dirname(__FILE__), "bad_core_package")} -czf #{@bad_core_package} . 21 | } 22 | @path = Helper.mk_tmpdir 23 | @deployer = Galaxy::Deployer.new @path, Logger.new("/dev/null") 24 | end 25 | 26 | def test_core_base_is_right 27 | core_base = @deployer.deploy "2", @core_package, "/config", "/repository", "/binaries" 28 | assert_equal File.join(@path, "2"), core_base 29 | end 30 | 31 | def test_deployment_dir_is_made 32 | core_base = @deployer.deploy "2", @core_package, "/config", "/repository", "/binaries" 33 | assert FileTest.directory?(core_base) 34 | end 35 | 36 | def test_xndeploy_exists_after_deployment 37 | core_base = @deployer.deploy "2", @core_package, "/config", "/repository", "/binaries" 38 | assert FileTest.exists?(File.join(core_base, "bin", "xndeploy")) 39 | end 40 | 41 | def test_xndeploy_invoked_on_deploy 42 | core_base = @deployer.deploy "2", @core_package, "/config", "/repository", "/binaries" 43 | assert FileTest.exists?(File.join(core_base, "xndeploy_touched_me")) 44 | end 45 | 46 | def test_xndeploy_gets_correct_values 47 | core_base = @deployer.deploy "2", @core_package, "/config", "/repository", "/binaries" 48 | dump = File.open(File.join(core_base, "xndeploy_touched_me")) do |file| 49 | Marshal.load file 50 | end 51 | assert_equal core_base, dump[:deploy_base] 52 | assert_equal "/config", dump[:config_path] 53 | assert_equal "/repository", dump[:repository] 54 | assert_equal "/binaries", dump[:binaries_base] 55 | end 56 | 57 | def test_current_symlink_created 58 | core_base = @deployer.deploy "2", @core_package, "/config", "/repository", "/binaries" 59 | link = File.join(@path, "current") 60 | assert_equal false, FileTest.symlink?(link) 61 | @deployer.activate "2" 62 | assert FileTest.symlink?(link) 63 | assert_equal File.join(@path, "2"), File.readlink(link) 64 | end 65 | 66 | def test_upgrade 67 | first = @deployer.deploy "1", @core_package, "/config", "/repository", "/binaries" 68 | @deployer.activate "1" 69 | assert_equal File.join(@path, "1"), File.readlink(File.join(@path, "current")) 70 | 71 | first = @deployer.deploy "2", @core_package, "/config", "/repository", "/binaries" 72 | @deployer.activate "2" 73 | assert_equal File.join(@path, "2"), File.readlink(File.join(@path, "current")) 74 | end 75 | 76 | def test_bad_archive 77 | assert_raise RuntimeError do 78 | @deployer.deploy "bad", "/etc/hosts", "/config", "/repository", "/binaries" 79 | end 80 | end 81 | 82 | def test_deploy_script_failure 83 | assert_raise RuntimeError do 84 | @deployer.deploy "bad", @bad_core_package, "/config", "/repository", "/binaries" 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/test_logger.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/events' 6 | require 'galaxy/host' 7 | require 'galaxy/log' 8 | 9 | class TestLogger < Test::Unit::TestCase 10 | def setup 11 | end 12 | 13 | def teardown 14 | #puts `cat /tmp/galaxy_unit_test.log*` 15 | FileUtils.rm Dir.glob('/tmp/galaxy_unit_test.log*') 16 | end 17 | 18 | def test_initialize_dummy_event_sender 19 | glogger = Galaxy::Log::Glogger.new "/tmp/galaxy_unit_test.log" 20 | assert_kind_of Galaxy::DummyEventSender, glogger.event_dispatcher 21 | assert_kind_of Logger, glogger.log 22 | end 23 | 24 | def test_initialize_collector 25 | glogger = Galaxy::Log::Glogger.new "/tmp/galaxy_unit_test.log", "http://collector.com" 26 | assert_kind_of Galaxy::GalaxyLogEventSender, glogger.event_dispatcher 27 | assert_kind_of Logger, glogger.log 28 | end 29 | 30 | def test_syslog 31 | # Real-life example that was breaking 2.6.pre2 32 | logger = Galaxy::HostUtils.logger 33 | assert logger.debug("http://prod1.company.com:8080/1?v=GalaxyLog,81267034768000%2C4168954152%2C29654%2Csdebug%2Cs%2CsRegistered%2520Event%2520listener%2520type%2520Galaxy%253A%253AGalaxyEventSender%2520at%2520http%253A%252F%252Fz1205a9.company.com%253A8080%252C%2520sender%2520url%25200.1.9.4%2C&rt=b") 34 | assert logger.info("http://prod1.company.com:8080/1?v=GalaxyLog,81267034768000%2C4168954152%2C29654%2Csdebug%2Cs%2CsRegistered%2520Event%2520listener%2520type%2520Galaxy%253A%253AGalaxyEventSender%2520at%2520http%253A%252F%252Fz1205a9.company.com%253A8080%252C%2520sender%2520url%25200.1.9.4%2C&rt=b") 35 | assert logger.warn("http://prod1.company.com:8080/1?v=GalaxyLog,81267034768000%2C4168954152%2C29654%2Csdebug%2Cs%2CsRegistered%2520Event%2520listener%2520type%2520Galaxy%253A%253AGalaxyEventSender%2520at%2520http%253A%252F%252Fz1205a9.company.com%253A8080%252C%2520sender%2520url%25200.1.9.4%2C&rt=b") 36 | assert logger.error("http://prod1.company.com:8080/1?v=GalaxyLog,81267034768000%2C4168954152%2C29654%2Csdebug%2Cs%2CsRegistered%2520Event%2520listener%2520type%2520Galaxy%253A%253AGalaxyEventSender%2520at%2520http%253A%252F%252Fz1205a9.company.com%253A8080%252C%2520sender%2520url%25200.1.9.4%2C&rt=b") 37 | end 38 | 39 | def test_syslog_raw 40 | logger = Galaxy::HostUtils.logger 41 | assert logger << "foo bar baz" 42 | end 43 | 44 | def test_respect_loglevel_with_event_dispatcher 45 | glogger = Galaxy::Log::Glogger.new "/tmp/galaxy_unit_test.log", "non-existent-host", "http://gonsole.test.company.com:1242", "10.15.12.14" 46 | assert_kind_of Galaxy::GalaxyLogEventSender, glogger.event_dispatcher 47 | assert_kind_of Logger, glogger.log 48 | 49 | # Make sure we don't try to send events at the wrong level 50 | 51 | glogger.log.level = Logger::INFO 52 | assert glogger.debug("debug hello from unit test") 53 | 54 | glogger.log.level = Logger::DEBUG 55 | assert_raise URI::BadURIError do 56 | glogger.debug("debug hello from unit test") 57 | end 58 | end 59 | 60 | def test_syslog_level 61 | logger = Galaxy::HostUtils.logger 62 | 63 | assert_equal Logger::INFO, logger.level 64 | 65 | logger.level = Logger::DEBUG 66 | assert_equal Logger::DEBUG, logger.level 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'fileutils' 3 | 4 | require 'galaxy/filter' 5 | require 'galaxy/temp' 6 | require 'galaxy/transport' 7 | require 'galaxy/version' 8 | require 'galaxy/versioning' 9 | 10 | module Helper 11 | 12 | def Helper.mk_tmpdir 13 | Galaxy::Temp.mk_auto_dir "testing" 14 | end 15 | 16 | class Mock 17 | 18 | def initialize listeners={} 19 | @listeners = listeners 20 | end 21 | 22 | def method_missing sym, *args 23 | f = @listeners[sym] 24 | if f 25 | f.call(*args) 26 | end 27 | end 28 | end 29 | 30 | end 31 | 32 | class MockConsole 33 | def initialize agents 34 | @agents = agents 35 | end 36 | 37 | def shutdown 38 | end 39 | 40 | def agents filters = { :set => :all } 41 | filter = Galaxy::Filter.new filters 42 | @agents.select(&filter) 43 | end 44 | end 45 | 46 | class MockAgent 47 | attr_reader :host, :config_path, :stopped, :started, :restarted 48 | attr_reader :gonsole_url, :env, :version, :type, :url, :agent_status, :proxy, :build, :core_type, :machine, :ip 49 | 50 | def initialize host, env = nil, version = nil, type = nil, gonsole_url=nil 51 | @host = host 52 | @env = env 53 | @version = version 54 | @type = type 55 | @gonsole_url = gonsole_url 56 | @stopped = @started = @restarted = false 57 | 58 | @url = "local://#{host}" 59 | Galaxy::Transport.publish @url, self 60 | 61 | @config_path = nil 62 | @config_path = "/#{env}/#{version}/#{type}" unless env.nil? || version.nil? || type.nil? 63 | @agent_status = 'online' 64 | @status = 'online' 65 | @proxy = Galaxy::Transport.locate(@url) 66 | @build = "1.2.3" 67 | @core_type = 'test' 68 | 69 | @ip = nil 70 | @drb_url = nil 71 | @os = nil 72 | @machine = nil 73 | end 74 | 75 | def shutdown 76 | Galaxy::Transport.unpublish @url 77 | end 78 | 79 | def status 80 | OpenStruct.new( 81 | :host => @host, 82 | :ip => @ip, 83 | :url => @drb_url, 84 | :os => @os, 85 | :machine => @machine, 86 | :core_type => @core_type, 87 | :config_path => @config_path, 88 | :build => @build, 89 | :status => @status, 90 | :agent_status => 'online', 91 | :galaxy_version => Galaxy::Version 92 | ) 93 | end 94 | 95 | def stop! 96 | @stopped = true 97 | status 98 | end 99 | 100 | def start! 101 | @started = true 102 | status 103 | end 104 | 105 | def restart! 106 | @restarted = true 107 | status 108 | end 109 | 110 | def become! path, versioning_policy = Galaxy::Versioning::StrictVersioningPolicy 111 | md = %r!^/([^/]+)/([^/]+)/(.*)$!.match path 112 | new_env, new_version, new_type = md[1], md[2], md[3] 113 | # XXX We don't test the versioning code - but it should go away soon 114 | #raise if @version == new_version 115 | @env = new_env 116 | @version = new_version 117 | @type = new_type 118 | @config_path = "/#{@env}/#{@version}/#{@type}" 119 | status 120 | end 121 | 122 | def update_config! new_version, versioning_policy = Galaxy::Versioning::StrictVersioningPolicy 123 | # XXX We don't test the versioning code - but it should go away soon 124 | #raise if @version == new_version 125 | @version = new_version 126 | @config_path = "/#{@env}/#{@version}/#{@type}" 127 | status 128 | end 129 | 130 | def check_credentials!(command, credentials) 131 | true 132 | end 133 | 134 | def inspect 135 | Galaxy::Client::SoftwareDeploymentReport.new.record_result(self) 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/galaxy/command.rb: -------------------------------------------------------------------------------- 1 | require 'galaxy/agent_utils' 2 | require 'galaxy/parallelize' 3 | require 'galaxy/report' 4 | 5 | module Galaxy 6 | module Commands 7 | @@commands = {} 8 | 9 | def self.register_command command_name, command_class 10 | @@commands[command_name] = command_class 11 | end 12 | 13 | def self.[] command_name 14 | @@commands[command_name] 15 | end 16 | 17 | def self.each 18 | @@commands.keys.sort.each { |command| yield command } 19 | end 20 | 21 | class Command 22 | class << self 23 | attr_reader :name 24 | end 25 | 26 | def self.register_command name 27 | @name = name 28 | Galaxy::Commands.register_command name, self 29 | end 30 | 31 | def self.changes_agent_state 32 | define_method("changes_agent_state") do 33 | true 34 | end 35 | end 36 | 37 | def self.changes_console_state 38 | define_method("changes_console_state") do 39 | true 40 | end 41 | end 42 | 43 | def initialize args = [], options = {} 44 | @args = args 45 | @options = options 46 | @options[:thread_count] ||= 1 47 | end 48 | 49 | def changes_agent_state 50 | false 51 | end 52 | 53 | def changes_console_state 54 | false 55 | end 56 | 57 | def select_agents filter 58 | normalized_filter = normalize_filter(filter) 59 | @options[:console].agents(normalized_filter) 60 | end 61 | 62 | def normalize_filter filter 63 | filter = default_filter if filter.empty? 64 | filter 65 | end 66 | 67 | def default_filter 68 | {:set => :all} 69 | end 70 | 71 | def execute agents 72 | report.start 73 | error_report.start 74 | agents.parallelize(@options[:thread_count]) do |agent| 75 | begin 76 | unless agent.agent_status == 'online' 77 | raise "Agent is not online" 78 | end 79 | Galaxy::AgentUtils::ping_agent(agent) 80 | result = execute_for_agent(agent) 81 | report.record_result result 82 | rescue TimeoutError 83 | error_report.record_result "Error: Timed out communicating with agent #{agent.host}" 84 | rescue Exception => e 85 | error_report.record_result "Error: #{agent.host}: #{e}" 86 | end 87 | end 88 | return report.finish, error_report.finish 89 | end 90 | 91 | def report 92 | @report ||= report_class.new 93 | end 94 | 95 | def report_class 96 | Galaxy::Client::SoftwareDeploymentReport 97 | end 98 | 99 | def error_report 100 | @error_report ||= Galaxy::Client::Report.new 101 | end 102 | end 103 | end 104 | end 105 | 106 | # load and register all commands 107 | Dir.entries("#{File.join(File.dirname(__FILE__))}/commands").each do |entry| 108 | if entry =~ /\.rb$/ 109 | require "galaxy/commands/#{File.basename(entry, '.rb')}" 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/galaxy/proxy_console.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'logger' 3 | require 'galaxy/filter' 4 | require 'galaxy/transport' 5 | 6 | module Galaxy 7 | class ProxyConsole 8 | attr_reader :db 9 | @@max_conn_failures = 3 10 | 11 | def initialize drb_url, console_url, log, log_level, ping_interval 12 | @log = 13 | case log 14 | when "SYSLOG" 15 | Galaxy::HostUtils.logger "galaxy-console" 16 | when "STDOUT" 17 | Logger.new STDOUT 18 | when "STDERR" 19 | Logger.new STDERR 20 | else 21 | Logger.new log 22 | end 23 | @log.level = log_level 24 | @drb_url = drb_url 25 | @ping_interval = ping_interval 26 | @db = {} 27 | @mutex = Mutex.new 28 | @conn_failures = 0 29 | 30 | @console_proxyied_url = console_url 31 | @console_proxied = Galaxy::Transport.locate(console_url) 32 | 33 | Thread.new do 34 | loop do 35 | begin 36 | sleep @ping_interval 37 | synchronize 38 | if @conn_failures > 0 39 | @log.warn "Communication with the master gonsole re-established" 40 | end 41 | # Reset the number of connection failures 42 | @conn_failures = 0 43 | rescue DRb::DRbConnError => e 44 | @conn_failures += 1 45 | @log.warn "Unable to communicate with the master gonsole (#{@conn_failures})" 46 | if @conn_failures >= @@max_conn_failures 47 | @log.error "Number of connection failures reached" 48 | shutdown 49 | exit("Connection Error") 50 | end 51 | retry 52 | rescue Exception => e 53 | @log.warn "Uncaught exception in agent ping thread: #{e}" 54 | @log.warn e.backtrace 55 | abort("Unkown Error") 56 | end 57 | end 58 | end 59 | end 60 | 61 | def synchronize 62 | @mutex.synchronize do 63 | @db = @console_proxied.db 64 | @log.info "Synchronized with master gonsole at #{@console_proxyied_url}" 65 | @log.debug "Got new db: #{@db}" 66 | end 67 | end 68 | 69 | # Remote API 70 | def agents filters = {:set => :all} 71 | filter = Galaxy::Filter.new filters 72 | @mutex.synchronize do 73 | @db.values.select(& filter) 74 | end 75 | end 76 | 77 | # Remote API 78 | def log msg 79 | @log.info msg 80 | end 81 | 82 | def ProxyConsole.start args 83 | host = args[:host] || "localhost" 84 | drb_url = args[:url] || "druby://" + host # DRB transport 85 | drb_url += ":4440" unless drb_url.match ":[0-9]+$" 86 | 87 | console_proxyied_url = args[:console_proxyied_url] || "druby://localhost" 88 | console_proxyied_url += ":4440" unless console_proxyied_url.match ":[0-9]+$" 89 | 90 | console = ProxyConsole.new drb_url, console_proxyied_url, 91 | args[:log] || "STDOUT", 92 | args[:log_level] || Logger::INFO, 93 | args[:ping_interval] || 5 94 | 95 | Galaxy::Transport.publish drb_url, console # DRB transport 96 | console 97 | end 98 | 99 | def shutdown 100 | Galaxy::Transport.unpublish @drb_url 101 | end 102 | 103 | def join 104 | Galaxy::Transport.join @drb_url 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/galaxy/log.rb: -------------------------------------------------------------------------------- 1 | require 'galaxy/events' 2 | require 'galaxy/host' 3 | 4 | module Galaxy 5 | module Log 6 | class Glogger 7 | attr_reader :log, :event_dispatcher 8 | 9 | def initialize(logdev, event_listener = nil, gonsole_url = nil, ip_addr = nil, shift_age = 0, shift_size = 1048576) 10 | @gonsole_url = gonsole_url 11 | 12 | case logdev 13 | when "SYSLOG" 14 | @log = Galaxy::HostUtils.logger "galaxy" 15 | when "STDOUT" 16 | @log = Logger.new(STDOUT, shift_age, shift_size) 17 | when "STDERR" 18 | @log = Logger.new(STDERR, shift_age, shift_size) 19 | else 20 | @log = Logger.new(logdev, shift_age, shift_size) 21 | end 22 | 23 | if event_listener.nil? 24 | @event_dispatcher = Galaxy::DummyEventSender.new() 25 | else 26 | @event_dispatcher = Galaxy::GalaxyLogEventSender.new(event_listener, @gonsole_url, ip_addr, @log) 27 | end 28 | end 29 | 30 | def debug(progname = nil, & block) 31 | @event_dispatcher.dispatch_debug_log(format_message(progname, & block)) if @log.level <= Logger::DEBUG 32 | @log.debug progname, & block 33 | end 34 | 35 | def error(progname = nil, & block) 36 | @event_dispatcher.dispatch_error_log(format_message(progname, & block)) if @log.level <= Logger::ERROR 37 | @log.error progname, & block 38 | end 39 | 40 | def fatal(progname = nil, & block) 41 | @event_dispatcher.dispatch_fatal_log(format_message(progname, & block)) if @log.level <= Logger::FATAL 42 | @log.fatal progname, & block 43 | end 44 | 45 | def info(progname = nil, & block) 46 | @event_dispatcher.dispatch_info_log(format_message(progname, & block)) if @log.level <= Logger::INFO 47 | @log.info progname, & block 48 | end 49 | 50 | def warn(progname = nil, & block) 51 | @event_dispatcher.dispatch_warn_log(format_message(progname, & block)) if @log.level <= Logger::WARN 52 | @log.warn progname, & block 53 | end 54 | 55 | # Pipeline other methods to Logger 56 | # We don't want to define << for instance: when we'll end up having a dedicated Thrift 57 | # schema for logs, we do want to know beforehand the expected formatting. 58 | def method_missing(m, * args, & block) 59 | @log.send m, * args, & block 60 | end 61 | 62 | private 63 | 64 | def format_message(progname, & block) 65 | if block_given? 66 | message = yield 67 | else 68 | message = progname 69 | end 70 | message 71 | end 72 | end 73 | 74 | class LoggerIO < IO 75 | require 'strscan' 76 | 77 | def initialize log, level = :info 78 | @log = log 79 | @level = level 80 | @buffer = "" 81 | end 82 | 83 | def write str 84 | @buffer << str 85 | 86 | scanner = StringScanner.new(@buffer) 87 | 88 | while scanner.scan(/([^\n]*)\n/) 89 | line = scanner[1] 90 | case @level 91 | when :warn 92 | @log.warn line 93 | when :info 94 | @log.info line 95 | when :error 96 | @log.error line 97 | end 98 | end 99 | 100 | @buffer = scanner.rest 101 | end 102 | end 103 | end 104 | end 105 | 106 | if __FILE__ == $0 107 | def a 108 | b 109 | end 110 | 111 | def b 112 | raise "error" 113 | end 114 | 115 | require 'logger' 116 | 117 | log = Logger.new(STDERR) 118 | info = Galaxy::Log::LoggerIO.new log, :info 119 | warn = Galaxy::Log::LoggerIO.new log, :error 120 | $stdout = info 121 | $stderr = warn 122 | 123 | puts "hello world\nbye bye" 124 | 125 | a 126 | end 127 | -------------------------------------------------------------------------------- /lib/galaxy/transport.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | class Transport 3 | @@transports = [] 4 | 5 | def self.register transport 6 | @@transports << transport 7 | end 8 | 9 | def self.locate url, log=nil 10 | handler_for(url).locate url, log 11 | end 12 | 13 | def self.publish url, object, log=nil 14 | handler_for(url).publish url, object, log 15 | end 16 | 17 | def self.unpublish url 18 | handler_for(url).unpublish url 19 | end 20 | 21 | def self.handler_for url 22 | @@transports.select { |t| t.can_handle? url }.first or raise "No handler found for #{url}" 23 | end 24 | 25 | def initialize pattern 26 | @pattern = pattern 27 | end 28 | 29 | def can_handle? url 30 | @pattern =~ url 31 | end 32 | 33 | def self.join url 34 | handler_for(url).join url 35 | end 36 | end 37 | 38 | class DRbTransport < Transport 39 | require 'drb' 40 | 41 | def initialize 42 | super(/^druby:.*/) 43 | @servers = {} 44 | end 45 | 46 | def locate url, log=nil 47 | DRbObject.new_with_uri url 48 | end 49 | 50 | def publish url, object, log=nil 51 | @servers[url] = DRb.start_service url, object 52 | end 53 | 54 | def unpublish url 55 | @servers[url].stop_service 56 | @servers[url] = nil 57 | end 58 | 59 | def join url 60 | @servers[url].thread.join 61 | end 62 | end 63 | 64 | class LocalTransport < Transport 65 | def initialize 66 | super(/^local:/) 67 | @servers = {} 68 | end 69 | 70 | def locate url, log=nil 71 | @servers[url] 72 | end 73 | 74 | def publish url, object, log=nil 75 | @servers[url] = object 76 | end 77 | 78 | def unpublish url 79 | @servers[url] = nil 80 | end 81 | 82 | def join url 83 | raise "Not yet implemented" 84 | end 85 | end 86 | 87 | # This http transport isn't used in Galaxy 2.4, which uses http only for anonucements. However, this code shows 88 | # how announcements could be merged via transport. The unit test for this class shows one-direction communication 89 | # (eg, for announcements). To do two way, servers (eg, locate()) would be needed on both sides. 90 | # Note that the console code assumes that the transport initialize blocks, so the calling code (eg console) waits 91 | # for an explicit 'join'. But the Announcer class used here starts a server without blocking and returns immediately. 92 | # Therefore, explicit join is not necessary. So to use, make the console work like the agent: track the main polling 93 | # thread started in initialize() and kill/join when done. 94 | # 95 | class HttpTransport < Transport 96 | require 'galaxy/announcements' 97 | 98 | def initialize 99 | super(/^http:.*/) 100 | @servers = {} 101 | @log = nil 102 | end 103 | 104 | # get object (ie announce fn) 105 | # - install announce() callback 106 | def locate url, log=nil 107 | #DRbObject.new_with_uri url 108 | HTTPAnnouncementSender.new url, log 109 | end 110 | 111 | # make object available (ie console) 112 | def publish url, obj, log=nil 113 | if !obj.respond_to?('process_get') || !obj.respond_to?('process_post') 114 | raise TypeError.new("#{obj.class.name} doesn't contain 'process_post' and 'process_get' methods") 115 | end 116 | return @servers[url] if @servers[url] 117 | @servers[url] = Galaxy::HTTPServer.new(url, obj) 118 | end 119 | 120 | def unpublish url 121 | @servers[url].shutdown 122 | @servers[url] = nil 123 | end 124 | 125 | def join url 126 | #nop 127 | end 128 | end 129 | end 130 | 131 | Galaxy::Transport.register Galaxy::DRbTransport.new 132 | Galaxy::Transport.register Galaxy::LocalTransport.new 133 | Galaxy::Transport.register Galaxy::HttpTransport.new 134 | 135 | # Disable DRb persistent connections (monkey patch) 136 | module DRb 137 | class DRbConn 138 | remove_const :POOL_SIZE 139 | POOL_SIZE = 0 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /bin/galaxy-console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'optparse' 3 | require 'ostruct' 4 | 5 | tried = false 6 | begin 7 | require 'galaxy/console' 8 | require 'galaxy/proxy_console' 9 | require 'galaxy/config' 10 | require 'galaxy/daemon' 11 | require 'galaxy/host' 12 | require 'galaxy/version' 13 | rescue LoadError 14 | tried = true 15 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 16 | tried ? raise : retry 17 | end 18 | 19 | action = "help" 20 | command_line_options = OpenStruct.new 21 | opts = OptionParser.new do |opts| 22 | opts.banner = "Usage: #{$0} [options]" 23 | 24 | opts.separator " Commands, use just one of these" 25 | opts.on("-s", "--start", "Start the console") { action = "start" } 26 | opts.on("-P", "--start-proxy", "Start the proxy console") { action = "start-proxy" } 27 | opts.on("-k", "--stop", "Stop the console") { action = "stop" } 28 | 29 | opts.separator " Options for Start" 30 | opts.on("-C", "--config FILE", "Configuration file (overrides GALAXY_CONFIG)") do |arg| 31 | command_line_options.config_file = arg 32 | end 33 | opts.on("-i", "--host HOST", "Hostname this console runs on") do |host| 34 | command_line_options.host = host 35 | end 36 | opts.on("-a", "--announcement-url HOST[:PORT]", "Port for Http post announcements") do |ann_host| 37 | command_line_options.announcement_url = ann_host 38 | end 39 | opts.on("-p", "--ping-interval INTERVAL", "How many seconds an agent can be silent before being marked dead") do |interval| 40 | command_line_options.ping_interval = interval 41 | end 42 | opts.on("-f", "--fore", "--foreground", "Run console in the foreground") do 43 | command_line_options.foreground = true 44 | end 45 | opts.on("-Q", "--console-proxied-url URL", "Gonsole to proxy") do |host| 46 | command_line_options.console_proxyied_url = host 47 | end 48 | 49 | 50 | opts.separator " General Options" 51 | opts.on_tail("-l", "--log LOG", "STDOUT | STDERR | SYSLOG | /path/to/file.log") do |log| 52 | command_line_options.log = log 53 | end 54 | opts.on_tail("-L", "--log-level LEVEL", "DEBUG | INFO | WARN | ERROR. Default=INFO") do |level| 55 | command_line_options.log_level = level 56 | end 57 | opts.on_tail("-g", "--console-log FILE", "File agent should rediect stdout and stderr to") do |log| 58 | command_line_options.agent_log = log 59 | end 60 | opts.on_tail("-u", "--user USER", "User to run as") do |arg| 61 | command_line_options.user = arg 62 | end 63 | opts.on("-z", "--event_listener URL", "Which listener to use") do |event_listener| 64 | command_line_options.event_listener = event_listener 65 | end 66 | opts.on_tail("-t", "--test", "Test, displays as -v without doing anything") do 67 | command_line_options.verbose = true 68 | command_line_options.test = true 69 | end 70 | opts.on_tail("-v", "--verbose", "Verbose output") { command_line_options.verbose = true } 71 | opts.on_tail("-V", "--version", "Print the galaxy version and exit") { action = "version" } 72 | opts.on_tail("-h", "--help", "Show this help") { action = "help" } 73 | 74 | begin 75 | opts.parse! ARGV 76 | rescue Exception => msg 77 | puts opts 78 | puts msg 79 | exit 1 80 | end 81 | end 82 | 83 | case action 84 | when "help" 85 | puts opts 86 | exit 87 | 88 | when "version" 89 | puts "Galaxy version #{Galaxy::Version}" 90 | 91 | when "start" 92 | config = Galaxy::ConsoleConfigurator.new(command_line_options).configure 93 | exit if command_line_options.test 94 | if command_line_options.foreground 95 | console = Galaxy::Console.start config 96 | console.join 97 | else 98 | Galaxy::Daemon.start('galaxy-console', config[:pid_file], config[:user]) do 99 | console = Galaxy::Console.start(config) 100 | console.join 101 | end 102 | end 103 | when "start-proxy" 104 | config = Galaxy::ConsoleConfigurator.new(command_line_options).configure 105 | exit if command_line_options.test 106 | if command_line_options.foreground 107 | console = Galaxy::ProxyConsole.start config 108 | console.join 109 | else 110 | Galaxy::Daemon.start('galaxy-proxy-console', config[:pid_file], config[:user]) do 111 | console = Galaxy::ProxyConsole.start(config) 112 | console.join 113 | end 114 | end 115 | when "stop" 116 | config = Galaxy::ConsoleConfigurator.new(command_line_options).configure 117 | begin 118 | Galaxy::Daemon.kill_daemon(config[:pid_file]) 119 | rescue Exception => e 120 | abort("Error: #{e}") 121 | end 122 | 123 | end 124 | -------------------------------------------------------------------------------- /bin/galaxy-agent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | tried = false 4 | begin 5 | require 'galaxy/agent' 6 | require 'galaxy/daemon' 7 | require 'galaxy/config' 8 | require 'galaxy/version' 9 | rescue LoadError 10 | tried = true 11 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 12 | tried ? raise : retry 13 | end 14 | require 'optparse' 15 | require 'ostruct' 16 | 17 | action = "help" 18 | command_line_options = OpenStruct.new 19 | opts = OptionParser.new do |opts| 20 | opts.banner = "Usage: #{$0} [options]" 21 | 22 | opts.separator " Commands, use just one of these" 23 | opts.on("-s", "--start", "Start the agent") { action = "start" } 24 | opts.on("-k", "--stop", "Stop the agent") { action = "stop" } 25 | 26 | opts.separator " Options for Start" 27 | opts.on("-C", "--config FILE", "Configuration file (overrides GALAXY_CONFIG)") do |arg| 28 | command_line_options.config_file = arg 29 | end 30 | opts.on("-i", "--host HOST[:PORT]", "Hostname this agent manages (default localhost)") do |host| 31 | command_line_options.host = host 32 | end 33 | opts.on("-m", "--machine MACHINE", "Physical machine where the agent lives (overrides -M)") do |machine| 34 | command_line_options.machine = machine 35 | end 36 | opts.on("-M", "--machine-file FILE", "Filename containing the physical machine name") do |machine_file| 37 | command_line_options.machine_file = machine_file 38 | end 39 | opts.on("-c", "--console ['http://'|'druby://']HOST[:PORT]", "Hostname where the console is listening") do |console| 40 | command_line_options.console = console 41 | end 42 | opts.on("-r", "--repository URL", "Base URL for the repository") do |repo| 43 | command_line_options.repository = repo 44 | end 45 | opts.on("-b", "--binaries URL", "Base URL for the binary archive") do |bin| 46 | command_line_options.binaries = bin 47 | end 48 | opts.on("-d", "--deploy-to DIR", "Directory where to make deployments") do |path| 49 | command_line_options.deploy_dir = path 50 | end 51 | opts.on("-x", "--data DIR", "Directory for the agent's database") do |path| 52 | command_line_options.data_dir = path 53 | end 54 | opts.on("-f", "--fore", "--foreground", "Run agent in the foreground") do 55 | command_line_options.foreground = true 56 | end 57 | opts.on("-a", "--announce-interval INTERVAL", "How frequently (in seconds) the agent should announce") do |interval| 58 | command_line_options.announce_interval = interval 59 | end 60 | 61 | opts.separator " General Options" 62 | opts.on_tail("-l", "--log LOG", "STDOUT | STDERR | SYSLOG | /path/to/file.log") do |log| 63 | command_line_options.log = log 64 | end 65 | opts.on_tail("-L", "--log-level LEVEL", "DEBUG | INFO | WARN | ERROR. Default=INFO") do |level| 66 | command_line_options.log_level = level 67 | end 68 | opts.on_tail("-u", "--user USER", "User to run as") do |arg| 69 | command_line_options.user = arg 70 | end 71 | opts.on("-z", "--event_listener URL", "Which listener to use") do |event_listener| 72 | command_line_options.event_listener = event_listener 73 | end 74 | opts.on_tail("-g", "--agent-log FILE", "File agent should rediect stdout and stderr to") do |log| 75 | command_line_options.agent_log = log 76 | end 77 | 78 | opts.on_tail("-t", "--test", "Test, displays as -v without doing anything") do 79 | command_line_options.verbose = true 80 | command_line_options.test = true 81 | end 82 | opts.on_tail("-v", "--verbose", "Verbose output") { command_line_options.verbose = true } 83 | opts.on_tail("-V", "--version", "Print the galaxy version and exit") { action = "version" } 84 | opts.on_tail("-h", "--help", "Show this help") { action = "help" } 85 | 86 | 87 | begin 88 | opts.parse! ARGV 89 | rescue Exception => msg 90 | puts opts 91 | puts msg 92 | exit 1 93 | end 94 | end 95 | 96 | case action 97 | when "help" 98 | puts opts 99 | 100 | when "version" 101 | puts "Galaxy version #{Galaxy::Version}" 102 | 103 | when "start" 104 | config = Galaxy::AgentConfigurator.new(command_line_options).configure 105 | exit if command_line_options.test 106 | if command_line_options.foreground 107 | agent = Galaxy::Agent.start config 108 | agent.join 109 | else 110 | Galaxy::Daemon.start('galaxy-agent', config[:pid_file], config[:user]) do 111 | agent = Galaxy::Agent.start config 112 | agent.join 113 | end 114 | end 115 | 116 | when "stop" 117 | config = Galaxy::AgentConfigurator.new(command_line_options).configure 118 | begin 119 | Galaxy::Daemon.kill_daemon(config[:pid_file]) 120 | rescue Exception => e 121 | abort("Error: #{e}") 122 | end 123 | 124 | end 125 | -------------------------------------------------------------------------------- /test/performance/src/LoadTest.java: -------------------------------------------------------------------------------- 1 | import java.text.MessageFormat; 2 | import java.util.concurrent.BlockingQueue; 3 | import java.util.concurrent.LinkedBlockingQueue; 4 | 5 | import org.apache.http.HttpVersion; 6 | import org.apache.http.client.HttpClient; 7 | import org.apache.http.client.methods.HttpPost; 8 | import org.apache.http.entity.StringEntity; 9 | import org.apache.http.impl.client.DefaultHttpClient; 10 | import org.apache.http.params.BasicHttpParams; 11 | import org.apache.http.params.HttpConnectionParams; 12 | import org.apache.http.params.HttpParams; 13 | import org.apache.http.params.HttpProtocolParams; 14 | 15 | public class LoadTest { 16 | static BlockingQueue queue = new LinkedBlockingQueue(); 17 | 18 | static Thread createWorker(int workerId, final int agentId, final int totalRequests, final String url) { 19 | final MessageFormat messageFormat = new MessageFormat( 20 | "--- !ruby/object:OpenStruct\ntable:\n " 21 | + ":agent_status: online\n " + ":config_path: a/b/c\n " 22 | + ":host: z{0}.company.com\n " 23 | + ":url: druby://z{1}.company.com:4441\n " 24 | + ":core_type: benchmark\n " + ":eventType: galaxy\n " 25 | + ":machine: m{2}.company.com\n " + ":galaxy_version: 2.6.4\n " 26 | + ":os: linux\n " + ":galaxy_event_type: success"); 27 | 28 | final String name = "Worker " + workerId; 29 | Thread thread = new Thread(name) { 30 | @Override 31 | public void run() { 32 | boolean success; 33 | for (int i = 0, j; i < totalRequests; i++) { 34 | success = false; 35 | try { 36 | // System.err.println(this.getName() + " : #" + (i + 1)); 37 | System.out.print("."); 38 | HttpParams httpParams = new BasicHttpParams(); 39 | HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1); 40 | HttpConnectionParams.setSoTimeout(httpParams, new Integer(30000)); 41 | HttpConnectionParams.setConnectionTimeout(httpParams, new Integer(30000)); 42 | HttpClient httpClient = new DefaultHttpClient(httpParams); 43 | HttpPost httpPost = new HttpPost(url); 44 | j = agentId + i; 45 | Object[] input = new Object[] { j , j, j }; 46 | StringEntity entity = new StringEntity(messageFormat.format(input)); 47 | httpPost.setEntity(entity); 48 | httpClient.execute(httpPost); 49 | httpClient.getConnectionManager().shutdown(); 50 | success = true; 51 | } catch (Exception e) { 52 | // System.err.println(e.getLocalizedMessage()); 53 | } 54 | finally { 55 | if (!success) { 56 | String msg = name + " #" + (i + 1); 57 | // System.err.println("retrying worker " + msg); 58 | try { 59 | queue.put(msg); 60 | } catch (InterruptedException e) { 61 | e.printStackTrace(); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | }; 68 | return thread; 69 | } 70 | 71 | public static void main(String[] args) throws Exception { 72 | int agents = 1400; 73 | int concurrentRequests = 70; 74 | String gonsoleUrl = "http://localhost:4442"; 75 | if (args.length == 1 && args[0].endsWith("-h")) { 76 | System.out.println("java -jar galaxy-loader.jar -g -a <# of agents> -c <# concurrent requests>"); 77 | System.exit(0); 78 | } 79 | for (int i = 1; i < args.length; i += 2) { 80 | if("-g".equals(args[i - 1].trim())) { 81 | gonsoleUrl = args[i].trim(); 82 | } 83 | else if("-a".equals(args[i - 1].trim())) { 84 | agents = Integer.parseInt(args[i].trim()); 85 | } 86 | else if("-c".equals(args[i - 1].trim())) { 87 | concurrentRequests = Integer.parseInt(args[i].trim()); 88 | } 89 | } 90 | 91 | System.out.println("gonsole = " + gonsoleUrl + ", agents = " + agents + ", concurrent requests = " + concurrentRequests); 92 | int totalWorkers = concurrentRequests; 93 | int totalRequests = (int) Math.round(agents / (double) concurrentRequests); 94 | Thread[] workers = new Thread[totalWorkers]; 95 | for (int i = 0, j; i < totalWorkers; i++) { 96 | j = (i + 1) * totalRequests; 97 | workers[i] = createWorker((i + 1), j, totalRequests, gonsoleUrl); 98 | } 99 | 100 | long time = System.currentTimeMillis(); 101 | for (Thread thread : workers) { 102 | thread.start(); 103 | } 104 | 105 | for (Thread thread : workers) { 106 | try { 107 | thread.join(); 108 | } catch (InterruptedException e) { 109 | } 110 | } 111 | time = System.currentTimeMillis() - time; 112 | // for (String msg : queue) { 113 | // System.out.println(msg); 114 | // } 115 | System.out.println("\ntotal failures: " + queue.size()); 116 | System.out.println("total run time: " + (time / (1000)) + " sec"); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/galaxy/daemon.rb: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # daemonize.rb is a slightly modified version of daemonize.rb was # 3 | # from the Daemonize Library written by Travis Whitton # 4 | # for details see http://grub.ath.cx/daemonize/ # 5 | ############################################################################### 6 | 7 | require 'galaxy/host' 8 | require 'fileutils' 9 | 10 | module Galaxy 11 | module Daemonize 12 | VERSION = "0.1.2" 13 | 14 | # Try to fork if at all possible retrying every 5 sec if the 15 | # maximum process limit for the system has been reached 16 | def safefork 17 | tryagain = true 18 | 19 | while tryagain 20 | tryagain = false 21 | begin 22 | if pid = fork 23 | return pid 24 | end 25 | rescue Errno::EWOULDBLOCK 26 | sleep 5 27 | tryagain = true 28 | end 29 | end 30 | end 31 | 32 | # This method causes the current running process to become a daemon 33 | # If closefd is true, all existing file descriptors are closed 34 | def daemonize(log = nil, oldmode=0, closefd=false) 35 | srand # Split rand streams between spawning and daemonized process 36 | safefork and exit # Fork and exit from the parent 37 | 38 | # Detach from the controlling terminal 39 | unless sess_id = Process.setsid 40 | raise 'Cannot detach from controlled terminal' 41 | end 42 | 43 | # Prevent the possibility of acquiring a controlling terminal 44 | if oldmode.zero? 45 | trap 'SIGHUP', 'IGNORE' 46 | exit if pid = safefork 47 | end 48 | 49 | Dir.chdir "/" # Release old working directory 50 | File.umask 0000 # Insure sensible umask 51 | 52 | if closefd 53 | # Make sure all file descriptors are closed 54 | ObjectSpace.each_object(IO) do |io| 55 | unless [STDIN, STDOUT, STDERR].include?(io) 56 | io.close rescue nil 57 | end 58 | end 59 | end 60 | 61 | log ||= "/dev/null" 62 | 63 | STDIN.reopen "/dev/null" # Free file descriptors and 64 | STDOUT.reopen log, "a" # point them somewhere sensible 65 | STDERR.reopen STDOUT # STDOUT/STDERR should go to a logfile 66 | return oldmode ? sess_id : 0 # Return value is mostly irrelevant 67 | end 68 | end 69 | 70 | class Daemon 71 | include Galaxy::Daemonize 72 | 73 | def self.pid_for pid_file 74 | begin 75 | File.open(pid_file) do |f| 76 | f.gets 77 | end.to_i 78 | rescue Errno::ENOENT 79 | return nil 80 | end 81 | end 82 | 83 | def self.kill_daemon pid_file 84 | pid = pid_for(pid_file) 85 | if pid.nil? 86 | raise "Cannot determine process id: Pid file #{pid_file} not found" 87 | end 88 | begin 89 | Process.kill("TERM", pid) 90 | rescue Errno::ESRCH 91 | raise "Cannot kill process id #{pid}: Not running" 92 | rescue Errno::EPERM 93 | raise "Cannot kill process id #{pid}: Permission denied" 94 | end 95 | end 96 | 97 | def self.daemon_running? pid_file 98 | pid = pid_for(pid_file) 99 | if pid.nil? 100 | return false 101 | end 102 | begin 103 | Process.kill(0, pid) 104 | rescue Errno::ESRCH 105 | return false 106 | rescue Errno::EPERM 107 | return true 108 | end 109 | return true 110 | end 111 | 112 | def initialize & block 113 | @block = block 114 | end 115 | 116 | def go pid_file, log 117 | daemonize(log) 118 | 119 | File.open(pid_file, "w", 0644) do |f| 120 | f.write Process.pid 121 | end 122 | trap "TERM" do 123 | FileUtils.rm_f pid_file 124 | exit 0 125 | end 126 | trap "KILL" do 127 | FileUtils.rm_f pid_file 128 | exit 0 129 | end 130 | @block.call 131 | end 132 | 133 | def self.start name, pid_file, user = nil, log = nil, & block 134 | Galaxy::HostUtils::switch_user(user) unless user.nil? 135 | if daemon_running?(pid_file) 136 | pid = pid_for(pid_file) 137 | abort("Error: #{name} is already running as pid #{pid}") 138 | end 139 | Daemon.new(& block).go(pid_file, log) 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/galaxy/report.rb: -------------------------------------------------------------------------------- 1 | module Galaxy 2 | module Client 3 | class Report 4 | def initialize 5 | @buffer = "" 6 | end 7 | 8 | def start 9 | end 10 | 11 | def record_result result 12 | @buffer += sprintf(format_string, * format_result(result)) 13 | end 14 | 15 | def finish 16 | @buffer.length > 0 ? @buffer : nil 17 | end 18 | 19 | private 20 | 21 | def format_string 22 | "%s\n" 23 | end 24 | 25 | def format_result result 26 | [result] 27 | end 28 | end 29 | 30 | class ConsoleStatusReport < Report 31 | private 32 | 33 | def format_string 34 | "%s\t%s\t%s\t%s\t%s\n" 35 | end 36 | 37 | def format_field field 38 | field ? field : '-' 39 | end 40 | 41 | def format_result result 42 | [ 43 | format_field(result.drb_url), 44 | format_field(result.http_url), 45 | format_field(result.host), 46 | format_field(result.env), 47 | format_field(result.ping_interval), 48 | ] 49 | end 50 | end 51 | 52 | class AgentStatusReport < Report 53 | private 54 | 55 | def format_string 56 | STDOUT.tty? ? "%-20s %-8s %-10s\n" : "%s\t%s\t%s\n" 57 | end 58 | 59 | def format_field field 60 | field ? field : '-' 61 | end 62 | 63 | def format_result result 64 | [ 65 | format_field(result.host), 66 | format_field(result.agent_status), 67 | format_field(result.galaxy_version), 68 | ] 69 | end 70 | end 71 | 72 | class SoftwareDeploymentReport < Report 73 | private 74 | 75 | def format_string 76 | STDOUT.tty? ? "%-20s %-45s %-10s %-15s %-20s %-20s %-15s %-8s\n" : "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" 77 | end 78 | 79 | def format_field field 80 | field ? field : '-' 81 | end 82 | 83 | def format_result result 84 | [ 85 | format_field(result.host), 86 | format_field(result.config_path), 87 | format_field(result.status), 88 | format_field(result.build), 89 | format_field(result.core_type), 90 | format_field(result.machine), 91 | format_field(result.ip), 92 | format_field(result.agent_status), 93 | ] 94 | end 95 | end 96 | 97 | class CoreStatusReport < Report 98 | private 99 | 100 | def format_string 101 | STDOUT.tty? ? "%-20s %-45s %-10s %-15s %-20s %-14s\n" : "%s\t%s\t%s\t%s\t%s\t%s\n" 102 | end 103 | 104 | def format_field field 105 | field ? field : '-' 106 | end 107 | 108 | def format_result result 109 | [ 110 | format_field(result.host), 111 | format_field(result.config_path), 112 | format_field(result.status), 113 | format_field(result.build), 114 | format_field(result.core_type), 115 | format_field(result.last_start_time), 116 | ] 117 | end 118 | end 119 | 120 | class LocalSoftwareDeploymentReport < Report 121 | private 122 | 123 | def format_string 124 | STDOUT.tty? ? "%-45s %-10s %-15s %-20s %s\n" : "%s\t%s\t%s\t%s\t%s\n" 125 | end 126 | 127 | def format_field field 128 | field ? field : '-' 129 | end 130 | 131 | def format_result result 132 | [ 133 | format_field(result.config_path), 134 | format_field(result.status), 135 | format_field(result.build), 136 | format_field(result.core_type), 137 | "autostart=#{result.auto_start}", 138 | ] 139 | end 140 | end 141 | 142 | class CommandOutputReport < Report 143 | def initialize 144 | super 145 | @software_deployment_report = SoftwareDeploymentReport.new 146 | end 147 | 148 | def record_result result 149 | @software_deployment_report.record_result(result[0]) 150 | host, output = format_result(result) 151 | output.split("\n").each { |line| @buffer += sprintf(format_string, host, line) } 152 | end 153 | 154 | private 155 | 156 | def format_string 157 | "%-20s %s\n" 158 | end 159 | 160 | def format_result result 161 | status, output = result 162 | return "#{status.host}:", output 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/galaxy/host.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'syslog' 3 | require 'logger' 4 | 5 | module Galaxy 6 | module HostUtils 7 | def HostUtils.logger ident="galaxy" 8 | @logger ||= begin 9 | log = Syslog.open ident, Syslog::LOG_PID | Syslog::LOG_CONS, Syslog::LOG_LOCAL7 10 | class << log 11 | attr_reader :level 12 | # The interface is busted between Logger and Syslog. The later expects a format string. The former a string. 13 | # This was breaking logging in the event code in production (we log the url, which contains escaped characters). 14 | # Poor man's solution: assume the message is not a format string if we pass only one argument. 15 | # 16 | alias_method :unsafe_debug, :debug 17 | 18 | def debug * args 19 | args.length == 1 ? unsafe_debug(safe_format(args[0])) : unsafe_debug(* args) 20 | end 21 | 22 | alias_method :unsafe_info, :info 23 | 24 | def info * args 25 | args.length == 1 ? unsafe_info(safe_format(args[0])) : unsafe_info(* args) 26 | end 27 | 28 | def warn * args 29 | args.length == 1 ? warning(safe_format(args[0])) : warning(* args) 30 | end 31 | 32 | def error * args 33 | args.length == 1 ? err(safe_format(args[0])) : err(* args) 34 | end 35 | 36 | # set log levels from standard Logger levels 37 | def level=(val) 38 | @level = val 39 | case val # Note that there are other log levels: LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_NOTICE 40 | when Logger::ERROR 41 | Syslog.mask = Syslog::LOG_UPTO(Syslog::LOG_ERR) 42 | when Logger::WARN 43 | Syslog.mask = Syslog::LOG_UPTO(Syslog::LOG_WARNING) 44 | when Logger::DEBUG 45 | Syslog.mask = Syslog::LOG_UPTO(Syslog::LOG_DEBUG) 46 | when Logger::INFO 47 | Syslog.mask = Syslog::LOG_UPTO(Syslog::LOG_INFO) 48 | end 49 | end 50 | 51 | def safe_format(arg) 52 | return arg.gsub("%", "%%") 53 | end 54 | 55 | # The logger implementation dump msg directly, without appending any loglevel. We need one though for Syslog. 56 | # By default, logger(1) uses ``user.notice''. Do the same here. 57 | def <<(msg) 58 | notice(msg) 59 | end 60 | end 61 | log.level = Logger::INFO 62 | log 63 | end 64 | end 65 | 66 | # Returns the name of the user that invoked the command 67 | # 68 | # This implementation tries +who am i+, available on some unix platforms, to check the owner of the controlling terminal, 69 | # which preserves ownership across +su+ and +sudo+. Failing that, the environment is checked for a +USERNAME+ or +USER+ variable. 70 | # Finally, the system password database is consulted. 71 | def HostUtils.shell_user 72 | guesses = [] 73 | guesses << `who am i 2> /dev/null`.split[0] 74 | guesses << ENV['USERNAME'] 75 | guesses << ENV['USER'] 76 | guesses << Etc.getpwuid(Process.uid).name 77 | guesses.first { |guess| notguess.nil? and notguess.empty? } 78 | end 79 | 80 | def HostUtils.avail_path 81 | @avail_path ||= begin 82 | directories = %w{ /usr/local/var/galaxy /var/galaxy /var/tmp /tmp } 83 | directories.find { |dir| FileTest.writable? dir } 84 | end 85 | end 86 | 87 | def HostUtils.tar 88 | @tar ||= begin 89 | unless `which gtar` =~ /^no gtar/ || `which gtar`.length == 0 90 | "gtar" 91 | else 92 | "tar" 93 | end 94 | end 95 | end 96 | 97 | def HostUtils.switch_user user 98 | pwent = Etc::getpwnam(user) 99 | uid, gid = pwent.uid, pwent.gid 100 | if Process.gid != gid or Process.uid != uid 101 | Process::GID::change_privilege(gid) 102 | Process::initgroups(user, gid) 103 | Process::UID::change_privilege(uid) 104 | end 105 | if Process.gid != gid or Process.uid != uid 106 | abort("Error: unable to switch user to #{user}") 107 | end 108 | end 109 | 110 | class CommandFailedError < Exception 111 | attr_reader :command, :exitstatus, :output 112 | 113 | def initialize command, exitstatus, output 114 | @command = command 115 | @exitstatus = exitstatus 116 | @output = output 117 | end 118 | 119 | def message 120 | "Command '#{@command}' exited with status code #{@exitstatus} and output: #{@output}".chomp() 121 | end 122 | end 123 | 124 | # An alternative to Kernel.system that invokes a command, raising an exception containing 125 | # the command's stdout and stderr if the command returns a status code other than 0 126 | def HostUtils.system command 127 | output = IO.popen("#{command} 2>&1") { |io| io.readlines } 128 | unless $?.success? 129 | raise CommandFailedError.new(command, $?.exitstatus, output) 130 | end 131 | output 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'fileutils' 3 | require 'tmpdir' 4 | require 'rake' 5 | require 'rake/testtask' 6 | require 'rake/clean' 7 | require 'rake/gempackagetask' 8 | require 'lib/galaxy/version' 9 | begin 10 | require 'rcov/rcovtask' 11 | $RCOV_LOADED = true 12 | rescue LoadError 13 | $RCOV_LOADED = false 14 | puts "Unable to load rcov" 15 | end 16 | 17 | THIS_FILE = File.expand_path(__FILE__) 18 | PWD = File.dirname(THIS_FILE) 19 | RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) 20 | 21 | PACKAGE_NAME = 'galaxy' 22 | PACKAGE_VERSION = Galaxy::Version 23 | GEM_VERSION = PACKAGE_VERSION.split('-')[0] 24 | 25 | task :default => [:test] 26 | 27 | task :install do 28 | sitelibdir = Config::CONFIG["sitelibdir"] 29 | cd 'lib' do 30 | for file in Dir["galaxy/*.rb", "galaxy/commands/*.rb" ] 31 | d = File.join(sitelibdir, file) 32 | mkdir_p File.dirname(d) 33 | install(file, d) 34 | end 35 | end 36 | 37 | bindir = Config::CONFIG["bindir"] 38 | cd 'bin' do 39 | for file in ["galaxy", "galaxy-agent", "galaxy-console" ] 40 | d = File.join(bindir, file) 41 | mkdir_p File.dirname(d) 42 | install(file, d) 43 | end 44 | end 45 | end 46 | 47 | 48 | Rake::TestTask.new("test") do |t| 49 | t.pattern = 'test/test*.rb' 50 | t.libs << 'test' 51 | t.warning = true 52 | end 53 | 54 | if $RCOV_LOADED 55 | Rcov::RcovTask.new do |t| 56 | t.pattern = 'test/test*.rb' 57 | t.libs << 'test' 58 | t.rcov_opts = ['--exclude', 'gems/*', '--text-report'] 59 | end 60 | end 61 | 62 | Rake::PackageTask.new(PACKAGE_NAME, PACKAGE_VERSION) do |p| 63 | p.tar_command = 'gtar' if RUBY_PLATFORM =~ /solaris/ 64 | p.need_tar = true 65 | p.package_files.include(["lib/galaxy/**/*.rb", "bin/*"]) 66 | end 67 | 68 | spec = Gem::Specification.new do |s| 69 | s.name = PACKAGE_NAME 70 | s.version = GEM_VERSION 71 | s.author = "Ning, Inc." 72 | s.email = "pierre@ning.com" 73 | s.homepage = "http://github.com/ning/galaxy" 74 | s.platform = Gem::Platform::RUBY 75 | s.summary = "Galaxy is a lightweight software deployment and management tool." 76 | s.files = FileList["lib/galaxy/**/*.rb", "bin/*"] 77 | s.executables = FileList["galaxy-agent", "galaxy-console", "galaxy"] 78 | s.require_path = "lib" 79 | s.add_dependency("fileutils", ">= 0.7") 80 | s.add_dependency("json", ">= 1.5.1") 81 | s.add_dependency("mongrel", ">= 1.1.5") 82 | s.add_dependency("rcov", ">= 0.9.9") 83 | end 84 | 85 | Rake::GemPackageTask.new(spec) do |pkg| 86 | pkg.need_zip = false 87 | pkg.tar_command = 'gtar' if RUBY_PLATFORM =~ /solaris/ 88 | pkg.need_tar = true 89 | end 90 | 91 | namespace :run do 92 | desc "Run a Gonsole locally" 93 | task :gonsole do 94 | # Note that -i localhost is needed. Otherwise the DRb server will bind to the 95 | # hostname, which can be as ugly as "Pierre-Alexandre-Meyers-MacBook-Pro.local" 96 | system(RUBY, "-I", File.join(PWD, "lib"), 97 | File.join(PWD, "bin", "galaxy-console"), "--start", 98 | "-i", "localhost", 99 | "--ping-interval", "10", "-f", "-l", "STDOUT", "-L", "DEBUG", "-v") 100 | end 101 | 102 | desc "Run a Gagent locally" 103 | task :gagent do 104 | system(RUBY, "-I", File.join(PWD, "lib"), 105 | File.join(PWD, "bin", "galaxy-agent"), "--start", 106 | "-i", "localhost", "-c", "localhost", 107 | "-r", "http://localhost/config/trunk/qa", 108 | "-b", "http://localhost/binaries", 109 | "-d", "/tmp/deploy", "-x", "/tmp/extract", 110 | "--announce-interval", "10", "-f", "-l", "STDOUT", "-L", "DEBUG", "-v") 111 | end 112 | end 113 | 114 | desc "Build a Gem with the full version number" 115 | task :versioned_gem => :gem do 116 | gem_version = PACKAGE_VERSION.split('-')[0] 117 | if gem_version != PACKAGE_VERSION 118 | FileUtils.mv("pkg/#{PACKAGE_NAME}-#{gem_version}.gem", "pkg/#{PACKAGE_NAME}-#{PACKAGE_VERSION}.gem") 119 | end 120 | end 121 | 122 | namespace :package do 123 | desc "Build an RPM package" 124 | task :rpm => :versioned_gem do 125 | build_dir = "/tmp/galaxy-package" 126 | rpm_dir = "/tmp/galaxy-rpm" 127 | rpm_version = PACKAGE_VERSION 128 | rpm_version += "-final" unless rpm_version.include?('-') 129 | 130 | FileUtils.rm_rf(build_dir) 131 | FileUtils.mkdir_p(build_dir) 132 | FileUtils.rm_rf(rpm_dir) 133 | FileUtils.mkdir_p(rpm_dir) 134 | 135 | `rpmbuild --target=noarch -v --define "_builddir ." --define "_rpmdir #{rpm_dir}" -bb build/rpm/galaxy.spec` || raise("Failed to create package") 136 | # You can tweak the rpm as follow: 137 | #`rpmbuild --target=noarch -v --define "_gonsole_url gonsole.company.com" --define "_gepo_url http://gepo.company.com/config/trunk/prod" --define "_builddir ." --define "_rpmdir #{rpm_dir}" -bb build/rpm/galaxy.spec` || raise("Failed to create package") 138 | 139 | FileUtils.cp("#{rpm_dir}/noarch/#{PACKAGE_NAME}-#{rpm_version}.noarch.rpm", "pkg/#{PACKAGE_NAME}-#{rpm_version}.noarch.rpm") 140 | FileUtils.rm_rf(build_dir) 141 | FileUtils.rm_rf(rpm_dir) 142 | end 143 | 144 | desc "Build a Sun package" 145 | task :sunpkg => :versioned_gem do 146 | build_dir = "#{Dir.tmpdir}/galaxy-package" 147 | source_dir = File.dirname(__FILE__) 148 | 149 | FileUtils.rm_rf(build_dir) 150 | FileUtils.mkdir_p(build_dir) 151 | FileUtils.cp_r("#{source_dir}/build/sun/.", build_dir) 152 | FileUtils.cp("#{source_dir}/pkg/#{PACKAGE_NAME}-#{PACKAGE_VERSION}.gem", "#{build_dir}/#{PACKAGE_NAME}.gem") 153 | FileUtils.mkdir_p("#{build_dir}/root/lib/svc/method") 154 | 155 | # Expand version tokens 156 | `ruby -pi -e "gsub('\#{PACKAGE_VERSION}', '#{PACKAGE_VERSION}'); gsub('\#{GEM_VERSION}', '#{GEM_VERSION}')" #{build_dir}/*` 157 | 158 | # Build the package 159 | `cd #{build_dir} && pkgmk -r root -d .` || raise("Failed to create package") 160 | `cd #{build_dir} && pkgtrans -s . #{PACKAGE_NAME}.pkg galaxy` || raise("Failed to translate package") 161 | 162 | FileUtils.cp("#{build_dir}/#{PACKAGE_NAME}.pkg", "#{source_dir}/pkg/#{PACKAGE_NAME}-#{PACKAGE_VERSION}.pkg") 163 | FileUtils.rm_rf(build_dir) 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /test/test_filter.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'ostruct' 6 | require 'galaxy/filter' 7 | 8 | class TestFilter < Test::Unit::TestCase 9 | def setup 10 | @null = OpenStruct.new({ }) 11 | 12 | @foo = OpenStruct.new({ 13 | :host => 'foo', 14 | :ip => '10.0.0.1', 15 | :machine => 'foomanchu', 16 | :config_path => '/alpha/1.0/bloo', 17 | :status => 'running', 18 | }) 19 | 20 | @bar = OpenStruct.new({ 21 | :host => 'bar', 22 | :ip => '10.0.0.2', 23 | :machine => 'barmanchu', 24 | :config_path => '/beta/2.0/blar', 25 | :status => 'stopped', 26 | }) 27 | 28 | @baz = OpenStruct.new({ 29 | :host => 'baz', 30 | :ip => '10.0.0.3', 31 | :machine => 'bazmanchu', 32 | :config_path => '/gamma/3.0/blaz', 33 | :status => 'dead', 34 | }) 35 | 36 | @blee = OpenStruct.new({ 37 | :host => 'blee', 38 | :ip => '10.0.0.4', 39 | :machine => 'bleemanchu', 40 | }) 41 | 42 | @agents = [@null, @foo, @bar, @baz, @blee] 43 | end 44 | 45 | def test_filter_none 46 | filter = Galaxy::Filter.new({ }) 47 | 48 | assert_equal 0, @agents.select(&filter).size 49 | end 50 | 51 | def test_filter_by_known_host 52 | filter = Galaxy::Filter.new :host => "foo" 53 | 54 | assert_equal [@foo], @agents.select(&filter) 55 | end 56 | 57 | def test_filter_by_unknown_host 58 | filter = Galaxy::Filter.new :host => "unknown" 59 | 60 | assert_equal [ ], @agents.select(&filter) 61 | end 62 | 63 | def test_filter_by_known_machine 64 | filter = Galaxy::Filter.new :machine => "foomanchu" 65 | 66 | assert_equal [@foo], @agents.select(&filter) 67 | end 68 | 69 | def test_filter_by_unknown_machine 70 | filter = Galaxy::Filter.new :machine => "unknown" 71 | 72 | assert_equal [ ], @agents.select(&filter) 73 | end 74 | 75 | def test_filter_by_known_ip 76 | filter = Galaxy::Filter.new :ip => "10.0.0.1" 77 | 78 | assert_equal [@foo], @agents.select(&filter) 79 | end 80 | 81 | def test_filter_by_unknown_ip 82 | filter = Galaxy::Filter.new :ip => "20.0.0.1" 83 | 84 | assert_equal [ ], @agents.select(&filter) 85 | end 86 | 87 | def test_filter_by_state_running 88 | filter = Galaxy::Filter.new :state => "running" 89 | 90 | assert_equal [@foo], @agents.select(&filter) 91 | end 92 | 93 | def test_filter_by_state_stopped 94 | filter = Galaxy::Filter.new :state => "stopped" 95 | 96 | assert_equal [@bar], @agents.select(&filter) 97 | end 98 | 99 | def test_filter_by_state_dead 100 | filter = Galaxy::Filter.new :state => "dead" 101 | 102 | assert_equal [@baz], @agents.select(&filter) 103 | end 104 | 105 | def test_filter_by_unknown_state 106 | filter = Galaxy::Filter.new :state => "unknown" 107 | 108 | assert_equal [ ], @agents.select(&filter) 109 | end 110 | 111 | def test_filter_by_known_env 112 | filter = Galaxy::Filter.new :env => "beta" 113 | 114 | assert_equal [@bar], @agents.select(&filter) 115 | end 116 | 117 | def test_filter_by_known_env 118 | filter = Galaxy::Filter.new :env => "unknown" 119 | 120 | assert_equal [ ], @agents.select(&filter) 121 | end 122 | 123 | def test_filter_by_known_version 124 | filter = Galaxy::Filter.new :version => "1.0" 125 | 126 | assert_equal [@foo], @agents.select(&filter) 127 | end 128 | 129 | def test_filter_by_unknown_version 130 | filter = Galaxy::Filter.new :version => "0.0" 131 | 132 | assert_equal [ ], @agents.select(&filter) 133 | end 134 | 135 | def test_filter_by_known_type 136 | filter = Galaxy::Filter.new :type => "bloo" 137 | 138 | assert_equal [@foo], @agents.select(&filter) 139 | end 140 | 141 | def test_filter_by_unknown_type 142 | filter = Galaxy::Filter.new :type => "unknown" 143 | 144 | assert_equal [ ], @agents.select(&filter) 145 | end 146 | 147 | def test_filter_by_assigned 148 | filter = Galaxy::Filter.new :set => :taken 149 | 150 | assert_equal [@foo, @bar, @baz], @agents.select(&filter) 151 | end 152 | 153 | def test_filter_by_unassigned 154 | filter = Galaxy::Filter.new :set => :empty 155 | 156 | assert_equal [@null, @blee], @agents.select(&filter) 157 | end 158 | 159 | def test_filter_all 160 | filter = Galaxy::Filter.new :set => :all 161 | 162 | assert_equal @agents, @agents.select(&filter) 163 | end 164 | 165 | 166 | ##################################################################################### 167 | # The following are additions for GAL-151. Given the way the code is _currently_ 168 | # written, we really only need to check against host and machine, but the others are 169 | # added for increased safety and future-proofing 170 | # 171 | def test_filter_by_unknown_host_like_known_host 172 | filter = Galaxy::Filter.new :host => "fo" #don't match with "foo" 173 | 174 | assert_equal [ ], @agents.select(&filter) 175 | end 176 | 177 | def test_filter_by_unknown_machine_like_known_machine 178 | filter = Galaxy::Filter.new :machine => "fooman" # don't match with "foomanchu" 179 | 180 | assert_equal [ ], @agents.select(&filter) 181 | end 182 | 183 | def test_filter_by_unknown_ip_like_known_ip 184 | filter = Galaxy::Filter.new :ip => "10.0.0." # don't match with "10.0.0.1" 185 | 186 | assert_equal [ ], @agents.select(&filter) 187 | end 188 | 189 | def test_filter_by_unknown_env_like_known_env 190 | filter = Galaxy::Filter.new :env => "bet" #don't match with "beta" 191 | 192 | assert_equal [ ], @agents.select(&filter) 193 | end 194 | 195 | def test_filter_by_unknown_version_like_known_version 196 | filter = Galaxy::Filter.new :version => "1." #don't match with "1.0" 197 | 198 | assert_equal [ ], @agents.select(&filter) 199 | end 200 | 201 | def test_filter_by_unknown_type_like_known_type 202 | filter = Galaxy::Filter.new :type => "blo" # don't match with "bloo" 203 | 204 | assert_equal [ ], @agents.select(&filter) 205 | end 206 | 207 | end 208 | -------------------------------------------------------------------------------- /test/test_commands.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), "..", "lib") 2 | $:.unshift File.join(File.dirname(__FILE__)) 3 | 4 | require 'test/unit' 5 | require 'galaxy/command' 6 | require 'galaxy/transport' 7 | require 'helper' 8 | 9 | class TestCommands < Test::Unit::TestCase 10 | def setup 11 | @agents = [ 12 | MockAgent.new("agent1", "alpha", "1.0", "sysc"), 13 | MockAgent.new("agent2", "alpha", "1.0", "idtc"), 14 | MockAgent.new("agent3", "alpha", "1.0", "appc/aclu0"), 15 | MockAgent.new("agent4"), 16 | MockAgent.new("agent5", "alpha", "2.0", "sysc"), 17 | MockAgent.new("agent6", "beta", "1.0", "sysc"), 18 | MockAgent.new("agent7") 19 | ] 20 | 21 | @console = MockConsole.new(@agents) 22 | end 23 | 24 | def teardown 25 | @agents.each { |a| a.shutdown } 26 | @console.shutdown 27 | end 28 | 29 | def test_all_registered 30 | assert Galaxy::Commands["assign"] 31 | assert Galaxy::Commands["clear"] 32 | assert Galaxy::Commands["reap"] 33 | assert Galaxy::Commands["restart"] 34 | assert Galaxy::Commands["rollback"] 35 | assert Galaxy::Commands["show"] 36 | assert Galaxy::Commands["ssh"] 37 | assert Galaxy::Commands["start"] 38 | assert Galaxy::Commands["stop"] 39 | assert Galaxy::Commands["update"] 40 | assert Galaxy::Commands["update-config"] 41 | end 42 | 43 | def internal_test_all_for cmd 44 | command = Galaxy::Commands[cmd].new [], {:console => @console} 45 | agents = command.select_agents(:set => :all) 46 | command.execute agents 47 | 48 | @agents.select { |a| a.config_path }.each { |a| assert_equal true, yield(a) } 49 | @agents.select { |a| a.config_path.nil? }.each { |a| assert_equal false, yield(a) } 50 | end 51 | 52 | def internal_test_by_host cmd 53 | command = Galaxy::Commands[cmd].new [], {:console => @console} 54 | agents = command.select_agents(:host => "agent1") 55 | command.execute agents 56 | 57 | @agents.select {|a| a.host == "agent1" }.each { |a| assert_equal true, yield(a) } 58 | @agents.select {|a| a.host != "agent1" }.each { |a| assert_equal false, yield(a) } 59 | end 60 | 61 | def internal_test_by_type cmd 62 | command = Galaxy::Commands[cmd].new [], {:console => @console} 63 | agents = command.select_agents(:type => "sysc") 64 | command.execute agents 65 | 66 | @agents.select {|a| a.type == "sysc" }.each { |a| assert_equal true, yield(a) } 67 | @agents.select {|a| a.type != "sysc" }.each { |a| assert_equal false, yield(a) } 68 | end 69 | 70 | def test_stop_all 71 | internal_test_all_for("stop") { |a| a.stopped } 72 | end 73 | 74 | def test_start_all 75 | internal_test_all_for("start") { |a| a.started } 76 | end 77 | 78 | def test_restart_all 79 | internal_test_all_for("restart") { |a| a.restarted } 80 | end 81 | 82 | def test_stop_by_host 83 | internal_test_by_host("stop") { |a| a.stopped } 84 | end 85 | 86 | def test_start_by_host 87 | internal_test_by_host("start") { |a| a.started } 88 | end 89 | 90 | def test_restart_by_host 91 | internal_test_by_host("restart") { |a| a.restarted } 92 | end 93 | 94 | def test_stop_by_type 95 | internal_test_by_type("stop") { |a| a.stopped } 96 | end 97 | 98 | def test_start_by_type 99 | internal_test_by_type("start") { |a| a.started } 100 | end 101 | 102 | def test_restart_by_type 103 | internal_test_by_type("restart") { |a| a.restarted } 104 | end 105 | 106 | def test_show_all 107 | command = Galaxy::Commands["show"].new [], {:console => @console} 108 | agents = command.select_agents(:set => :all) 109 | results = command.execute agents 110 | 111 | assert_equal format_agents, results 112 | end 113 | 114 | def test_show_by_env 115 | command = Galaxy::Commands["show"].new [], {:console => @console} 116 | agents = command.select_agents(:env => "alpha") 117 | results = command.execute agents 118 | 119 | assert_equal format_agents(@agents.select {|a| a.env == "alpha"}), results 120 | end 121 | 122 | def test_show_by_version 123 | command = Galaxy::Commands["show"].new [], {:console => @console, :version => "1.0"} 124 | agents = command.select_agents(:version => "1.0") 125 | results = command.execute agents 126 | 127 | assert_equal format_agents(@agents.select {|a| a.version == "1.0"}), results 128 | end 129 | 130 | def test_show_by_type 131 | command = Galaxy::Commands["show"].new [], {:console => @console} 132 | agents = command.select_agents(:type => :sysc) 133 | results = command.execute agents 134 | 135 | assert_equal format_agents(@agents.select {|a| a.type == "sysc"}), results 136 | end 137 | 138 | def test_show_by_type2 139 | command = Galaxy::Commands["show"].new [], {:console => @console} 140 | agents = command.select_agents(:type => "appc/aclu0") 141 | results = command.execute agents 142 | 143 | assert_equal format_agents(@agents.select {|a| a.type == "appc/aclu0"}), results 144 | end 145 | 146 | def test_show_by_env_version_type 147 | command = Galaxy::Commands["show"].new [], {:console => @console} 148 | agents = command.select_agents({:type => "sysc", :env => "alpha", :version => "1.0"}) 149 | results = command.execute agents 150 | 151 | assert_equal format_agents(@agents.select {|a| a.type == "sysc" && a.env == "alpha" && a.version == "1.0"}), results 152 | end 153 | 154 | def test_assign_empty 155 | command = Galaxy::Commands["assign"].new ["beta", "3.0", "rslv"], {:console => @console, :set => :empty} 156 | agents = command.select_agents(:set => :all) 157 | agent = @agents.select { |a| a.config_path.nil? }.first 158 | command.execute agents 159 | assert_equal "beta", agent.env 160 | assert_equal "rslv", agent.type 161 | assert_equal "3.0", agent.version 162 | end 163 | 164 | def test_assign_by_host 165 | agent = @agents.select { |a| a.host == "agent7" }.first 166 | 167 | command = Galaxy::Commands["assign"].new ["beta", "3.0", "rslv"], { :console => @console } 168 | agents = command.select_agents(:host => agent.host) 169 | command.execute agents 170 | 171 | assert_equal "beta", agent.env 172 | assert_equal "rslv", agent.type 173 | assert_equal "3.0", agent.version 174 | end 175 | 176 | def test_clear 177 | # TODO 178 | end 179 | 180 | def test_clear_by_host 181 | # TODO 182 | end 183 | 184 | def test_update_by_host 185 | agent = @agents.select { |a| !a.config_path.nil? }.first 186 | env = agent.env 187 | type = agent.type 188 | 189 | command = Galaxy::Commands["update"].new ["4.0"], { :console => @console } 190 | agents = command.select_agents(:host => agent.host) 191 | command.execute agents 192 | 193 | assert_equal env, agent.env 194 | assert_equal type, agent.type 195 | assert_equal "4.0", agent.version 196 | end 197 | 198 | def test_update_config_by_host 199 | agent = @agents.select { |a| !a.config_path.nil? }.first 200 | env = agent.env 201 | type = agent.type 202 | 203 | command = Galaxy::Commands["update-config"].new ["4.0"], { :console => @console } 204 | agents = command.select_agents(:version => "1.0") 205 | results = command.execute agents 206 | assert_equal env, agent.env 207 | assert_equal type, agent.type 208 | assert_equal "4.0", agent.version 209 | end 210 | 211 | private 212 | 213 | def format_agents(agents=@agents) 214 | res = agents.inject("") do |memo, a| 215 | memo.empty? ? a.inspect : memo.to_s + a.inspect 216 | end 217 | res 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/galaxy/console.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'logger' 3 | require 'rubygems' 4 | begin 5 | # We don't install the json gem by default on our machines. 6 | # Not critical, since it is for the HTTP API that will be rolled in the future 7 | require 'json' 8 | $JSON_LOADED = true 9 | rescue LoadError 10 | $JSON_LOADED = false 11 | end 12 | require 'resolv' 13 | 14 | require 'galaxy/events' 15 | require 'galaxy/filter' 16 | require 'galaxy/log' 17 | require 'galaxy/transport' 18 | require 'galaxy/announcements' 19 | 20 | module Galaxy 21 | class Console 22 | attr_reader :db, :drb_url, :http_url, :ping_interval, :host, :env, :logger 23 | 24 | def self.locate url 25 | Galaxy::Transport.locate url 26 | end 27 | 28 | def initialize drb_url, http_url, log, log_level, ping_interval, host, env, event_listener 29 | @host = host 30 | @ip = Resolv.getaddress(@host) 31 | @env = env 32 | 33 | @drb_url = drb_url 34 | @http_url = http_url 35 | 36 | # Setup the logger and the event dispatcher (HDFS) if needed 37 | @logger = Galaxy::Log::Glogger.new(log, event_listener, @http_url, @ip) 38 | @logger.log.level = log_level 39 | 40 | @ping_interval = ping_interval 41 | @db = {} 42 | @mutex = Mutex.new 43 | 44 | # set up event listener 45 | @event_dispatcher = Galaxy::GalaxyEventSender.new(event_listener, @http_url, @ip, @logger) 46 | 47 | Thread.new do 48 | loop do 49 | begin 50 | cutoff = Time.new 51 | sleep @ping_interval 52 | ping cutoff 53 | rescue Exception => e 54 | @logger.warn "Uncaught exception in agent ping thread: #{e}" 55 | @logger.warn e.backtrace 56 | end 57 | end 58 | end 59 | end 60 | 61 | # Remote API 62 | def reap host 63 | @mutex.synchronize do 64 | @db.delete host 65 | end 66 | end 67 | 68 | # Return agents matching a filter query 69 | # Used by both HTTP and DRb API. 70 | def agents filters = {} 71 | # Log command run by the client 72 | if filters[:command] 73 | @logger.info filters[:command] 74 | filters.delete :command 75 | end 76 | 77 | filters = {:set => :all} if (filters.empty? or filters.nil?) 78 | 79 | filter = Galaxy::Filter.new filters 80 | @logger.debug "Filtering agents by #{filter}" 81 | 82 | @mutex.synchronize do 83 | @db.values.select(& filter) 84 | end 85 | end 86 | 87 | # Process announcement (ping) from agent (HTTP API) 88 | # 89 | # this function is called as a callback from http post server. We could just use the announce function as the 90 | # callback, but using this function allows us to add in different stats for post announcements. 91 | def process_post announcement 92 | announce announcement 93 | end 94 | 95 | include Galaxy::HTTPUtils 96 | 97 | # Return agents matching a filter query (HTTP API). 98 | # 99 | # Note that & in the query means actually OR. 100 | def process_get query_string 101 | # Convert env=prod&host=prod-1.company.com to {:env => "prod", :host => 102 | # "prod-1.company.com"} 103 | filters = {} 104 | CGI::parse(query_string).each { |k, v| filters[k.to_sym] = v.first } 105 | if $JSON_LOADED 106 | return agents(filters).to_json 107 | else 108 | return agents(filters).inspect 109 | end 110 | end 111 | 112 | # Remote API 113 | def dispatch_event type, msg 114 | @event_dispatcher.send("dispatch_#{type}_event", msg) 115 | end 116 | 117 | def Console.start args 118 | host = args[:host] || "localhost" 119 | drb_url = args[:url] || "druby://" + host # DRB transport 120 | drb_url += ":4440" unless drb_url.match ":[0-9]+$" 121 | 122 | http_url = args[:announcement_url] || "http://localhost" # http announcements 123 | http_url = "#{http_url}:4442" unless http_url.match ":[0-9]+$" 124 | 125 | console = Console.new drb_url, http_url, 126 | args[:log] || "STDOUT", 127 | args[:log_level] || Logger::INFO, 128 | args[:ping_interval] || 5, 129 | host, args[:environment], args[:event_listener] 130 | 131 | # DRb transport (galaxy command line client) 132 | Galaxy::Transport.publish drb_url, console, console.logger 133 | 134 | # HTTP API (announcements, status, ...) 135 | Galaxy::Transport.publish http_url, console, console.logger 136 | 137 | console 138 | end 139 | 140 | def shutdown 141 | Galaxy::Transport.unpublish @http_url 142 | end 143 | 144 | def join 145 | Galaxy::Transport.join @drb_url 146 | end 147 | 148 | private 149 | 150 | # Update the agents database 151 | def announce announcement 152 | begin 153 | host = announcement.host 154 | @logger.debug "Received announcement from #{host}" 155 | @mutex.synchronize do 156 | if @db.has_key?(host) 157 | unless @db[host].agent_status != "offline" 158 | announce_message = "#{host} is now online again" 159 | @logger.info announce_message 160 | @event_dispatcher.dispatch_announce_success_event announce_message 161 | end 162 | if @db[host].status != announcement.status 163 | announce_message = "#{host} core state changed: #{@db[host].status} --> #{announcement.status}" 164 | @logger.info announce_message 165 | @event_dispatcher.dispatch_announce_success_event announce_message 166 | end 167 | else 168 | announce_message = "Discovered new agent: #{host} [#{announcement.inspect}]" 169 | @logger.info "Discovered new agent: #{host} [#{announcement.inspect}]" 170 | @event_dispatcher.dispatch_announce_success_event announce_message 171 | end 172 | 173 | @db[host] = announcement 174 | @db[host].timestamp = Time.now 175 | @db[host].agent_status = 'online' 176 | end 177 | rescue RuntimeError => e 178 | error_message = "Error receiving announcement: #{e}" 179 | @logger.warn error_message 180 | @event_dispatcher.dispatch_announce_error_event error_message 181 | end 182 | end 183 | 184 | # Iterate through the database to find agents that haven't pinged home 185 | def ping cutoff 186 | @mutex.synchronize do 187 | @db.each_pair do |host, entry| 188 | if entry.agent_status != "offline" and entry.timestamp < cutoff 189 | error_message = "#{host} failed to announce; marking as offline" 190 | @logger.warn error_message 191 | @event_dispatcher.dispatch_announce_error_event error_message 192 | 193 | entry.agent_status = "offline" 194 | entry.status = "unknown" 195 | end 196 | end 197 | end 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /lib/galaxy/events.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'galaxy/announcements' 3 | 4 | module Galaxy 5 | # Generic Event sender class 6 | class EventSender 7 | COLLECTOR_API_VERSION = 1 8 | 9 | GALAXY_SCHEMA = 'Galaxy' 10 | GALAXY_LOG_SCHEMA = 'GalaxyLog' 11 | DUMMY_TYPE = 'dummy' 12 | 13 | attr_reader :type, :log 14 | 15 | def initialize(listener_url, gonsole_url = nil, ip_addr=nil, log=Logger.new(STDOUT)) 16 | @log = log 17 | @gonsole_url = gonsole_url 18 | @ip_addr = ip_addr 19 | @log.debug "Registered Event listener type #{self.class} at #{listener_url}, sender url #{ip_addr}" 20 | listener_url.nil? ? @uri = nil : @uri = URI.parse(listener_url) 21 | end 22 | 23 | # To override in the child class. event is an OpenStruct 24 | def send_event(event) 25 | # no-op 26 | end 27 | 28 | private 29 | 30 | def escape str 31 | # default is URI::REGEXP::UNSAFE is /[^-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n 32 | # and is not enough! (& and , not escaped) 33 | return URI.escape(str, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) 34 | end 35 | 36 | # Sanitize strings 37 | def add_field(type, value) 38 | return "#{type}#{escape(value.to_s)}," 39 | end 40 | 41 | def do_send_event(formatted_query) 42 | return if @uri.nil? 43 | http_query = @uri.merge(formatted_query).to_s 44 | @log.debug http_query 45 | begin 46 | # GET 47 | res = Net::HTTP.start(@uri.host, @uri.port) do |http| 48 | headers = {'Content-Type' => 'text/plain; charset=utf-8'} 49 | response = http.send_request('GET', http_query, nil, headers) 50 | @log.debug "Event sent to Collector #{@uri.host} = #{http_query}" if @log 51 | response # Return the response form the block 52 | end 53 | case res 54 | when Net::HTTPAccepted 55 | return true 56 | else 57 | res.error! 58 | end 59 | rescue Exception => e 60 | if @log 61 | @log.warn "Unable to contact EventListener on #{@uri}" 62 | @log.warn "Request: #{http_query}" 63 | @log.warn "Client side error: #{e}" 64 | @log.warn "Body reponse: #{res.body}" if res 65 | end 66 | return false 67 | end 68 | end 69 | end 70 | 71 | # Send logs to HDFS 72 | class GalaxyLogEventSender < EventSender 73 | EVENTS_SUPPORTED = [:debug, :error, :fatal, :info, :warn] 74 | 75 | EVENTS_SUPPORTED.each do |loglevel| 76 | define_method "dispatch_#{loglevel.to_s}_log" do |* args| 77 | message, progname, * ignored = args 78 | send_event(generate_event(loglevel.to_s, message, progname)) 79 | end 80 | end 81 | 82 | private 83 | 84 | # Pre-process logs and generate OpenStruct mapping the GalaxyLog Thrift Schema 85 | # 86 | # struct GalaxyLog { 87 | # 1:i64 date, 88 | # 2:i32 ip_addr, 89 | # 3:i16 pid, 90 | # 4:string severity, 91 | # 5:string progname, 92 | # 6:string message 93 | #} 94 | def generate_event(severity, message, progname) 95 | event = OpenStruct.new 96 | 97 | event.date = Time.now.to_i * 1000 98 | event.gonsole_url = @gonsole_url 99 | event.ip_addr = @ip_addr 100 | event.pid = $$ 101 | event.severity = severity.downcase 102 | event.progname = progname 103 | event.message = message 104 | 105 | return event 106 | end 107 | 108 | def send_event(event) 109 | do_send_event format_url(event) 110 | end 111 | 112 | private 113 | 114 | def format_url(event) 115 | url = "/#{COLLECTOR_API_VERSION}?" 116 | url += "v=#{EventSender::GALAXY_LOG_SCHEMA}," 117 | url += escape(add_field("8", event.date)) 118 | # Make sure to add a valid number (int) or the collector will choke on it (400 bad request) 119 | url += escape(add_field("4", (format_ip(event.ip_addr)))) 120 | url += escape(add_field("2", (event.pid || 0))) 121 | url += escape(add_field("s", event.severity)) 122 | url += escape(add_field("s", event.progname)) 123 | url += escape(add_field("s", event.message)) 124 | url += escape(add_field("s", event.gonsole_url)) 125 | url += "&rt=b" 126 | return url 127 | end 128 | 129 | def format_ip(ip_addr) 130 | if ip_addr.nil? or ip_addr.empty? 131 | return 0 132 | end 133 | addr = 0 134 | ip_addr.split(".").each do |x| 135 | addr = addr * 256 + x.to_i 136 | end 137 | return addr 138 | end 139 | end 140 | 141 | # Send actions related events to HDFS 142 | class GalaxyEventSender < EventSender 143 | EVENTS_SUPPORTED = [:announce, :cleanup, :command, :update_config, :become, :rollback, :start, :stop, :clear, :perform, :restart] 144 | EVENTS_RESULT_SUPPORTED = [:success, :error] 145 | 146 | EVENTS_SUPPORTED.each do |event| 147 | EVENTS_RESULT_SUPPORTED.each do |result| 148 | define_method "dispatch_#{event}_#{result}_event" do |status| 149 | if status.is_a? String 150 | status = OpenStruct.new(:message => status) 151 | elsif status.is_a? Hash 152 | status = OpenStruct.new(status) 153 | end 154 | # e.g. perform, announce 155 | status.event_type = event 156 | # e.g. error, success 157 | status.galaxy_event_type = result 158 | status.gonsole_url = @gonsole_url 159 | send_event(status) 160 | end 161 | end 162 | end 163 | 164 | def send_event(event) 165 | do_send_event format_url(event) 166 | end 167 | 168 | private 169 | 170 | def format_url(event) 171 | url = "/#{COLLECTOR_API_VERSION}?" 172 | url += "v=#{EventSender::GALAXY_SCHEMA}," 173 | #url += add_field "8", event.timestamp 174 | url += escape(add_field("x", "date")) 175 | url += escape(add_field("s", event.event_type)) 176 | url += escape(add_field("s", event.message)) 177 | url += escape(add_field("s", event.agent_status)) 178 | url += escape(add_field("s", event.os)) 179 | url += escape(add_field("s", event.host)) 180 | url += escape(add_field("s", event.galaxy_version)) 181 | url += escape(add_field("s", event.core_type)) 182 | url += escape(add_field("s", event.machine)) 183 | url += escape(add_field("s", event.status)) 184 | url += escape(add_field("s", event.galaxy_event_type)) 185 | url += escape(add_field("s", event.url)) 186 | url += escape(add_field("s", event.config_path)) 187 | url += escape(add_field("s", event.build)) 188 | url += escape(add_field("s", event.ip)) 189 | url += escape(add_field("s", event.user)) 190 | url += escape(add_field("s", event.gonsole_url)) 191 | url += "&rt=b" 192 | return url 193 | end 194 | end 195 | 196 | # When the user doesn't specify a collector URL 197 | class DummyEventSender < EventSender 198 | def initialize() 199 | end 200 | 201 | def method_missing(m, * args, & block) 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/galaxy/announcements.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'uri' 3 | require 'yaml' 4 | require 'ostruct' 5 | require 'logger' 6 | require 'rubygems' 7 | 8 | begin 9 | # mongrel is installed only on the gonsole, not on agent machines 10 | require 'mongrel' 11 | $MONGREL_LOADED = true 12 | rescue LoadError 13 | $MONGREL_LOADED = false 14 | end 15 | 16 | begin 17 | # We don't install the json gem by default on our machines. 18 | # Not critical, since it is for the HTTP API that will be rolled in the future 19 | require 'json' 20 | $JSON_LOADED = true 21 | rescue LoadError 22 | $JSON_LOADED = false 23 | end 24 | 25 | module Galaxy 26 | module HTTPUtils 27 | def url_escape(string) 28 | string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do 29 | '%' + $1.unpack('H2' * $1.size).join('%').upcase 30 | end.tr(' ', '+') 31 | end 32 | 33 | def url_unescape(string) 34 | string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do 35 | [$1.delete('%')].pack('H*') 36 | end 37 | end 38 | end 39 | 40 | if $MONGREL_LOADED 41 | class HTTPServer 42 | def initialize(url, console, callbacks=nil, log=nil) 43 | @log = log || Logger.new(STDOUT) 44 | 45 | # Create server 46 | begin 47 | @server = Mongrel::HttpServer.new("0.0.0.0", get_port(url)) 48 | rescue Exception => err 49 | msg = "HTTP server initialization error: #{err}" 50 | @log.error msg 51 | raise IOError, msg 52 | end 53 | 54 | @server.register("/", ReceiveAnnouncement.new(console), true) 55 | @server.register("/status", AnnouncementStatus.new, true) 56 | 57 | # Actually start the server 58 | @thread = Thread.new do 59 | begin 60 | @server.run.join 61 | rescue Exception => err 62 | msg = "HTTP server start error: #{err}" 63 | @log.error msg 64 | raise msg 65 | end 66 | end 67 | end 68 | 69 | # parse the port from the given url string 70 | def get_port(url) 71 | begin 72 | last = url.count(':') 73 | raise "malformed url: '#{url}'." if last==0 || last>2 74 | port = url.split(':')[last].to_i 75 | rescue Exception => err 76 | msg = "Problem parsing port for string '#{url}': error = #{err}" 77 | @log.error msg 78 | raise msg 79 | end 80 | port 81 | end 82 | 83 | def shutdown 84 | if @server 85 | @server.stop 86 | @server.graceful_shutdown 87 | @thread.join 88 | @server = @thread = nil 89 | end 90 | end 91 | end 92 | 93 | 94 | # POST handler that receives announcements and calls the callback function with the data payload 95 | class ReceiveAnnouncement < Mongrel::HttpHandler 96 | ANNOUNCEMENT_RESPONSE_TEXT = 'Announcement received.' 97 | 98 | def initialize(console) 99 | @console = console 100 | end 101 | 102 | def process(request, response) 103 | response.start(200) do |head, out| 104 | head['Context-Type'] = 'text/plain; charset=utf-8' 105 | head['Connection'] = 'close' 106 | if request.params['REQUEST_METHOD'] == 'POST' 107 | @console.process_post(YAML::load(request.body)) 108 | out.write ANNOUNCEMENT_RESPONSE_TEXT 109 | elsif request.params['REQUEST_METHOD'] == 'GET' 110 | out.write @console.process_get(request.params['REQUEST_PATH']) 111 | end 112 | end 113 | end 114 | end 115 | 116 | # optional GET response for querying the server status 117 | class AnnouncementStatus < Mongrel::HttpHandler 118 | def process(request, response) 119 | if request.params['REQUEST_METHOD'] == 'GET' 120 | response.start(200) do |head, out| 121 | head['Context-Type'] = 'text/plain; charset=utf-8' 122 | head['Connection'] = 'close' 123 | time = Time.now 124 | body = "" 125 | body += "

Announcement Status



"; 126 | body += time.strftime("%Y%m%d-%H:%M:%S") + sprintf(".%06d", time.usec) 127 | body += "" 128 | out.write body 129 | end 130 | end 131 | end 132 | end 133 | end 134 | end 135 | 136 | # HTTP client library. 137 | # Used by the galaxy agent to send announcements to the server. 138 | # Used by the command line client to query the gonsole over HTTP. 139 | class HTTPAnnouncementSender 140 | include Galaxy::HTTPUtils 141 | 142 | def initialize(url, log = nil) 143 | # eg: 'http://encomium.company.com:4440' 144 | @uri = URI.parse(url) 145 | @log = log 146 | end 147 | 148 | # Announce an agent to a gonsole 149 | # agent is an OpenStruct defining the state of the agent. 150 | def announce(agent) 151 | begin 152 | # POST 153 | Net::HTTP.start(@uri.host, @uri.port) do |http| 154 | headers = {'Content-Type' => 'text/plain; charset=utf-8', 'Connection' => 'close'} 155 | put_data = agent.to_yaml 156 | start_time = Time.now 157 | response = http.send_request('POST', @uri.request_uri, put_data, headers) 158 | @log.debug "Announcement send response time for #{agent.host} = #{Time.now-start_time}" if @log 159 | #puts "Response = #{response.code} #{response.message}: #{response.body}" 160 | response.body 161 | end 162 | rescue Exception => e 163 | @log.warn "Client side error: #{e}" if @log 164 | end 165 | end 166 | 167 | # Retrieve a list of agents matching a giving filter 168 | # args is a hash filter (cf Galaxy::Filter). 169 | def agents(args) 170 | # Convert filter string ({:set=>:all}) to URI string (/set=all) 171 | # XXX Built-in method to do that? 172 | filter = "" 173 | args.each do |key, value| 174 | filter += url_escape(key.to_s) + "=" + url_escape(value.to_s) + "&" 175 | end 176 | filter.chomp!("&") 177 | 178 | begin 179 | Net::HTTP.start(@uri.host, @uri.port) do |http| 180 | headers = {'Content-Type' => 'text/plain; charset=utf-8', 'Connection' => 'close'} 181 | start_time = Time.now 182 | response = http.send_request('GET', @uri.request_uri + filter, headers) 183 | @log.debug "Announcement send response time for #{agent.host} = #{Time.now-start_time}" if @log 184 | return JSON.parse(response.body).collect { |x| OpenStruct.new(x) } 185 | end 186 | rescue Exception => e 187 | # If the json gem is not loaded, we will log the issue here. 188 | @log.warn "Client side error: #{e}" if @log 189 | end 190 | end 191 | 192 | # :nodoc: 193 | # Compatibility with the DRb galaxy client. 194 | # XXX Should go away (overhead). 195 | def log(* args) 196 | nil 197 | end 198 | end 199 | 200 | 201 | ################################################################################################ 202 | # 203 | # sample MAIN 204 | # 205 | 206 | # example callback for action upon receiving an announcement 207 | def on_announcement(ann) 208 | puts "...received announcement: #{ann.inspect}" 209 | end 210 | 211 | # Initialize and POST to server 212 | if $0 == __FILE__ then 213 | # start server 214 | url = 'http://encomium.company.com:4440' 215 | Galaxy::HTTPAnnouncementReceiver.new(url, lambda { |a| on_announcement(a) }) 216 | announcer = HTTPAnnouncementSender.new(url) 217 | 218 | # periodically, send stuff to it 219 | loop do 220 | begin 221 | 222 | announcer.announce(OpenStruct.new(:foo=>"bar", :rand => rand(100), :item => "eggs")) 223 | 224 | puts "server running..." 225 | sleep 15 226 | rescue Exception => err 227 | STDERR.puts "* #{err}" 228 | exit(1) 229 | end 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /bin/galaxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'optparse' 5 | require 'resolv' 6 | require 'timeout' 7 | 8 | require 'galaxy/client' 9 | require 'galaxy/command' 10 | require 'galaxy/config' 11 | require 'galaxy/host' 12 | require 'galaxy/version' 13 | require 'galaxy/transport' 14 | require 'galaxy/versioning' 15 | 16 | @filter = {} 17 | @options = { 18 | :console_url => ENV['GALAXY_CONSOLE'], 19 | :thread_count => 25, 20 | :versioning_policy => Galaxy::Versioning::StrictVersioningPolicy, 21 | } 22 | 23 | @opts = OptionParser.new do |opts| 24 | opts.banner = "#{$0} [options] [args]" 25 | 26 | opts.separator "" 27 | opts.separator "Options:" 28 | opts.on("-h", "--help", "Display a help message and exit") { @options[:help_requested] = true } 29 | opts.on("-c", "--console CONSOLE", "Galaxy console host (overrides GALAXY_CONSOLE)") { |arg| @options[:console_url] = arg } 30 | opts.on("-C", "--config FILE", "Configuration file (overrides GALAXY_CONFIG)") { |arg| @options[:config_file] = arg } 31 | opts.on("-p", "--parallel-count THREADS", "Maximum number of threads to use, default #{@options[:thread_count]}") { |arg| @options[:thread_count] = arg.to_i } 32 | opts.on("-r", "--relaxed-versioning", "Allow updates to the currently assigned version") { @options[:versioning_policy] = Galaxy::Versioning::RelaxedVersioningPolicy } 33 | opts.on("-V", "Display the Galaxy version number and exit") do |x| 34 | puts "Galaxy version #{Galaxy::Version}" 35 | @options[:version_requested] = true 36 | end 37 | opts.on("-y", "--yes", "Avoid confirmation prompts by automatically confirming all actions") { @options[:implicit_confirmation] = true } 38 | 39 | opts.separator "" 40 | opts.separator "Filters:" 41 | 42 | opts.on("-i", "--host HOST", "Select a specific agent by hostname") do |arg| 43 | @filter[:host] = arg 44 | end 45 | 46 | opts.on("-I", "--ip IP", "Select a specific agent by IP address") do |arg| 47 | @filter[:ip] = arg 48 | end 49 | 50 | opts.on("-m", "--machine MACHINE", "Select agents by physical machine") do |arg| 51 | @filter[:machine] = arg 52 | end 53 | 54 | opts.on("-M", "--cohabitants HOST", "Select agents that share a physical machine with the specified host") do |arg| 55 | @options[:cohabitant_host] = arg 56 | end 57 | 58 | opts.on("-s", "--set SET", "Select 'e{mpty}', 't{aken}' or 'a{ll}' hosts", [:empty, :all, :taken, :e, :a, :t]) do |arg| 59 | case arg 60 | when :all, :a then 61 | @filter[:set] = :all 62 | when :empty, :e then 63 | @filter[:set] = :empty 64 | when :taken, :t then 65 | @filter[:set] = :taken 66 | end 67 | end 68 | 69 | opts.on("-S", "--state STATE", "Select 'r{unning}' or 's{topped}' hosts", [:running, :stopped, :r, :s]) do |arg| 70 | case arg 71 | when :running, :r then 72 | @filter[:state] = 'running' 73 | when :stopped, :s then 74 | @filter[:state] = 'stopped' 75 | end 76 | end 77 | 78 | opts.on("-A", "--agent-state STATE", "Select 'online' or 'offline' agents", [:online, :offline]) do |arg| 79 | case arg 80 | when :online then 81 | @filter[:agent_state] = 'online' 82 | when :offline then 83 | @filter[:agent_state] = 'offline' 84 | end 85 | end 86 | 87 | opts.on("-e", "--env ENV", "Select agents in the given environment") { |arg| @filter[:env] = arg } 88 | opts.on("-t", "--type TYPE", "Select agents with a given software type") { |arg| @filter[:type] = arg } 89 | opts.on("-v", "--version VERSION", "Select agents with a given software version") { |arg| @filter[:version] = arg } 90 | 91 | opts.separator "" 92 | opts.separator "Notes:" 93 | opts.separator " - Filters are evaluated as: set | host | (env & version & type)" 94 | opts.separator " - The HOST, MACHINE, and TYPE arguments are regular expressions (not globs)" 95 | opts.separator " - The default filter selects all hosts" 96 | 97 | begin 98 | @original_args = ARGV.join(" ") 99 | @args = opts.parse! ARGV 100 | @filter[:command] = @original_args 101 | rescue Exception => msg 102 | puts opts 103 | puts msg 104 | exit 1 105 | end 106 | end 107 | 108 | def parse_command_line 109 | begin 110 | @options[:config_from_file] = Galaxy::Config::read_config_file(@options[:config_file]) 111 | get_command 112 | abort(usage_message) if @options[:help_requested] 113 | validate_options 114 | rescue CommandLineError => e 115 | puts usage_message if @command_class 116 | $stderr.puts "Error: #{e}" unless e.message.empty? 117 | exit(1) 118 | end 119 | end 120 | 121 | def get_command 122 | command_name = @args.shift 123 | unless @options[:help_requested] 124 | raise CommandLineError.new("Missing command") if command_name.nil? 125 | end 126 | 127 | unless command_name.nil? 128 | @command_class = Galaxy::Commands[command_name] 129 | if @command_class.nil? 130 | raise CommandLineError.new("Unrecognized command: #{command_name}") 131 | end 132 | @command = @command_class.new(@args, @options) 133 | end 134 | 135 | # If a host is removed from dns, it should then be possible to reap it from the gonsole, without 136 | # having to restart the gonsole. Don't bail out if the DNS does not exist anymore. 137 | # See GAL-290. 138 | begin 139 | @filter[:host] = canonical_hostname(@filter[:host]) if @filter[:host] 140 | rescue Exception => e 141 | raise CommandLineError.new("DNS error: #{e}") unless command_name == "reap" 142 | @filter[:host] = @filter[:host] 143 | end 144 | begin 145 | @filter[:machine] = canonical_hostname(@filter[:machine]) if @filter[:machine] 146 | rescue Exception => e 147 | raise CommandLineError.new("DNS error: #{e}") unless command_name == "reap" 148 | @filter[:machine] = @filter[:machine] 149 | end 150 | begin 151 | @options[:cohabitant_host] = canonical_hostname(@options[:cohabitant_host]) if @options[:cohabitant_host] 152 | rescue Exception => e 153 | raise CommandLineError.new("DNS error: #{e}") unless command_name == "reap" 154 | @options[:cohabitant_host] = @options[:cohabitant_host] 155 | end 156 | end 157 | 158 | def validate_options 159 | console_url = @options[:console_url] || @options[:config_from_file]['galaxy.client.console'] 160 | if console_url.nil? 161 | raise CommandLineError.new("Cannot determine console host; consider passing -c or setting GALAXY_CONSOLE") 162 | end 163 | @options[:console_url] = normalize_console_url(console_url) 164 | @options[:console] = Galaxy::Transport.locate(@options[:console_url]) 165 | 166 | if @options[:cohabitant_host] 167 | begin 168 | agents = @options[:console].agents({:host => @options[:cohabitant_host], :command => @original_args}) 169 | if agents.length != 1 170 | raise "Found #{agents.length} agents matching #{@options[:cohabitant_host]}" 171 | end 172 | @filter[:machine] = agents[0].machine 173 | rescue Exception => e 174 | raise "Unable to determine machine for host #{@options[:cohabitant_host]}: #{e}" 175 | end 176 | end 177 | end 178 | 179 | def usage_message 180 | @opts.separator "" 181 | 182 | if @command_class.nil? 183 | @opts.separator "Commands:" 184 | Galaxy::Commands.each do |command_name| 185 | @opts.separator " #{command_name}" 186 | end 187 | else 188 | @opts.separator "Usage for '#{@command_class.name}':" 189 | 190 | help = @command_class.help 191 | indent = help.scan(/^\s+/).first 192 | 193 | help.split("\n").each do |line| 194 | @opts.separator line.gsub(/^#{indent}/, " ") 195 | end 196 | end 197 | @opts.to_s 198 | end 199 | 200 | def get_agents 201 | @agents = @command.select_agents(@filter) 202 | rescue Exception => e 203 | abort("Error: #{e}") 204 | end 205 | 206 | def validate_agents 207 | # If command is show-console, it's okay not to have found any agent 208 | if @agents.length == 0 and @command.class.name != "show-console" 209 | abort("No agents matching the provided filter(s) were available for #{@command.class.name}") 210 | elsif @agents.length > 1 and (@command.changes_agent_state or @command.changes_console_state) and not@options[:implicit_confirmation] 211 | abort unless prompt_and_wait_for_user_confirmation("#{@agents.length} agents will be affected; continue? (y/n) ") 212 | end 213 | locate_agent_proxies # AgentProxy should provide this instead of having to instantiate it here 214 | end 215 | 216 | def locate_agent_proxies 217 | @agents.each { |agent| agent.proxy = Galaxy::Transport.locate(agent.url) if agent.url } 218 | end 219 | 220 | def run_command 221 | user = Galaxy::HostUtils::shell_user || 'unknown' 222 | message = "#{user} ran: galaxy " + @original_args 223 | stdout, stderr = @command.execute(@agents) 224 | puts stdout unless stdout.nil? 225 | $stderr.puts stderr unless stderr.nil? 226 | end 227 | 228 | exit(0) if @options[:version_requested] 229 | parse_command_line 230 | get_agents 231 | validate_agents 232 | run_command 233 | -------------------------------------------------------------------------------- /lib/galaxy/agent.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'logger' 3 | require 'ostruct' 4 | require 'resolv' 5 | require 'socket' 6 | require 'stringio' 7 | require 'yaml' 8 | 9 | require 'galaxy/agent_remote_api' 10 | require 'galaxy/config' 11 | require 'galaxy/controller' 12 | require 'galaxy/db' 13 | require 'galaxy/deployer' 14 | require 'galaxy/events' 15 | require 'galaxy/fetcher' 16 | require 'galaxy/log' 17 | require 'galaxy/properties' 18 | require 'galaxy/repository' 19 | require 'galaxy/software' 20 | require 'galaxy/starter' 21 | require 'galaxy/transport' 22 | require 'galaxy/version' 23 | require 'galaxy/versioning' 24 | 25 | module Galaxy 26 | class Agent 27 | attr_reader :host, :machine, :config, :locked, :logger, :gonsole_url 28 | attr_accessor :starter, :fetcher, :deployer, :db 29 | 30 | include Galaxy::AgentRemoteApi 31 | 32 | def initialize host, url, machine, announcements_url, repository_base, deploy_dir, 33 | data_dir, binaries_base, log, log_level, announce_interval, event_listener 34 | @drb_url = url 35 | @host = host 36 | @machine = machine 37 | @ip = Resolv.getaddress(@host) 38 | 39 | # Setup the logger and the event dispatcher (HDFS) if needed 40 | @logger = Galaxy::Log::Glogger.new log, event_listener, announcements_url, @ip 41 | @logger.log.level = log_level 42 | 43 | @lock = OpenStruct.new(:owner => nil, :count => 0, :mutex => Mutex.new) 44 | 45 | # set up announcements 46 | @gonsole_url = announcements_url 47 | @announcer = Galaxy::Transport.locate announcements_url, @logger 48 | 49 | # Setup event listener 50 | @event_dispatcher = Galaxy::GalaxyEventSender.new(event_listener, @gonsole_url, @ip, @logger) 51 | 52 | @announce_interval = announce_interval 53 | @prop_builder = Galaxy::Properties::Builder.new repository_base, @logger 54 | @repository = Galaxy::Repository.new repository_base, @logger 55 | @deployer = Galaxy::Deployer.new deploy_dir, @logger 56 | @fetcher = Galaxy::Fetcher.new binaries_base, @logger 57 | @starter = Galaxy::Starter.new @logger 58 | @db = Galaxy::DB.new data_dir 59 | @repository_base = repository_base 60 | @binaries_base = binaries_base 61 | 62 | if RUBY_PLATFORM =~ /\w+-(\D+)/ 63 | @os = $1 64 | @logger.debug "Detected OS: #{@os}" 65 | end 66 | 67 | @logger.debug "Detected machine: #{@machine}" 68 | 69 | @config = read_config current_deployment_number 70 | 71 | Galaxy::Transport.publish url, self 72 | announce 73 | sync_state! 74 | 75 | @thread = Thread.start do 76 | loop do 77 | sleep @announce_interval 78 | announce 79 | end 80 | end 81 | end 82 | 83 | def lock 84 | @lock.mutex.synchronize do 85 | raise "Agent is locked performing another operation" unless @lock.owner.nil? || @lock.owner == Thread.current 86 | 87 | @lock.owner = Thread.current if @lock.owner.nil? 88 | 89 | @logger.debug "Locking from #{caller[2]}" if @lock.count == 0 90 | @lock.count += 1 91 | end 92 | end 93 | 94 | def unlock 95 | @lock.mutex.synchronize do 96 | raise "Lock not owned by current thread" unless @lock.owner.nil? || @lock.owner == Thread.current 97 | @lock.count -= 1 98 | @lock.owner = nil if @lock.count == 0 99 | 100 | @logger.debug "Unlocking from #{caller[2]}" if @lock.count == 0 101 | end 102 | end 103 | 104 | def status 105 | OpenStruct.new( 106 | :host => @host, 107 | :ip => @ip, 108 | :url => @drb_url, 109 | :os => @os, 110 | :machine => @machine, 111 | :core_type => config.core_type, 112 | :config_path => config.config_path, 113 | :build => config.build, 114 | :status => @starter.status(config.core_base), 115 | :last_start_time => config.last_start_time, 116 | :agent_status => 'online', 117 | :galaxy_version => Galaxy::Version 118 | ) 119 | end 120 | 121 | def announce 122 | begin 123 | res = @announcer.announce status 124 | @event_dispatcher.dispatch_announce_success_event status 125 | return res 126 | rescue Exception => e 127 | error_reason = "Unable to communicate with console, #{e.message}" 128 | @logger.warn "Unable to communicate with console, #{e.message}" 129 | @logger.warn e 130 | @event_dispatcher.dispatch_announce_error_event error_reason 131 | end 132 | end 133 | 134 | def read_config deployment_number 135 | config = nil 136 | deployment_number = deployment_number.to_s 137 | data = @db[deployment_number] 138 | unless data.nil? 139 | begin 140 | config = YAML.load data 141 | unless config.is_a? OpenStruct 142 | config = nil 143 | raise "Expecting serialized OpenStruct" 144 | end 145 | rescue Exception => e 146 | @logger.warn "Error reading deployment descriptor: #{@db.file_for(deployment_number)}: #{e}" 147 | end 148 | end 149 | config ||= OpenStruct.new 150 | # Ensure autostart=true for pre-2.5 deployments 151 | if config.auto_start.nil? 152 | config.auto_start = true 153 | end 154 | config 155 | end 156 | 157 | def write_config deployment_number, config 158 | deployment_number = deployment_number.to_s 159 | @db[deployment_number] = YAML.dump config 160 | end 161 | 162 | def current_deployment_number 163 | @db['deployment'] ||= "0" 164 | @db['deployment'].to_i 165 | end 166 | 167 | def current_deployment_number= deployment_number 168 | deployment_number = deployment_number.to_s 169 | @db['deployment'] = deployment_number 170 | @config = read_config deployment_number 171 | end 172 | 173 | # private 174 | def sync_state! 175 | lock 176 | 177 | begin 178 | if @config 179 | # Get the status from the core 180 | status = @starter.status @config.core_base 181 | @config.state = status 182 | write_config current_deployment_number, @config 183 | end 184 | ensure 185 | unlock 186 | end 187 | end 188 | 189 | # Stop the agent 190 | def shutdown 191 | @starter.stop! config.core_base if config 192 | @thread.kill 193 | Galaxy::Transport.unpublish @drb_url 194 | end 195 | 196 | # Wait for the agent to finish 197 | def join 198 | @thread.join 199 | end 200 | 201 | # args: host => IP/Name to uniquely identify this agent 202 | # console => hostname of the console 203 | # repository => base of url to repository 204 | # binaries => base of url=l to binary repository 205 | # deploy_dir => /path/to/deployment 206 | # data_dir => /path/to/agent/data/storage 207 | # log => /path/to/log || STDOUT || STDERR || SYSLOG 208 | # url => url to listen on 209 | # event_listener => url of the event listener 210 | def Agent.start args 211 | host_url = args[:host] || "localhost" 212 | host_url = "druby://#{host_url}" unless host_url.match("^http://") || host_url.match("^druby://") # defaults to drb 213 | host_url = "#{host_url}:4441" unless host_url.match ":[0-9]+$" 214 | 215 | # default console to http/4442 unless specified 216 | console_url = args[:console] || "localhost" 217 | console_url = "http://" + console_url unless console_url.match("^http://") || console_url.match("^druby://") 218 | console_url += ":4442" unless console_url.match ":[0-9]+$" 219 | 220 | # need host as simple name without protocol or port 221 | host = args[:host] || "localhost" 222 | host = host.sub(/^http:\/\//, "") 223 | host = host.sub(/^druby:\/\//, "") 224 | host = host.sub(/:[0-9]+$/, "") 225 | 226 | if args[:machine] 227 | machine = args[:machine] 228 | else 229 | machine_file = args[:machine_file] || Galaxy::Config::DEFAULT_MACHINE_FILE 230 | if File.exists? machine_file 231 | File.open machine_file, "r" do |f| 232 | machine = f.read.chomp 233 | end 234 | else 235 | machine = Socket.gethostname 236 | end 237 | end 238 | 239 | agent = Agent.new host, 240 | host_url, 241 | machine, 242 | console_url, 243 | args[:repository] || "/tmp/galaxy-agent-properties", 244 | args[:deploy_dir] || "/tmp/galaxy-agent-deploy", 245 | args[:data_dir] || "/tmp/galaxy-agent-data", 246 | args[:binaries] || "http://localhost:8000", 247 | args[:log] || "STDOUT", 248 | args[:log_level] || Logger::INFO, 249 | args[:announce_interval] || 60, 250 | args[:event_listener] 251 | 252 | agent 253 | end 254 | 255 | private :initialize, :sync_state!, :config 256 | end 257 | 258 | end 259 | -------------------------------------------------------------------------------- /lib/galaxy/config.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'logger' 3 | require 'socket' 4 | require 'galaxy/host' 5 | 6 | module Galaxy 7 | module Config 8 | DEFAULT_HOST = ENV["GALAXY_HOST"] || "localhost" 9 | DEFAULT_LOG = ENV["GALAXY_LOG"] || "SYSLOG" 10 | DEFAULT_LOG_LEVEL = ENV["GALAXY_LOG_LEVEL"] || "INFO" 11 | DEFAULT_MACHINE_FILE = ENV["GALAXY_MACHINE_FILE"] || "" 12 | DEFAULT_AGENT_PID_FILE = ENV["GALAXY_AGENT_PID_FILE"] || "/tmp/galaxy-agent.pid" 13 | DEFAULT_CONSOLE_PID_FILE = ENV["GALAXY_CONSOLE_PID_FILE"] || "/tmp/galaxy-agent.pid" 14 | 15 | DEFAULT_PING_INTERVAL = 60 16 | 17 | def read_config_file config_file 18 | config_file = config_file || ENV['GALAXY_CONFIG'] 19 | unless config_file.nil? or config_file.empty? 20 | msg = "Cannot find configuration file: #{config_file}" 21 | unless File.exist?(config_file) 22 | # Log exception to syslog 23 | syslog_log msg 24 | raise msg 25 | end 26 | end 27 | config_files = [config_file, '/etc/galaxy.conf', '/usr/local/etc/galaxy.conf'].compact 28 | config_files.each do |file| 29 | begin 30 | File.open file, "r" do |f| 31 | return YAML.load(f.read) 32 | end 33 | rescue Errno::ENOENT 34 | end 35 | end 36 | # Fall through to empty config hash 37 | return {} 38 | end 39 | 40 | def set_host host_from_file 41 | @host ||= @config.host || host_from_file || begin 42 | Socket.gethostname rescue DEFAULT_HOST 43 | end 44 | end 45 | 46 | def set_machine machine_from_file 47 | @machine ||= @config.machine || machine_from_file 48 | end 49 | 50 | def set_pid_file pid_file_from_file 51 | @pid_file ||= @config.pid_file || pid_file_from_file 52 | end 53 | 54 | def set_user user_from_file 55 | @user ||= @config.user || user_from_file || nil 56 | end 57 | 58 | def set_log log_from_file 59 | @log ||= @config.log || log_from_file || DEFAULT_LOG 60 | 61 | begin 62 | # Check if we can log to it 63 | test_logger = Galaxy::Log::Glogger.new(@log) 64 | # Make sure to reap file descriptors (except STDOUT/STDERR/SYSLOG) 65 | test_logger.close unless @log == "STDOUT" or @log == "STDERR" or @log == "SYSLOG" 66 | rescue 67 | # Log exception to syslog 68 | syslog_log $! 69 | raise $! 70 | end 71 | 72 | return @log 73 | end 74 | 75 | def set_log_level log_level_from_file 76 | @log_level ||= begin 77 | log_level = @config.log_level || log_level_from_file || DEFAULT_LOG_LEVEL 78 | case log_level 79 | when "DEBUG" 80 | Logger::DEBUG 81 | when "INFO" 82 | Logger::INFO 83 | when "WARN" 84 | Logger::WARN 85 | when "ERROR" 86 | Logger::ERROR 87 | end 88 | end 89 | end 90 | 91 | def guess key 92 | val = self.send key 93 | puts " --#{correct key} #{val}" if @config.verbose 94 | val 95 | end 96 | 97 | def syslog_log e 98 | Syslog.open($0, Syslog::LOG_PID | Syslog::LOG_CONS) { |s| s.warning e } 99 | end 100 | 101 | module_function :read_config_file, :set_machine, :set_host, :set_pid_file, 102 | :set_log, :set_log_level, :set_user, :guess 103 | end 104 | 105 | class AgentConfigurator 106 | include Config 107 | 108 | def initialize config 109 | @config = config 110 | @config_from_file = read_config_file(config.config_file) 111 | end 112 | 113 | def correct key 114 | case key 115 | when :deploy_dir 116 | "deploy-to" 117 | when :data_dir 118 | "data-dir" 119 | when :announce_interval 120 | "announce-interval" 121 | else 122 | key 123 | end 124 | end 125 | 126 | def configure 127 | puts "startup configuration" if @config.verbose 128 | { 129 | :host => guess(:host), 130 | :machine => guess(:machine), 131 | :console => guess(:console), 132 | :repository => guess(:repository), 133 | :binaries => guess(:binaries), 134 | :deploy_dir => guess(:deploy_dir), 135 | :verbose => @config.verbose || false, 136 | :data_dir => guess(:data_dir), 137 | :log => guess(:log), 138 | :log_level => guess(:log_level), 139 | :pid_file => guess(:pid_file), 140 | :user => guess(:user), 141 | :announce_interval => guess(:announce_interval), 142 | :event_listener => guess(:event_listener) 143 | } 144 | end 145 | 146 | def log 147 | set_log @config_from_file['galaxy.agent.log'] 148 | end 149 | 150 | def log_level 151 | set_log_level @config_from_file['galaxy.agent.log-level'] 152 | end 153 | 154 | def pid_file 155 | set_pid_file @config_from_file['galaxy.agent.pid-file'] || 156 | DEFAULT_AGENT_PID_FILE 157 | end 158 | 159 | def user 160 | set_user @config_from_file['galaxy.agent.user'] 161 | end 162 | 163 | def machine 164 | set_machine @config_from_file['galaxy.agent.machine'] 165 | end 166 | 167 | def host 168 | set_host @config_from_file['galaxy.agent.host'] 169 | end 170 | 171 | def console 172 | @console ||= @config.console || @config_from_file['galaxy.agent.console'] 173 | end 174 | 175 | def repository 176 | @repository ||= @config.repository || @config_from_file['galaxy.agent.config-root'] 177 | end 178 | 179 | def binaries 180 | @binaries ||= @config.binaries || @config_from_file['galaxy.agent.binaries-root'] 181 | end 182 | 183 | def deploy_dir 184 | @deploy_dir ||= @config.deploy_dir || @config_from_file['galaxy.agent.deploy-dir'] || "#{HostUtils.avail_path}/galaxy-agent/deploy" 185 | FileUtils.mkdir_p(@deploy_dir) unless File.exists? @deploy_dir 186 | @deploy_dir 187 | end 188 | 189 | def data_dir 190 | @data_dir ||= @config.data_dir || @config_from_file['galaxy.agent.data-dir'] || "#{HostUtils.avail_path}/galaxy-agent/data" 191 | FileUtils.mkdir_p(@data_dir) unless File.exists? @data_dir 192 | @data_dir 193 | end 194 | 195 | def announce_interval 196 | @announce_interval ||= @config.announce_interval || @config_from_file['galaxy.agent.announce-interval'] || 60 197 | @announce_interval = @announce_interval.to_i 198 | end 199 | 200 | def event_listener 201 | @event_listener ||= @config.event_listener || @config_from_file['galaxy.agent.event_listener'] 202 | end 203 | end 204 | 205 | class ConsoleConfigurator 206 | include Config 207 | 208 | def initialize config 209 | @config = config 210 | @config_from_file = read_config_file(config.config_file) 211 | end 212 | 213 | def correct key 214 | case key 215 | when :data_dir 216 | return :data 217 | when :deploy_dir 218 | "deploy-to" 219 | when :ping_interval 220 | "ping-interval" 221 | else 222 | key 223 | end 224 | end 225 | 226 | def configure 227 | puts "startup configuration" if @config.verbose 228 | { 229 | :environment => guess(:environment), 230 | :verbose => @config.verbose || false, 231 | :log => guess(:log), 232 | :log_level => guess(:log_level), 233 | :pid_file => guess(:pid_file), 234 | :user => guess(:user), 235 | :host => guess(:host), 236 | :announcement_url => guess(:announcement_url), 237 | :ping_interval => guess(:ping_interval), 238 | :console_proxyied_url => guess(:console_proxyied_url), 239 | :event_listener => guess(:event_listener) 240 | } 241 | end 242 | 243 | def console_proxyied_url 244 | return @config.console_proxyied_url 245 | end 246 | 247 | def log 248 | set_log @config_from_file['galaxy.console.log'] 249 | end 250 | 251 | def log_level 252 | set_log_level @config_from_file['galaxy.console.log-level'] 253 | end 254 | 255 | def pid_file 256 | set_pid_file @config_from_file['galaxy.console.pid-file'] || 257 | DEFAULT_CONSOLE_PID_FILE 258 | end 259 | 260 | def user 261 | set_user @config_from_file['galaxy.console.user'] 262 | end 263 | 264 | def announcement_url 265 | @announcement_url ||= @config.announcement_url || @config_from_file['galaxy.console.announcement-url'] || "http://#{`hostname`.strip}" 266 | end 267 | 268 | def host 269 | set_host @config_from_file['galaxy.console.host'] 270 | end 271 | 272 | def ping_interval 273 | @ping_interval ||= @config.ping_interval || @config_from_file['galaxy.console.ping-interval'] || 60 274 | @ping_interval = @ping_interval.to_i 275 | end 276 | 277 | def event_listener 278 | @event_listener ||= @config.event_listener || @config_from_file['galaxy.console.event_listener'] 279 | end 280 | 281 | def environment 282 | @env ||= begin 283 | if @config.environment 284 | @config.environment 285 | elsif @config_from_file['galaxy.console.environment'] 286 | @config_from_file['galaxy.console.environment'] 287 | end 288 | end 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2010 Ning, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------