require "json" require "chef" require "commands/knife_commands" require 'rufus-scheduler' require "routes/v2.0/base_routes" require "providers/provider_factory" require "db/mongo/models/deploy_env" require "commands/status" require "commands/server" module Version2_0 class ExpireHandler include ServerCommands def initialize server, logger @server = server @logger = logger end def call(job) @logger.info("Removing node '#{@server.chef_node_name}' form project '#{@server.project}' and env '#{@server.deploy_env}'") begin delete_server(@server, BaseRoutes.mongo, @logger) rescue => e logger.error "ExpiredHandler error: " + e.message end end end class ServerRoutes < BaseRoutes include StatusCommands include ServerCommands def initialize wrapper super wrapper puts "Server routes initialized" end before "/server/:name_or_cmd" do if request.get? check_headers :accept else check_headers :accept, :content_type end check_privileges("server") end before %r{\A/server/[\w]+/(pause|unpouse|reserve|unreserve)\z} do check_headers :accept, :content_type check_privileges("server", "w") body = create_object_from_json_body(Hash, true) @key = (body.nil? ? nil : body["key"]) end before "/servers/:provider" do check_headers :accept check_privileges("server", "r") end after %r{\A/server(/[\w]+)?\z | \A/server/(add|bootstrap)\z | \A/server/[\w]+/(un)?pause\z} do statistic end scheduler = Rufus::Scheduler.new # Get devops servers list # # * *Request* # - method : GET # - headers : # - Accept: application/json # # * *Returns* : # [ # { # "id": "instance id", # "chef_node_name": "chef name" # } # ] get "/servers" do check_headers :accept check_privileges("server", "r") json BaseRoutes.mongo.servers.map {|s| s.to_list_hash} end # Get chef nodes list # # * *Request* # - method : GET # - headers : # - Accept: application/json # # * *Returns* : # [ # { # "chef_node_name": "chef name" # } # ] get "/servers/chef" do json KnifeCommands.chef_node_list end # Get provider servers list # # * *Request* # - method : GET # - headers : # - Accept: application/json # # * *Returns* : # -ec2 # [ # { # "state": "running", # "name": "name", # "image": "ami-83e4bcea", # "flavor": "m1.small", # "keypair": "ssh key", # "instance_id": "i-8441bfd4", # "dns_name": "ec2-204-236-199-49.compute-1.amazonaws.com", # "zone": "us-east-1d", # "private_ip": "10.215.217.210", # "public_ip": "204.236.199.49", # "launched_at": "2014-04-25 07:56:33 UTC" # } # ] # -openstack # [ # { # "state": "ACTIVE", # "name": "name", # "image": "image id", # "flavor": null, # "keypair": "ssh key", # "instance_id": "instance id", # "private_ip": "172.17.0.1" # } # ] get "/servers/:provider" do json ::Version2_0::Provider::ProviderFactory.get(params[:provider]).servers end # Get server info by :name # # * *Request* # - method : GET # - headers : # - Accept: application/json # - parameters: # key=instance -> search server by instance_id rather then chef_node_name # # * *Returns* : # [ # { # "chef_node_name": "chef name" # } # ] get "/server/:name" do json get_server(params[:name], params[:key]).to_hash end # Delete devops server # # * *Request* # - method : DELETE # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "key": "instance", -> search server by instance_id rather then chef_node_name # } # # * *Returns* : # 200 - Deleted delete "/server/:id" do body = create_object_from_json_body(Hash, true) key = (body.nil? ? nil : body["key"]) s = get_server(params[:id], key) ### Authorization BaseRoutes.mongo.check_project_auth s.project, s.deploy_env, request.env['REMOTE_USER'] info, r = delete_server(s, BaseRoutes.mongo, logger) create_response(info, r) end # Create devops server # # * *Request* # - method : POST # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "project": "project name", -> mandatory parameter # "deploy_env": "env", -> mandatory parameter # "name": "server_name", -> if null, name will be generated # "without_bootstrap": null, -> do not install chef on instance if true # "force": null, -> do not delete server on error # "groups": [], -> specify special security groups, overrides value from project env # "key": "ssh key" -> specify ssh key for server, overrides value from project env # } # # * *Returns* : text stream post "/server" do check_headers :content_type check_privileges("server", "w") body = create_object_from_json_body user = request.env['REMOTE_USER'] project_name = check_string(body["project"], "Parameter 'project' must be a not empty string") env_name = check_string(body["deploy_env"], "Parameter 'deploy_env' must be a not empty string") server_name = check_string(body["name"], "Parameter 'name' should be null or not empty string", true) without_bootstrap = body["without_bootstrap"] halt_response("Parameter 'without_bootstrap' should be a null or true") unless without_bootstrap.nil? or without_bootstrap == true force = body["force"] halt_response("Parameter 'force' should be a null or true") unless force.nil? or force == true groups = check_array(body["groups"], "Parameter 'groups' should be null or not empty array of string", String, true) key_name = check_string(body["key"], "Parameter 'key' should be null or not empty string", true) new_key = BaseRoutes.mongo.key(key_name) unless key_name.nil? p = BaseRoutes.mongo.check_project_auth(project_name, env_name, user) env = p.deploy_env(env_name) provider = ::Version2_0::Provider::ProviderFactory.get(env.provider) check_chef_node_name(server_name, provider) unless server_name.nil? unless groups.nil? buf = groups - provider.groups.keys halt_response("Invalid security groups '#{buf.join("', '")}' for provider '#{provider.name}'") if buf.empty? end servers = extract_servers(provider, p, env, body, user, BaseRoutes.mongo) stream() do |out| status = [] servers.each do |s| begin unless provider.create_server(s, out) status.push 3 next end logger.info "Server with parameters: #{s.to_hash.inspect} is running" unless without_bootstrap key = new_key || BaseRoutes.mongo.key(s.key) bootstrap(s, out, key.path, logger) logger.info "Server with id '#{s.id}' is bootstraped" if force or check_server(s) BaseRoutes.mongo.server_insert s scheduler.in(env.expires, ExpireHandler.new(s, logger)) unless env.expires.nil? out << "Server #{s.chef_node_name} is created" status.push 0 else out << roll_back(s, provider) status.push 5 end out << "\n" else BaseRoutes.mongo.server_insert s status.push 0 end out << create_status(status) rescue IOError => e logger.error e.message logger.warn roll_back(s, provider) break end end end end # Pause devops server by name # # * *Request* # - method : POST # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "key": "instance", -> search server by instance_id rather then chef_node_name # } # # * *Returns* : # 200 - Paused post "/server/:node_name/pause" do s = get_server(params[:node_name], @key) ## Authorization BaseRoutes.mongo.check_project_auth s.project, s.deploy_env, request.env['REMOTE_USER'] provider = ::Version2_0::Provider::ProviderFactory.get(s.provider) r = provider.pause_server s.id if r.nil? create_response("Server with instance ID '#{s.id}' and node name '#{params[:node_name]}' is paused") else halt_response("Server with instance ID '#{s.id}' and node name '#{params[:node_name]}' can not be paused, It in state '#{r}'", 409) end end # Unpause devops server by name # # * *Request* # - method : POST # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "key": "instance", -> search server by instance_id rather then chef_node_name # } # # * *Returns* : # 200 - Unpaused post "/server/:node_name/unpause" do s = get_server(params[:node_name], @key) ## Authorization BaseRoutes.mongo.check_project_auth s.project, s.deploy_env, request.env['REMOTE_USER'] provider = ::Version2_0::Provider::ProviderFactory.get(s.provider) r = provider.unpause_server s.id if r.nil? create_response("Server with instance ID '#{s.id}' and node name '#{params[:node_name]}' is unpaused") else halt_response("Server with instance ID '#{s.id}' and node name '#{params[:node_name]}' can not be unpaused, It in state '#{r}'", 409) end end # Reserve devops server # # * *Request* # - method : POST # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "key": "instance", -> search server by instance_id rather then chef_node_name # } # # * *Returns* : # 200 - Reserved post "/server/:node_name/reserve" do s = get_server(params[:node_name], params[:key]) user = request.env['REMOTE_USER'] BaseRoutes.mongo.check_project_auth s.project, s.deploy_env, user halt_response(400, "Server '#{params[:node_name]}' already reserved") unless s.reserved_by.nil? s.reserved_by = user BaseRoutes.mongo.server_update(s) create_response("Server '#{params[:node_name]}' has been reserved") end # Unreserve devops server # # * *Request* # - method : POST # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "key": "instance", -> search server by instance_id rather then chef_node_name # } # # * *Returns* : # 200 - Unreserved post "/server/:node_name/unreserve" do s = get_server(params[:node_name], params[:key]) BaseRoutes.mongo.check_project_auth s.project, s.deploy_env, request.env['REMOTE_USER'] halt_response(400, "Server '#{params[:node_name]}' is not reserved") if s.reserved_by.nil? s.reserved_by = nil BaseRoutes.mongo.server_update(s) create_response("Server '#{params[:node_name]}' has been unreserved") end # Bootstrap devops server # # * *Request* # - method : POST # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "instance_id": "instance id", -> mandatory parameter # "name": "server_name", -> if null, name will be generated # "run_list": [], -> specify list of roles and recipes # "bootstrap_template": "template" -> specify ssh key for server, overrides value from project env # } # # * *Returns* : text stream # TODO: check bootstrap template name post "/server/bootstrap" do body = create_object_from_json_body(Hash, true) id = check_string(body["instance_id"], "Parameter 'instance_id' must be a not empty string") name = check_string(body["name"], "Parameter 'name' should be a not empty string", true) rl = check_array(body["run_list"], "Parameter 'run_list' should be a not empty array of string", String, true) unless rl.nil? e = DeployEnv.validate_run_list(rl) halt_response("Invalid run list elements: '#{e.join("', '")}'") unless e.empty? end t = check_string(body["bootstrap_template"], "Parameter 'bootstrap_template' should be a not empty string", true) s = BaseRoutes.mongo.server_by_instance_id(id) p = BaseRoutes.mongo.check_project_auth s.project, s.deploy_env, request.env['REMOTE_USER'] d = p.deploy_env s.deploy_env s.options = { :run_list => rl || d.run_list, } s.options[:bootstrap_template] = t unless t.nil? stream() do |out| begin s.chef_node_name = name || "static_#{s.key}-#{Time.now.to_i}" cert = BaseRoutes.mongo.key s.key logger.debug "Bootstrap certificate path: #{cert.path}" bootstrap s, out, cert.path, logger str = nil if check_server(s) BaseRoutes.mongo.server_update s str = "Server with id '#{s.id}' is bootstraped" logger.info str else str = "Server with id '#{s.id}' is not bootstraped" logger.warn str end out << str out << "\n" rescue IOError => e logger.error e.message end end end # Add external server to devops # # * *Request* # - method : POST # - headers : # - Accept: application/json # - Content-Type: application/json # - body : # { # "project": "project name", -> mandatory parameter # "deploy_env": "env", -> mandatory parameter # "key": "ssh key", -> mandatory parameter # "remote_user": "ssh user", -> mandatory parameter # "private_ip": "ip", -> mandatory parameter # "public_ip": "ip" # } # # * *Returns* : # 200 - Added # TODO: should be refactored post "/server/add" do body = create_object_from_json_body project = check_string(body["project"], "Parameter 'project' must be a not empty string") deploy_env = check_string(body["deploy_env"], "Parameter 'deploy_env' must be a not empty string") key = check_string(body["key"], "Parameter 'key' must be a not empty string") remote_user = check_string(body["remote_user"], "Parameter 'remote_user' must be a not empty string") private_ip = check_string(body["private_ip"], "Parameter 'private_ip' must be a not empty string") public_ip = check_string(body["public_ip"], "Parameter 'public_ip' should be a not empty string", true) p = BaseRoutes.mongo.check_project_auth project, deploy_env, request.env['REMOTE_USER'] d = p.deploy_env(deploy_env) cert = BaseRoutes.mongo.key(key) s = Server.new s.provider = "static" s.project = project s.deploy_env = deploy_env s.remote_user = remote_user s.private_ip = private_ip s.public_ip = public_ip s.static = true s.id = "static_#{cert.id}-#{Time.now.to_i}" s.key = cert.id BaseRoutes.mongo.server_insert s create_response("Server '#{s.id}' has been added") end private def get_server id, key key == "instance" ? BaseRoutes.mongo.server_by_instance_id(id) : BaseRoutes.mongo.server_by_chef_node_name(id) end def roll_back s, provider str = "" unless s.id.nil? str << "Server '#{s.chef_node_name}' with id '#{s.id}' is not created\n" str << delete_from_chef_server(s.chef_node_name).values.join("\n") begin str << provider.delete_server(s.id) unless s.static rescue => e str << e.message end str << "\nRolled back\n" end return str end def check_chef_node_name name, provider BaseRoutes.mongo.server_by_chef_node_name name halt(400, "Server with name '#{name}' already exist") rescue RecordNotFound => e # server not found - OK s = provider.servers.detect {|s| s["name"] == name} halt(400, "#{provider.name} node with name '#{name}' already exist") unless s.nil? s = KnifeCommands.chef_node_list.detect {|n| n == name} halt(400, "Chef node with name '#{name}' already exist") unless s.nil? s = KnifeCommands.chef_client_list.detect {|c| c == name} halt(400, "Chef client with name '#{name}' already exist") unless s.nil? end end end