├── 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 |
--------------------------------------------------------------------------------