# Copyright

Copyright © 2014 eGlobalTech, Inc., all rights reserved

# # Licensed under the BSD-3 license (the “License”); # you may not use this file except in compliance with the License. # You may obtain a copy of the License in the root of the project or at # # egt-labs.com/mu/LICENSE.html # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an “AS IS” BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.

require 'pp' require 'base64' require 'etc'

Etc.getpwuid(Process.uid).dir

if !ENV.include?('MU_INSTALLDIR')

ENV['MU_INSTALLDIR'] = "/opt/mu"

end if !ENV.include?('MU_LIBDIR')

ENV['MU_LIBDIR'] = ENV['MU_INSTALLDIR']+"/lib"

end

$MUDIR = ENV

$LOAD_PATH << “#{$MUDIR}/modules”

require File.realpath(File.expand_path(File.dirname(__FILE__)+“/mu-load-config.rb”)) require 'mu'

begin

MU::Groomer::Chef.loadChefLib # pre-cache this so we don't take a hit on a user-interactive need
$ENABLE_SCRATCHPAD = true

rescue LoadError

MU.log "Chef libraries not available, disabling Scratchpad", MU::WARN

end #MU.setLogging($opts, $opts) if MU.myCloud == “AWS”

MU::Cloud::AWS.openFirewallForClients # XXX add the other clouds, or abstract

end

Signal.trap(“URG”) do

puts "------------------------------"
puts "Open flock() locks:"
pp MU::MommaCat.trapSafeLocks
puts "------------------------------"

end

begin

MU::Master.syncMonitoringConfig(false)

rescue StandardError => e

MU.log e.inspect, MU::ERR, details: e.backtrace
# ...but don't die!

end

parent_thread_id = Thread.current.object_id Thread.new {

MU.dupGlobals(parent_thread_id)
begin
  MU::MommaCat.cleanTerminatedInstances
  MU::Master.cleanExpiredScratchpads if $ENABLE_SCRATCHPAD
  sleep 60
rescue StandardError => e
  MU.log "Error in cleanTerminatedInstances thread: #{e.inspect}", MU::ERR, details: e.backtrace
  retry
end while true

}

# Use a template to generate a pleasant-looking HTML page for simple messages # and errors. def genHTMLMessage(title: “”, headline: “”, msg: “”, template: $MU_CFG, extra_vars: {})

logo_url = "/cloudamatic.png"
page = "<img src='#{logo_url}'><h1>#{title}</h1>#{msg}"
vars = {
  "title" => title,
  "msg" => msg,
  "headline" => headline,
  "logo" => logo_url
}.merge(extra_vars)
if !template.nil? and File.exist?(template) and File.readable?(template)
  page = Erubis::Eruby.new(File.read(template)).result(vars)
elsif $MU_CFG.has_key?('html_template') and
   File.exist?($MU_CFG['html_template']) and
   File.readable?($MU_CFG['html_template'])
  page = Erubis::Eruby.new(File.read($MU_CFG['html_template'])).result(vars)
elsif File.exist?("#{$MU_CFG['libdir']}/modules/html.erb") and
   File.readable?("#{$MU_CFG['libdir']}/modules/html.erb")
  page = Erubis::Eruby.new(File.read("#{$MU_CFG['libdir']}/modules/html.erb")).result(vars)
end
page

end

# Return an error message to web clients. def throw500(msg = “”, details = nil)

MU.log "Returning 500 to client: #{msg}", MU::ERR, details: details
page = genHTMLMessage(title: "500 Error", headline: msg, msg: details)
[
    500,
    {
        'Content-Type' => 'text/html',
        'Content-Length' => page.length.to_s
    },
    [page]
]

end

def throw404(msg = “”, details = nil)

MU.log "Returning 404 to client: #{msg}", MU::ERR, details: details
page = genHTMLMessage(title: "404 Not Found", headline: msg, msg: details)
[
    404,
    {
        'Content-Type' => 'text/html',
        'Content-Length' => page.length.to_s
    },
    [page]
]

end

def returnRawJSON(data)

MU.log "Returning 200 to client", MU::NOTICE, details: data
[
    200,
    {
        'Content-Type' => 'application/json',
        'Content-Length' => data.length.to_s
    },
    [data]
]

end

@litters = Hash.new @litter_semaphore = Mutex.new

# Load a {MU::MommaCat} instance for the requested deployment. # @param req [Hash]: The web request describing the requested deployment. Must include a mu_id and mu_deploy_secret. # return [MU::MommaCat] def getKittenPile(req)

@litter_semaphore.synchronize {
  mu_id = req["mu_id"]
  if !@litters.has_key?(mu_id)
    begin
      kittenpile = MU::MommaCat.new(
          mu_id,
          deploy_secret: Base64.urlsafe_decode64(req["mu_deploy_secret"]),
          set_context_to_me: true,
          mu_user: req['mu_user']
      )
    rescue MU::MommaCat::DeployInitializeError => e
      MU.log e.inspect, MU::ERR, details: req
      return nil
    end
    @litters[mu_id] = Hash.new
    @litters[mu_id]['kittenpile'] = kittenpile
    @litters[mu_id]['kittencount'] = 1
    @litters[mu_id]['threads'] = [Thread.current.object_id]
  else
    @litters[mu_id]['kittencount'] += 1
    @litters[mu_id]['threads'] << Thread.current.object_id
    MU.dupGlobals(@litters[mu_id]['threads'].first)
  end
  # Make sure enough per-thread global MU class variables are set for us
  # to operate in this thread context

  MU.setVar("mu_id", mu_id)
  MU.setVar("mommacat", @litters[mu_id]['kittenpile'])
  MU.log "Adding kitten in #{mu_id}: #{@litters[mu_id]['kittencount']}", MU::DEBUG, details: @litters
  return @litters[mu_id]['kittenpile']
}

end

# Release a {MU::MommaCat} object. # @param mu_id [String]: The MU identifier of the loaded deploy to replace. def releaseKitten(mu_id)

@litter_semaphore.synchronize {
  if @litters.has_key?(mu_id)
    @litters[mu_id]['kittencount'] -= 1
    @litters[mu_id]['threads'].delete(Thread.current.object_id)
    MU.log "Releasing kitten in #{mu_id}: #{@litters[mu_id]['kittencount']}", MU::DEBUG, details: @litters
    if @litters[mu_id]['kittencount'] < 1
      @litters.delete(mu_id)
    end
    MU.purgeGlobals
  end
}

end

app = proc do |env|

returnval = [
    200,
    {
        'Content-Type' => 'text/html',
        'Content-Length' => '2'
    },
    ['hi']
]
begin
  if !env.nil? and !env['REQUEST_PATH'].nil? and env['REQUEST_PATH'].match(/^\/scratchpad/)
    if !$ENABLE_SCRATCHPAD
      msg = "Scratchpad disabled in non-Chef Mu installations"
      return [
        504,
        {
          'Content-Type' => 'text/html',
          'Content-Length' => msg.length.to_s
        },
        [msg]
      ]
    end
    itemname = env['REQUEST_PATH'].sub(/^\/scratchpad\//, "")
    begin
      if itemname.sub!(/\/secret$/, "")
        secret = MU::Master.fetchScratchPadSecret(itemname)
        MU.log "Retrieved scratchpad secret #{itemname} for #{env['REMOTE_ADDR']}"
        returnval = [
          200,
          {
            'Content-Type' => 'text/plain',
            'Content-Length' => secret.length.to_s
          },
          [secret]
        ]
      else
        secret = "

<script>

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    document.getElementById('scratchpad-button').outerHTML = this.responseText;
  }
};
function showScratchPadSecret(){
  xhttp.open('GET', '#{env['REQUEST_PATH']}/secret', true);
  xhttp.send();
}

</script> <button id='scratchpad-button' onclick='showScratchPadSecret()'>Show My Secret</button> “

      page = nil
      if $MU_CFG.has_key?('scratchpad') and
         $MU_CFG['scratchpad'].has_key?("template_path") and
         File.exist?($MU_CFG['scratchpad']['template_path']) and
         File.readable?($MU_CFG['scratchpad']['template_path'])
        page = genHTMLMessage(
          title: "Your Scratchpad Secret",
          headline:"<strong>YOU MAY ONLY RETRIVE THIS SECRET ONCE!</strong> Be sure to copy it somewhere safe before reloading, browsing away, or closing your browser window.",
          msg: secret,
          template: $MU_CFG['scratchpad']['template_path'],
          extra_vars: { "secret" => secret }
        )
      else
        page = genHTMLMessage(
          title: "Your Scratchpad Secret",
          headline:"<strong>YOU MAY ONLY RETRIVE THIS SECRET ONCE!</strong> Be sure to copy it somewhere safe before reloading, browsing away, or closing your browser window.",
          msg: secret
        )
      end

      returnval = [
        200,
        {
          'Content-Type' => 'text/html',
          'Content-Length' => page.length.to_s
        },
        [page]
      ]
    end
  rescue MU::Groomer::MuNoSuchSecret
    page = nil
    if $MU_CFG.has_key?('scratchpad') and
       $MU_CFG['scratchpad'].has_key?("template_path") and
       File.exist?($MU_CFG['scratchpad']['template_path']) and
       File.readable?($MU_CFG['scratchpad']['template_path'])
      page = genHTMLMessage(
        title: "No such secret",
        headline: "No such secret",
        msg: "The secret '#{itemname}' does not exist or has already been retrieved",
        template: $MU_CFG['scratchpad']['template_path'],
        extra_vars: { "secret" => nil }
      )
    else
      page = genHTMLMessage(
        title: "No such secret",
        headline: "No such secret",
        msg: "The secret '#{itemname}' does not exist or has already been retrieved"
        )
    end
    returnval = [
      200,
      {
        'Content-Type' => 'text/html',
        'Content-Length' => page.length.to_s
      },
      [page]
    ]
  end
elsif !env.nil? and !env['REQUEST_PATH'].nil? and env['REQUEST_PATH'].match(/^\/rest\//)

  action, filter, path = env['REQUEST_PATH'].sub(/^\/rest\/?/, "").split(/\//, 3)
  # Don't give away the store. This can't be public until we can
  # authenticate and access-control properly.
  if env['REMOTE_ADDR'] != "127.0.0.1" and action != "bucketname"
    returnval = throw500 "Service not available"
    next
  end

  if action == "hosts_add"
    if Process.uid != 0
      returnval = throw500 "Service not available"
    elsif !filter or !path
      returnval = throw404 env['REQUEST_PATH']
    else
      MU::Master.addInstanceToEtcHosts(path, filter)
      returnval = [
        200,
        {
          'Content-Type' => 'text/plain',
          'Content-Length' => 2
        },
        ["ok"]
      ]
    end
  elsif action == "deploy"
    returnval = throw404 env['REQUEST_PATH'] if !filter
    MU.log "Loading deploy data for #{filter} #{path}"
    kittenpile = MU::MommaCat.getLitter(filter)
    returnval = returnRawJSON JSON.generate(kittenpile.deployment)
  elsif action == "config"
    returnval = throw404 env['REQUEST_PATH'] if !filter
    MU.log "Loading config #{filter} #{path}"
    kittenpile = MU::MommaCat.getLitter(filter)
    returnval = returnRawJSON JSON.generate(kittenpile.original_config)
  elsif action == "list"
    MU.log "Listing deployments"
    returnval = returnRawJSON JSON.generate(MU::MommaCat.listDeploys)
  elsif action == "bucketname"
    returnval = [
      200,
      {
        'Content-Type' => 'text/plain',
        'Content-Length' => MU.adminBucketName(filter, credentials: path).length.to_s
      },
      [MU.adminBucketName(filter, credentials: path)]
    ]
  else
    returnval = throw404 env['REQUEST_PATH']
  end

elsif !env["rack.input"].nil?
  req = Rack::Utils.parse_nested_query(env["rack.input"].read)

  if req["mu_user"].nil?
    req["mu_user"] = "mu"
  end
  requesttype = nil
  ["mu_ssl_sign", "mu_bootstrap", "mu_windows_admin_creds", "add_volume"].each { |rt|
    if req[rt]
      requesttype = rt
      break
    end
  }

  MU.log "Processing #{requesttype} request from #{env["REMOTE_ADDR"]} (MU-ID #{req["mu_id"]}, #{req["mu_resource_type"]}: #{req["mu_resource_name"]}, instance: #{req["mu_instance_id"]}, mu_user #{req['mu_user']}, path #{env['REQUEST_PATH']})"
  kittenpile = getKittenPile(req)
  if kittenpile.nil? or kittenpile.original_config.nil? or kittenpile.original_config[req["mu_resource_type"]+"s"].nil?
    returnval = throw500 "Couldn't find config data for #{req["mu_resource_type"]} in deploy_id #{req["mu_id"]}"
    next
  end
  server_cfg = nil
  kittenpile.original_config[req["mu_resource_type"]+"s"].each { |svr|
    if svr["name"] == req["mu_resource_name"]
      server_cfg = svr.dup
      break
    end
  }
  if server_cfg.nil?
    returnval = throw500 "Couldn't find config data for #{req["mu_resource_type"]} name: #{req["mu_resource_name"]} deploy_id: #{req["mu_id"]}"
    next
  end

  MU.log "Dug up server config for #{req["mu_resource_type"]} name: #{req["mu_resource_name"]} deploy_id: #{req["mu_id"]}", MU::DEBUG, details: server_cfg

# XXX We can't assume AWS anymore. What does this look like otherwise? # If this is an already-groomed instance, try to get a real object for it

instance = MU::MommaCat.findStray("AWS", "server", cloud_id: req["mu_instance_id"], region: server_cfg["region"], deploy_id: req["mu_id"], name: req["mu_resource_name"], dummy_ok: true, calling_deploy: kittenpile).first
mu_name = nil
if instance.nil?
  # Now we're just checking for existence in the cloud provider, really
  MU.log "No existing groomed server found, verifying that a server with this cloud id exists"
  instance = MU::Cloud::Server.find(cloud_id: req["mu_instance_id"], region: server_cfg["region"])

# instance = MU::MommaCat.findStray(“AWS”, “server”, cloud_id: req, region: server_cfg, deploy_id: req, name: req, dummy_ok: true, calling_deploy: kittenpile).first

  if instance.nil?
    returnval = throw500 "Failed to find an instance with cloud id #{req["mu_instance_id"]}"
  end
else
  mu_name = instance.mu_name
  MU.log "Found an existing node named #{mu_name}"
end

if !req["mu_windows_admin_creds"].nil?
  if !instance.is_a?(MU::Cloud::Server)
    instance = MU::Cloud::Server.new(mommacat: kittenpile, kitten_cfg: server_cfg, cloud_id: req["mu_instance_id"])
  end
  returnval[2] = [kittenpile.retrieveWindowsAdminCreds(instance).join(";")]
  logstr = returnval[2].is_a?(Array) ? returnval[2].first.sub(/;.*/, ";*********") : returnval[2].sub(/;.*/, ";*********")
  MU.log logstr, MU::NOTICE
elsif !req["mu_ssl_sign"].nil?
  kittenpile.signSSLCert(req["mu_ssl_sign"], req["mu_ssl_sans"].split(/,/))
  kittenpile.signSSLCert(req["mu_ssl_sign"], req["mu_ssl_sans"].split(/,/))
elsif !req["add_volume"].nil?
  if instance.respond_to?(:addVolume)

# XXX make sure we handle mangled input safely

        params = JSON.parse(Base64.decode64(req["add_volume"]))
        MU.log "add_volume request", MU::NOTICE, details: params
        instance.addVolume(params["dev"], params["size"], delete_on_termination: params["delete_on_termination"])
      else
        returnval = throw500 "I don't know how to add a volume for #{instance}"
      end
    elsif !instance.nil?
      if !req["mu_bootstrap"].nil?
        kittenpile.groomNode(req["mu_instance_id"], req["mu_resource_name"], req["mu_resource_type"], mu_name: mu_name, sync_wait: true)
        returnval[2] = ["Grooming asynchronously, check Momma Cat logs on the master for details."]
      else
        returnval = throw500 "Didn't get 'mu_bootstrap' parameter from instance id '#{req["mu_instance_id"]}'"
      end
    else
      returnval = throw500 "No such instance id '#{req["mu_instance_id"]}' nor was this an SSL signing request"
    end
  end
rescue StandardError => e
  returnval = throw500 "Invalid request: #{e.inspect} (#{req})", e.backtrace
ensure
  if !req.nil?
    releaseKitten(req['mu_id'])
    MU.purgeGlobals
  end
end
if returnval[1] and returnval[1].has_key?("Content-Length") and
   returnval[2] and returnval[2].is_a?(Array)
  returnval[1]["Content-Length"] = returnval[2][0].size.to_s
end
returnval

end

run app