class OpenShift::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

json_create(obj) click to toggle source

Public: Load from json

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 216
def self.json_create(obj)
  data = obj['data']
  new_obj = new(data['container_uuid'],
                data['container_name'],
                data['namespace'])
  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 |a, c, k|
      new_obj.add_ssl_cert(a, c, k)
    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_uuid, container_name=nil, namespace=nil) click to toggle source
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 113
def initialize(container_uuid, container_name=nil, namespace=nil)
  @config = OpenShift::Config.new

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

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

  @container_uuid = container_uuid
  @container_name = container_name
  @namespace = namespace

  @fqdn = nil

  # Attempt to infer from the gear itself
  if (@container_name.to_s == "") or (@namespace.to_s == "")
    begin
      env = Utils::Environ.for_gear(File.join(@config.get("GEAR_BASE_DIR"), @container_uuid))

      @container_name = env['OPENSHIFT_GEAR_NAME']
      @fqdn = env['OPENSHIFT_GEAR_DNS']

      @namespace = @fqdn.sub(%r\.#{@cloud_domain}$/,"").sub(%r^#{@container_name}\-/,"")
    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 592
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 657
    def add_ssl_cert(ssl_cert, priv_key, server_alias, passphrase='')
      server_alias_clean = clean_server_name(server_alias)

      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
      end

      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
        raise FrontendHttpServerException.new("Invalid X509 Certificate",
                                              @container_uuid, @container_name,
                                              @namespace)
      rescue => e
        raise FrontendHttpServerException.new("Other key/cert error: #{e}",
                                              @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 = File.join(@basedir, alias_token)
      ssl_cert_file_path = File.join(alias_conf_dir_path, server_alias_clean + ".crt")
      priv_key_file_path = File.join(alias_conf_dir_path, server_alias_clean + ".key")
      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) }

      #
      # Create configuration for the alias
      #

      # Create top level config file for the alias
      alias_conf_contents = "<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
  # SSLVerifyClient must be set for +ExportCertData to take effect, so just use
  # optional_no_ca.
  SSLVerifyClient optional_no_ca

  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_file_path = File.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
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 578
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 788
def clean_server_name(name)
  dname = name.downcase

  if not dname.index(%r[^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 =~ %r^\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)
    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 343
def connect(*elements)

  ApacheDBNodes.open(ApacheDBNodes::WRCREAT) do |d|

    elements.flatten.enum_for(:each_slice, 3).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["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 377
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

  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 411
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|
      entry = [ k.sub(@fqdn, ""), "", {} ]

      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 v =~ %r^(GONE|FORBIDDEN|NOPROXY)$/
        entry[2][$~[1].downcase] = 1
      elsif v =~ %r^(REDIRECT|FILE|TOHTTPS):(.*)$/
        entry[2][$~[1].downcase] = 1
        entry[1] = $~[2]
      else
        entry[1] = v
      end
      entry
    }
  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 159
def create
  # Reserved for future use.
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 171
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) } }

  # Clean up SSL certs and legacy node configuration
  paths = Dir.glob(File.join(@basedir, "#{container_uuid}_*"))
  FileUtils.rm_rf(paths)
  paths.each do |p|
     if p =~ %r\.conf$/
       begin
         reload_httpd
       rescue
       end
       break
     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 458
def disconnect(*paths)
  ApacheDBNodes.open(ApacheDBNodes::WRCREAT) do |d|
    paths.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 467
def disconnect_websocket(*paths)
  if paths.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 562
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 484
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 513
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 548
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 815
def reload_httpd(async=false)
  async_opt="-b" if async
  begin
    Utils::oo_spawn("/usr/sbin/oo-httpd-singular #{async_opt} graceful", {:expected_exitstatus=>0})
  rescue 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 622
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 762
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 = File.join(@basedir, alias_token)
  alias_conf_file_path = File.join(@basedir, "#{alias_token}.conf")

  if File.exists?(alias_conf_file_path) or File.exists?(alias_conf_dir_path)

    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
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 637
def ssl_certs
  aliases.map { |a|
    alias_token = "#{@container_uuid}_#{@namespace}_#{a}"
    alias_conf_dir_path = File.join(@basedir, alias_token)
    ssl_cert_file_path = File.join(alias_conf_dir_path, a + ".crt")
    priv_key_file_path = File.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 529
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 194
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 208
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 499
def unidle
  ApacheDBIdler.open(ApacheDBIdler::WRCREAT) do |d|
    d.delete(@fqdn)
  end
end
update(container_name, namespace) click to toggle source

Public: Update identifier to the new names

# File lib/openshift-origin-node/model/frontend_httpd.rb, line 260
def update(container_name, namespace)
  new_fqdn = clean_server_name("#{container_name}-#{namespace}.#{@cloud_domain}")

  ApacheDBNodes.open(ApacheDBNodes::WRCREAT) do |d|
    d.update_block do |deletions, updates, k, v|
      if k.split('/')[0] == @fqdn
        deletions << k
        updates[k.sub(@fqdn, new_fqdn)] = v
      end
    end
  end

  ApacheDBAliases.open(ApacheDBAliases::WRCREAT) do |d|
    d.update_block do |deletions, updates, k, v|
      if v == @fqdn
        updates[k]=new_fqdn
      end
    end
  end

  ApacheDBIdler.open(ApacheDBIdler::WRCREAT) do |d|
    d.update_block do |deletions, updates, k, v|
      if k == @fqdn
        deletions << k
        updates[new_fqdn] = v
      end
    end
  end

  ApacheDBSTS.open(ApacheDBSTS::WRCREAT) do |d|
    d.update_block do |deletions, updates, k, v|
      if k == @fqdn
        deletions << k
        updates[new_fqdn] = v
      end
    end
  end

  NodeJSDBRoutes.open(NodeJSDBRoutes::WRCREAT) do |d|
    d.update_block do |deletions, updates, k, v|
      if k == @fqdn
        deletions << k
        updates[new_fqdn] = v
      end
    end
  end

  @container_name = container_name
  @namespace = namespace
  @fqdn = new_fqdn
end
update_name(container_name) click to toggle source
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 312
def update_name(container_name)
  update(container_name, @namespace)
end
update_namespace(namespace) click to toggle source
# File lib/openshift-origin-node/model/frontend_httpd.rb, line 316
def update_namespace(namespace)
  update(@container_name, namespace)
end