class OpenShift::Runtime::FrontendHttpServer

Frontend Http Server

Represents the front-end HTTP server on the system.

Note: This is the Apache VirtualHost implementation; other implementations may vary.

Attributes

container_name[R]
container_uuid[R]
fqdn[R]
namespace[R]

Public Class Methods

all() click to toggle source

Public: return an Enumerator which yields FrontendHttpServer objects for each gear which has run create.

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 127
def self.all
  Enumerator.new do |yielder|

    # Avoid deadlocks by listing the gears first
    gearlist = {}
    GearDB.open(GearDB::READER) do |d|
      d.each do |uuid, container|
        gearlist[uuid.clone] = container.clone
      end
    end

    gearlist.each do |uuid, container|
      frontend = nil
      begin

        frontend = FrontendHttpServer.new(ApplicationContainer.from_uuid(uuid))
      rescue => e
        NodeLogger.logger.error("Failed to instantiate FrontendHttpServer for #{uuid}: #{e}")
        NodeLogger.logger.error("Backtrace: #{e.backtrace}")
      else
        yielder.yield(frontend)
      end
    end
  end

end
json_create(obj) click to toggle source

Public: Load from json

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 261
def self.json_create(obj)
  data = obj['data']
  new_obj = FrontendHttpServer.new(ApplicationContainer.from_uuid(data['container_uuid']))
  new_obj.create

  if data.has_key?("connections")
    new_obj.connect(data["connections"])
  end

  if data.has_key?("aliases")
    data["aliases"].each do |a|
      new_obj.add_alias(a)
    end
  end

  if data.has_key?("ssl_certs")
    data["ssl_certs"].each do |c, k, a|
      new_obj.add_ssl_cert(c, k, a)
    end
  end

  if data.has_key?("idle")
    if data["idle"]
      new_obj.idle
    else
      new_obj.unidle
    end
  end

  if data.has_key?("sts")
    if data["sts"]
      new_obj.sts(data["sts"])
    else
      new_obj.no_sts
    end
  end

  new_obj
end
new(container) click to toggle source
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 154
def initialize(container)
  @config = ::OpenShift::Config.new

  @cloud_domain = clean_server_name(@config.get("CLOUD_DOMAIN"))

  @basedir = @config.get("OPENSHIFT_HTTP_CONF_DIR")

  @container_uuid = container.uuid
  @container_name = container.name
  @namespace = container.namespace

  @fqdn = nil

  # Did we save the old information?
  if (@container_name.to_s == "") or (@namespace.to_s == "")
    begin
      GearDB.open(GearDB::READER) do |d|
        @container_name = d.fetch(@container_uuid).fetch('container_name')
        @namespace = d.fetch(@container_uuid).fetch('namespace')
        @fqdn = d.fetch(@container_uuid).fetch('fqdn')
      end
    rescue
    end
  end

  # Could not infer from any source
  if (@container_name.to_s == "") or (@namespace.to_s == "")
    raise FrontendHttpServerException.new("Name or namespace not specified and could not infer it",
                                          @container_uuid)
  end

  if @fqdn.nil?
    @fqdn = clean_server_name("#{@container_name}-#{@namespace}.#{@cloud_domain}")
  end

end

Public Instance Methods

add_alias(name) click to toggle source

Public: Add an alias to this namespace

Examples

add_alias("foo.example.com")
# => nil

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 624
def add_alias(name)
  dname = clean_server_name(name)

  # Broker checks for global uniqueness
  ApacheDBAliases.open(ApacheDBAliases::WRCREAT) do |d|
    d.store(dname, @fqdn)
  end

  NodeJSDBRoutes.open(NodeJSDBRoutes::WRCREAT) do |d|
    begin
      routes_ent = d.fetch(@fqdn)
      if not routes_ent.nil?
        alias_ent = routes_ent.clone
        alias_ent["alias"] = @fqdn
        d.store(name, alias_ent)
      end
    rescue KeyError
    end
  end

end
add_ssl_cert(ssl_cert, priv_key, server_alias, passphrase='') click to toggle source

Public: Adds a ssl certificate for an alias

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 689
      def add_ssl_cert(ssl_cert, priv_key, server_alias, passphrase='')
        server_alias_clean = clean_server_name(server_alias)

        begin
          priv_key_clean = OpenSSL::PKey.read(priv_key, passphrase)
          ssl_cert_clean = []
          ssl_cert_unit = ""
          ssl_cert.each_line do |cert_line|
            ssl_cert_unit += cert_line
            if cert_line.start_with?('-----END')
              ssl_cert_clean << OpenSSL::X509::Certificate.new(ssl_cert_unit)
              ssl_cert_unit = ""
            end
          end
        rescue ArgumentError
          raise FrontendHttpServerException.new("Invalid Private Key or Passphrase",
                                                @container_uuid, @container_name,
                                                @namespace)
        rescue OpenSSL::X509::CertificateError => e
          raise FrontendHttpServerException.new("Invalid X509 Certificate: #{e.message}",
                                                @container_uuid, @container_name,
                                                @namespace)
        rescue => e
          raise FrontendHttpServerException.new("Other key/cert error: #{e.message}",
                                                @container_uuid, @container_name,
                                                @namespace)
        end

        if ssl_cert_clean.empty?
          raise FrontendHttpServerException.new("Could not parse certificates",
                                                @container_uuid, @container_name,
                                                @namespace)
        end

        if not ssl_cert_clean[0].check_private_key(priv_key_clean)
          raise FrontendHttpServerException.new("Key/cert mismatch",
                                                @container_uuid, @container_name,
                                                @namespace)
        end

        if not [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA].include?(priv_key_clean.class)
          raise FrontendHttpServerException.new("Key must be RSA or DSA for Apache mod_ssl",
                                                @container_uuid, @container_name,
                                                @namespace)
        end


        # Create a new directory for the alias and copy the certificates
        alias_token = "#{@container_uuid}_#{@namespace}_#{server_alias_clean}"
        alias_conf_dir_path = PathUtils.join(@basedir, alias_token)
        ssl_cert_file_path = PathUtils.join(alias_conf_dir_path, server_alias_clean + ".crt")
        priv_key_file_path = PathUtils.join(alias_conf_dir_path, server_alias_clean + ".key")

        #
        # Create configuration for the alias
        #

        # Create top level config file for the alias
        alias_conf_contents = <<-ALIAS_CONF_ENTRY
<VirtualHost *:443>
  ServerName #{server_alias_clean}
  ServerAdmin openshift-bofh@redhat.com
  DocumentRoot /var/www/html
  DefaultType None

  SSLEngine on

  SSLCertificateFile #{ssl_cert_file_path}
  SSLCertificateKeyFile #{priv_key_file_path}
  SSLCertificateChainFile #{ssl_cert_file_path}
  SSLCipherSuite RSA:!EXPORT:!DH:!LOW:!NULL:+MEDIUM:+HIGH
  SSLProtocol -ALL +SSLv3 +TLSv1
  SSLOptions +StdEnvVars +ExportCertData

  RequestHeader set X-Forwarded-Proto "https"
  RequestHeader set X-Forwarded-SSL-Client-Cert %{SSL_CLIENT_CERT}e

  RewriteEngine On
  include conf.d/openshift_route.include

</VirtualHost>
        ALIAS_CONF_ENTRY

        # Finally, commit the changes
        ApacheDBAliases.open(ApacheDBAliases::WRCREAT) do |d|
          if not (d.has_key? server_alias_clean)
            raise FrontendHttpServerException.new("Specified alias #{server_alias_clean} does not exist for the app",
                                                  @container_uuid, @container_name,
                                                  @namespace)
          end

          FileUtils.mkdir_p(alias_conf_dir_path)
          File.open(ssl_cert_file_path, 'w') { |f| f.write(ssl_cert_clean.map { |c| c.to_pem}.join) }
          File.open(priv_key_file_path, 'w') { |f| f.write(priv_key_clean.to_pem) }

          alias_conf_file_path = PathUtils.join(@basedir, "#{alias_token}.conf")
          File.open(alias_conf_file_path, 'w') { |f| f.write(alias_conf_contents) }

          # Reload httpd to pick up the new configuration
          reload_httpd
        end
      end
aliases() click to toggle source

Public: List aliases for this gear

Examples

aliases
# => ["foo.example.com", "bar.example.com"]
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 610
def aliases
  ApacheDBAliases.open(ApacheDBAliases::READER) do |d|
    return d.select { |k, v| v == @fqdn }.map { |k, v| k }
  end
end
clean_server_name(name) click to toggle source

Private: Validate the server name

The name is validated against DNS host name requirements from RFC 1123 and RFC 952. Additionally, OpenShift does not allow names/aliases to be an IP address.

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 821
def clean_server_name(name)
  dname = name.downcase

  if not dname =~ /^[a-z0-9]/
    raise FrontendHttpServerNameException.new("Invalid start character", @container_uuid,                                                       @container_name, @namespace, dname )
  end

  if not dname.index(/[^0-9a-z\-.]/).nil?
    raise FrontendHttpServerNameException.new("Invalid characters", @container_uuid,                                                       @container_name, @namespace, dname )
  end

  if dname.length > 255
    raise FrontendHttpServerNameException.new("Too long", @container_uuid,                                                      @container_name, @namespace, dname )
  end

  if dname.length == 0
    raise FrontendHttpServerNameException.new("Name was blank", @container_uuid,                                                      @container_name, @namespace, dname )
  end

  if dname =~ /^\d+\.\d+\.\d+\.\d+$/
    raise FrontendHttpServerNameException.new("IP addresses are not allowed", @container_uuid,                                                      @container_name, @namespace, dname )
  end

  return dname
end
connect(*elements) click to toggle source

Public: Connect path elements to a back-end URI for this namespace.

Examples

connect('', '127.0.250.1:8080')
connect('/', '127.0.250.1:8080/')
connect('/phpmyadmin', '127.0.250.2:8080/')
connect('/socket, '127.0.250.3:8080/', {"websocket"=>1}

    Options:
        websocket      Enable web sockets on a particular path
        gone           Mark the path as gone (uri is ignored)
        forbidden      Mark the path as forbidden (uri is ignored)
        noproxy        Mark the path as not proxied (uri is ignored)
        redirect       Use redirection to uri instead of proxy (uri must be a path)
        file           Ignore request and load file path contained in uri (must be path)
        tohttps        Redirect request to https and use the path contained in the uri (must be path)
        target_update  Preserve existing options and update the target.
    While more than one option is allowed, the above options conflict with each other.
    Additional options may be provided which are target specific.
# => nil

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 325
def connect(*elements)
  working_elements = []

  elements.flatten.enum_for(:each_slice, 3).each do |path, uri, options|
    if options["target_update"]
      ApacheDBNodes.open(ApacheDBNodes::READER) do |d|
        begin
          old_conn = d.fetch(@fqdn + path.to_s)
          options = decode_connection(path.to_s, old_conn)[2]
        rescue KeyError
          raise FrontendHttpServerException.new("The target_update option specified but no old configuration: #{path}",
                                                @container_uuid, @container_name, @namespace)
        end
      end
    end
    working_elements << [path, uri, options]
  end

  ApacheDBNodes.open(ApacheDBNodes::WRCREAT) do |d|
    working_elements.each do |path, uri, options|
      if options["gone"]
        map_dest = "GONE"
      elsif options["forbidden"]
        map_dest = "FORBIDDEN"
      elsif options["noproxy"]
        map_dest = "NOPROXY"
      elsif options["health"]
        map_dest = "HEALTH"
      elsif options["redirect"]
        map_dest = "REDIRECT:#{uri}"
      elsif options["file"]
        map_dest = "FILE:#{uri}"
      elsif options["tohttps"]
        map_dest = "TOHTTPS:#{uri}"
      else
        map_dest = uri
      end

      if options["websocket"]
        connect_websocket(path, uri, options)
      else
        disconnect_websocket(path) # We could be changing a path
      end

      d.store(@fqdn + path.to_s, map_dest)
    end
  end

end
connect_websocket(path, uri, options) click to toggle source
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 375
def connect_websocket(path, uri, options)

  if path != ""
    raise FrontendHttpServerException.new("Path must be empty for a websocket: #{path}",
                                          @container_uuid, @container_name, @namespace)

  end

  conn = options["connections"]
  if conn.nil?
    conn = 5
  end

  bw = options["bandwidth"]
  if bw.nil?
    bw = 100
  end

  # Use the websocket port if it is passed as an option
  port = options["websocket_port"]
  if port
    uri = uri.sub(/:(\d)+/, ":" + port.to_s)
  end

  routes_ent = {
    "endpoints" => [ uri ],
    "limits"    => {
      "connections" => conn,
      "bandwidth"   => bw
    }
  }

  NodeJSDBRoutes.open(NodeJSDBRoutes::WRCREAT) do |d|
    d.store(@fqdn, routes_ent)
  end

end
connections() click to toggle source

Public: List connections Returns [ [path, uri, options], [path, uri, options], …]

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 440
def connections
  # We can't simply rely on the open returning the block's value in unit testing.
  # http://rubyforge.org/tracker/?func=detail&atid=7477&aid=8687&group_id=1917
  entries = nil
  ApacheDBNodes.open(ApacheDBNodes::READER) do |d|
    entries = d.select { |k, v|
      k.split('/')[0] == @fqdn
    }.map { |k, v|
      decode_connection(k.sub(@fqdn, ""), v)
    }
  end
  entries
end
create() click to toggle source

Public: Initialize a new configuration for this gear

Examples

create
# => nil

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 199
def create
  GearDB.open(GearDB::WRCREAT) do |d|
    d.store(@container_uuid, {'fqdn' => @fqdn,  'container_name' => @container_name, 'namespace' => @namespace})
  end
end
decode_connection(path, connection) click to toggle source
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 413
def decode_connection(path, connection)
  entry = [ path, "", {} ]

  if entry[0] == ""
    begin
      NodeJSDBRoutes.open(NodeJSDBRoutes::READER) do |d|
        routes_ent = d.fetch(@fqdn)
        entry[2].merge!(routes_ent["limits"])
        entry[2]["websocket"]=1
      end
    rescue
    end
  end

  if connection =~ /^(GONE|FORBIDDEN|NOPROXY|HEALTH)$/
    entry[2][$~[1].downcase] = 1
  elsif connection =~ /^(REDIRECT|FILE|TOHTTPS):(.*)$/
    entry[2][$~[1].downcase] = 1
    entry[1] = $~[2]
  else
    entry[1] = connection
  end
  entry
end
destroy() click to toggle source

Public: Remove the frontend httpd configuration for a gear.

Examples

destroy
# => nil

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 213
def destroy
  ApacheDBNodes.open(ApacheDBNodes::WRCREAT)     { |d| d.delete_if { |k, v| k.split('/')[0] == @fqdn } }
  ApacheDBAliases.open(ApacheDBAliases::WRCREAT) { |d| d.delete_if { |k, v| v == @fqdn } }
  ApacheDBIdler.open(ApacheDBIdler::WRCREAT)     { |d| d.delete(@fqdn) }
  ApacheDBSTS.open(ApacheDBSTS::WRCREAT)         { |d| d.delete(@fqdn) }
  NodeJSDBRoutes.open(NodeJSDBRoutes::WRCREAT)   { |d| d.delete_if { |k, v| (k == @fqdn) or (v["alias"] == @fqdn) } }
  GearDB.open(GearDB::WRCREAT)                   { |d| d.delete(@container_uuid) }

  # Clean up SSL certs and legacy node configuration
  ApacheDBAliases.open(ApacheDBAliases::WRCREAT) do
    paths = Dir.glob(PathUtils.join(@basedir, "#{container_uuid}_*"))
    FileUtils.rm_rf(paths)
    paths.each do |p|
      if p =~ /\.conf$/
        begin
          reload_httpd
        rescue
        end
        break
      end
    end
  end

end
disconnect(*paths) click to toggle source

Public: Disconnect a path element from this namespace

Examples

disconnect('')
disconnect('/')
disconnect('/phpmyadmin)
disconnect('/a', '/b', '/c')

# => nil

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 466
def disconnect(*paths)
  ApacheDBNodes.open(ApacheDBNodes::WRCREAT) do |d|
    paths.flatten.each do |p|
      d.delete(@fqdn + p.to_s)
    end
  end
  disconnect_websocket(*paths)
end
disconnect_websocket(*paths) click to toggle source
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 475
def disconnect_websocket(*paths)
  if paths.flatten.include?("")
    NodeJSDBRoutes.open(NodeJSDBRoutes::WRCREAT) do |d|
      d.delete(@fqdn)
    end
  end
end
get_sts() click to toggle source

Public: Determine whether the gear has sts

Examples

sts?

# => true or false

Returns true if the gear is idled

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 594
def get_sts
  ApacheDBSTS.open(ApacheDBSTS::READER) do |d|
    if d.has_key?(@fqdn)
      return d.fetch(@fqdn)
    end
  end
  nil
end
idle() click to toggle source

Public: Mark a gear as idled

Examples

idle()

# => nil()

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 492
def idle
  ApacheDBIdler.open(ApacheDBIdler::WRCREAT) do |d|
    d.store(@fqdn, @container_uuid)
  end
end
idle?() click to toggle source

Public: Determine whether the gear is idle

Examples

idle?

# => true or false

Returns true if the gear is idled

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 545
def idle?
  ApacheDBIdler.open(ApacheDBIdler::READER) do |d|
    return d.has_key?(@fqdn)
  end
end
no_sts() click to toggle source

Public: Unmark a gear for sts

Examples

nosts()

# => nil()

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 580
def no_sts
  ApacheDBSTS.open(ApacheDBSTS::WRCREAT) do |d|
    d.delete(@fqdn)
  end
end
reload_httpd(async=false) click to toggle source

Reload the Apache configuration

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 853
def reload_httpd(async=false)
  async_opt="-b" if async
  begin
    ::OpenShift::Runtime::Utils::oo_spawn("/usr/sbin/oo-httpd-singular #{async_opt} graceful", {:expected_exitstatus=>0})
  rescue ::OpenShift::Runtime::Utils::ShellExecutionException => e
    logger.error("ERROR: failure from oo-httpd-singular(#{e.rc}): #{@uuid} stdout: #{e.stdout} stderr:#{e.stderr}")
    raise FrontendHttpServerExecException.new(e.message, @container_uuid, @container_name, @namespace, e.rc, e.stdout, e.stderr)
  end
end
remove_alias(name) click to toggle source

Public: Removes an alias from this namespace

Examples

add_alias("foo.example.com")
# => nil

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 654
def remove_alias(name)
  dname = clean_server_name(name)

  ApacheDBAliases.open(ApacheDBAliases::WRCREAT) do |d|
    d.delete(dname)
  end

  NodeJSDBRoutes.open(NodeJSDBRoutes::WRCREAT) do |d|
    d.delete(dname)
  end

  remove_ssl_cert(dname)
end
remove_ssl_cert(server_alias) click to toggle source

Public: Removes ssl certificate/private key associated with an alias

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 793
def remove_ssl_cert(server_alias)
  server_alias_clean = clean_server_name(server_alias)

  #
  # Remove the alias specific configuration
  #
  alias_token = "#{@container_uuid}_#{@namespace}_#{server_alias_clean}"

  alias_conf_dir_path = PathUtils.join(@basedir, alias_token)
  alias_conf_file_path = PathUtils.join(@basedir, "#{alias_token}.conf")

  if File.exists?(alias_conf_file_path) or File.exists?(alias_conf_dir_path)
    ApacheDBAliases.open(ApacheDBAliases::WRCREAT) do

      FileUtils.rm_rf(alias_conf_file_path)
      FileUtils.rm_rf(alias_conf_dir_path)

      # Reload httpd to pick up the configuration changes
      reload_httpd
    end
  end
end
ssl_certs() click to toggle source

Public: List aliases with SSL certs and unencrypted private keys

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 669
def ssl_certs
  aliases.map { |a|
    alias_token = "#{@container_uuid}_#{@namespace}_#{a}"
    alias_conf_dir_path = PathUtils.join(@basedir, alias_token)
    ssl_cert_file_path = PathUtils.join(alias_conf_dir_path, a + ".crt")
    priv_key_file_path = PathUtils.join(alias_conf_dir_path, a + ".key")

    begin
      ssl_cert = File.read(ssl_cert_file_path)
      priv_key = File.read(priv_key_file_path)
    rescue
      ssl_cert = nil
      priv_key = nil
    end

    [ ssl_cert, priv_key, a ]
  }.select { |e| e[0] != nil }
end
sts(max_age=15768000) click to toggle source

Public: Mark a gear for STS

Examples

sts(duration)

# => nil()

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 561
def sts(max_age=15768000)
  ApacheDBSTS.open(ApacheDBSTS::WRCREAT) do |d|
    if max_age.nil?
      d.delete(@fqdn)
    else
      d.store(@fqdn, max_age.to_i)
    end
  end
end
to_hash() click to toggle source

Public: extract hash version of complete data for this gear

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 239
def to_hash
  {
    "container_uuid" => @container_uuid,
    "container_name" => @container_name,
    "namespace"      => @namespace,
    "connections"    => connections,
    "aliases"        => aliases,
    "ssl_certs"      => ssl_certs,
    "idle"           => idle?,
    "sts"            => get_sts
  }
end
to_json(*args) click to toggle source

Public: Generate json

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 253
def to_json(*args)
  {
    'json_class' => self.class.name,
    'data'       => self.to_hash
  }.to_json(*args)
end
unidle() click to toggle source

Public: Unmark a gear as idled

Examples

unidle()

# => nil()

Returns nil on Success or raises on Failure

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 507
def unidle
  ApacheDBIdler.open(ApacheDBIdler::WRCREAT) do |d|
    d.delete(@fqdn)
  end
end
unprivileged_unidle() click to toggle source

Public: Make an unprivileged call to unidle the gear

Examples

unprivileged_unidle()

# => nil()

Returns nil. This is an opportunistic call, failure conditions are ignored but the call may take over a minute to complete.

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 523
def unprivileged_unidle
  begin
    http = Net::HTTP.new('127.0.0.1', 80)
    http.open_timeout = 5
    http.read_timeout = 60
    http.use_ssl = false
    http.start do |client|
      resp = client.request_head('/', { 'Host' => @fqdn })
      resp.code
    end
  rescue
  end
end