Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 2e52e2cda6db3e35645bb5c8bac56e8c035423a8 0 parents
Kyle Kingsbury authored
8 .gitignore
@@ -0,0 +1,8 @@
+pkg/
+log/
+._*
+.*.swp
+*~
+.DS_Store
+*.gemspec
+/test
3  .gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/net-ssh-shell"]
+ path = lib/net-ssh-shell
+ url = git://github.com/jamis/net-ssh-shell.git
13 README
@@ -0,0 +1,13 @@
+Hydra
+=====
+
+Hydra is a build system for administering servers. It is designed to run froma central, trusted distribution server, and uses SSH+sudo to update various production server aspects.
+
+Will support
+------------
+ - Firewall configuration
+ - Haproxy, Nginx, Monit configuration
+ - Apt-get updates
+ - Gem requirements
+ - SVN deploys
+ - Git deploys
6 bin/hydra
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+
+require "#{File.dirname(__FILE__)}/../lib/hydra"
+@h = Hydra.new
+@h.load 'common/**/*.rb'
+@h.load *ARGV
105 lib/hydra.rb
@@ -0,0 +1,105 @@
+require 'rubygems'
+require 'net/ssh'
+require 'net/ssh/gateway'
+
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+
+class Hydra
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'net-ssh-shell', 'lib'))
+ require 'net/ssh/shell'
+ require 'snippets/instance_exec'
+ require 'hydra/task'
+ require 'hydra/role'
+ require 'hydra/host'
+
+ attr_accessor :gw, :hosts, :roles, :tasks
+
+ def initialize
+ @gw = nil
+ @hosts = []
+ @roles = []
+ @tasks = []
+ end
+
+ # Default SSH gateway.
+ def gw(name = nil, &block)
+ # Get gateway from cache or set new one.
+ if name
+ # Set gateway
+ @gw = Hydra::Host.new(name, :hydra => self)
+ end
+
+ if block_given?
+ @gw.instance_exec &block
+ end
+
+ @gw
+ end
+
+ def host(name, &block)
+ unless host = @hosts.find{|h| h.name == name}
+ host = Hydra::Host.new(name, :hydra => self)
+ @hosts << host
+ end
+
+ if block_given?
+ host.instance_exec &block
+ end
+
+ host
+ end
+
+ # Loads one or more file globs into the current hydra.
+ def load(*globs)
+ globs.each do |glob|
+ Dir.glob(glob).each do |path|
+ instance_eval(File.read(path), path)
+ end
+ end
+ end
+
+ # Defines a new role. A role is a package of tasks.
+ def role(name, &block)
+ unless role = @roles.find{|r| r.name == name}
+ role = Hydra::Role.new(name, :hydra => self)
+ @roles << role
+ end
+
+ if block_given?
+ role.instance_eval &block
+ end
+
+ role
+ end
+
+ # Yields or returns an SSH connection to the given Host.
+ def ssh(host, opts = {})
+ if gw = opts[:through] or gw = @gw
+ @gateway ||= Net::SSH::Gateway.new(gw.name, gw.user)
+ if block_given?
+ @gateway.ssh(host.name, host.user) do |ssh|
+ yield ssh
+ end
+ else
+ @gateway.ssh(host.name, host.user)
+ end
+ end
+ end
+
+ # Finds (and optionally defines) a task.
+ # task :foo => returns a Task
+ # task :foo do ... end => defines a Task with given block
+ def task(name, &block)
+ unless task = @tasks.find{|t| t.name == name}
+ task = Hydra::Task.new(name, :hydra => self)
+ @tasks << task
+ end
+
+ if block_given?
+ task.block = block
+ end
+
+ task
+ end
+
+end
17 lib/hydra/config.rb
@@ -0,0 +1,17 @@
+module Hydra
+ def self.config
+ @config
+ end
+
+ def self.config=(config)
+ @config = config
+ end
+
+ def self.load_config(file)
+ @config = Construct.load(file)
+
+ @config.define :hosts, :default => []
+
+ @config
+ end
+end
168 lib/hydra/host.rb
@@ -0,0 +1,168 @@
+class Hydra::Host
+ attr_accessor :name, :user, :roles, :tasks, :hydra
+ def initialize(name, opts = {})
+ @name = name
+ @user = opts[:user]
+ @roles = opts[:roles] || []
+ @tasks = opts[:tasks] || []
+ @hydra = opts[:hydra]
+ end
+
+ def ==(other)
+ self.name == other.name
+ end
+
+ # Quotes a string for inclusion in a bash command line
+ def escape(string)
+ '"' + string.to_s.gsub(/[\\\$`"]/) { |match| '\\' + match } + '"'
+ end
+
+ # Returns true if a directory exists
+ def dir?(path)
+ ftype(path) == :directory rescue false
+ end
+
+ # Runs a remote command.
+ def exec!(*args)
+ response = @ssh.exec! *args
+ response
+ end
+
+ # Returns true when a file exists, otherwise false
+ def exists?(path)
+ true if ftype(path) rescue false
+ end
+
+ # Returns true if a regular file exists.
+ def file?(path)
+ ftype(path) == :file rescue false
+ end
+
+ # Returns the filetype, as symbol. Raises exceptions on failed stat.
+ def ftype(path)
+ stat = self.stat path
+ begin
+ stat.split("\n")[1].split(/\s+/).last.to_sym
+ rescue
+ if stat =~ /no such file or directory/i
+ raise Errno::ENOENT, "#{self}:#{path} does not exist"
+ else
+ raise RuntimeError, "stat #{self}:#{path} failed - #{stat}"
+ end
+ end
+ end
+
+ def inspect
+ "#<#{@user}@#{@name} roles=#{@roles.inspect} tasks=#{@tasks.inspect}>"
+ end
+
+ # Missing methods are resolved as follows:
+ # 1. From task_resolve
+ # 2. Converted to a command string and exec!'ed
+ def method_missing(meth, *args)
+ if task = resolve_task(meth)
+ task.run(self, *args)
+ else
+ str = ([meth] + args.map{|a| escape(a)}).join(' ')
+ exec! str
+ end
+ end
+
+ # Opens a shell.
+ def shell(&block)
+ ssh.shell do |sh|
+ sh.instance_exec(&block)
+ end
+ end
+
+ # Opens an SSH tunnel and stores the connection in @ssh.
+ def ssh
+ if @ssh
+ if @ssh.open?
+ return @ssh
+ else
+ @ssh.close
+ @ssh = @hydra.ssh self
+ end
+ else
+ @ssh = @hydra.ssh self
+ end
+ end
+
+ # Finds a task for this host, by name.
+ def resolve_task(name)
+ @tasks.each do |task|
+ return task if task.name == name
+ end
+ @roles.each do |role|
+ role.tasks.each do |task|
+ return task if task.name == name
+ end
+ end
+ nil
+ end
+
+ # Assigns roles to a host from the Hydra. Roles are unique in hosts; repeat
+ # assignments will not result in more than one copy of the role.
+ def role(role)
+ @roles = @roles | [@hydra.role(role)]
+ end
+
+ # Run a task, tasks, or all defined tasks.
+ # Tasks are run in the order given.
+ # Tasks are run with this host as context.
+ def run(*task_names)
+ ssh do
+ task_names.each do |name|
+ task = resolve_task(name) or raise RuntimeError, "no such task #{name} on #{self}"
+ task.run(self)
+ end
+ end
+ end
+
+ # Finds (and optionally defines) a task.
+ #
+ # Tasks are first resolved in the host's task list, then in the Hydra's task
+ # list. Finally, tasks are created from scratch. Any invocation of task adds
+ # that task to this host.
+ #
+ # If a block is given, the block is assigned to the local (host) task. The
+ # task is dup'ed to prevent modifying a possible global task.
+ #
+ # The task is returned at the end of the method.
+ def task(name, &block)
+ if task = @tasks.find{|t| t.name == name}
+ # Found in self
+ elsif (task = @hydra.tasks.find{|t| t.name == name}) and not block_given?
+ # Found in hydra
+ @tasks << task
+ else
+ # Create new task in self
+ task = Hydra::Task.new(name, :hydra => @hydra)
+ @tasks << task
+ end
+
+ if block_given?
+ # Remove the task from our list, and replace it with a copy.
+ # This is to prevent local declarations from clobbering global tasks.
+ i = @tasks.index(task) || @task.size
+ task = task.dup
+ task.block = block
+ @tasks[i] = task
+ end
+
+ task
+ end
+
+ def to_s
+ @name.to_s
+ end
+
+ def user(user = nil)
+ if user
+ @user = user
+ else
+ @user
+ end
+ end
+end
73 lib/hydra/role.rb
@@ -0,0 +1,73 @@
+class Hydra::Role
+ # A role is a list of tasks.
+ attr_reader :name, :tasks, :hydra
+
+ def initialize(name, opts = {})
+ @name = name
+ @tasks = []
+ @hydra = opts[:hydra]
+ end
+
+ # Runs the block in the context of each.
+ def each_host(&block)
+ hosts.each do |host|
+ host.instance_exec &block
+ end
+ end
+
+ # Returns an array of all hosts in this hydra which include this role.
+ def hosts
+ @hydra.hosts.select do |host|
+ host.roles.include? self
+ end
+ end
+
+ def inspect
+ "#<Role #{name} tasks=#{@tasks.inspect}>"
+ end
+
+ # Runs all tasks in sequence, in a given context
+ def run(context = nil)
+ tasks.each do |task|
+ task.run(context)
+ end
+ end
+
+ # Finds (and optionally defines) a task.
+ #
+ # Tasks are first resolved in the role's task list, then in the Hydra's task
+ # list. Finally, tasks are created from scratch. Any invocation of task adds
+ # that task to this role.
+ #
+ # If a block is given, the block is assigned to the local (role) task. The
+ # task is dup'ed to prevent modifying a possible global task.
+ #
+ # The task is returned at the end of the method.
+ def task(name, &block)
+ if task = @tasks.find{|t| t.name == name}
+ # Found in self
+ elsif (task = @hydra.tasks.find{|t| t.name == name}) and not block_given?
+ # Found in hydra
+ @tasks << task
+ else
+ # Create new task in self
+ task = Hydra::Task.new(name, :hydra => @hydra)
+ @tasks << task
+ end
+
+ if block_given?
+ # Remove the task from our list, and replace it with a copy.
+ # This is to prevent local declarations from clobbering global tasks.
+ i = @tasks.index(task) || @task.size
+ task = task.dup
+ task.block = block
+ @tasks[i] = task
+ end
+
+ task
+ end
+
+ def to_s
+ @name.to_s
+ end
+end
35 lib/hydra/task.rb
@@ -0,0 +1,35 @@
+class Hydra::Task
+ # A named block, runnable in some context
+ attr_accessor :name, :block
+
+ def initialize(name, opts = {})
+ @name = name
+ end
+
+ def ==(other)
+ self.name == other.name and
+ self.block == other.block
+ end
+
+ def dup
+ dup = Hydra::Task.new(@name)
+ dup.block = @block
+ dup
+ end
+
+ def inspect
+ "#<Task #{@name}>"
+ end
+
+ # Runs the task in a given context
+ def run(context = nil, *args)
+ if context
+ context.instance_exec(*args, &@block)
+ else
+ @block.call(*args)
+ end
+ end
+
+ def to_s
+ end
+end
1  lib/net-ssh-shell
@@ -0,0 +1 @@
+Subproject commit def7a172f8d527aecde8842bd6c00d77f2a9ed78
14 lib/snippets/instance_exec.rb
@@ -0,0 +1,14 @@
+class Object
+ module InstanceExecHelper; end
+ include InstanceExecHelper
+ def instance_exec(*args, &block) # !> method redefined; discarding old instance_exec
+ mname = "__instance_exec_#{Thread.current.object_id.abs}_#{object_id.abs}"
+ InstanceExecHelper.module_eval{ define_method(mname, &block) }
+ begin
+ ret = send(mname, *args)
+ ensure
+ InstanceExecHelper.module_eval{ undef_method(mname) } rescue nil
+ end
+ ret
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.