573 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			573 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| require "json"
 | |
| require "db/mongo/models/project"
 | |
| require "db/mongo/models/deploy_env"
 | |
| require "db/exceptions/invalid_record"
 | |
| require "commands/deploy"
 | |
| require "commands/status"
 | |
| require "commands/server"
 | |
| 
 | |
| module Version2_0
 | |
|   class ProjectRoutes < BaseRoutes
 | |
| 
 | |
|     include DeployCommands
 | |
|     include StatusCommands
 | |
|     include ServerCommands
 | |
| 
 | |
|     def initialize wrapper
 | |
|       super wrapper
 | |
|       puts "Project routes initialized"
 | |
|     end
 | |
| 
 | |
|     before "/project/:id/user" do
 | |
|       check_headers :accept, :content_type
 | |
|       check_privileges("project", "w")
 | |
|       body = create_object_from_json_body
 | |
|       @users = check_array(body["users"], "Parameter 'users' must be a not empty array of strings")
 | |
|       @deploy_env = check_string(body["deploy_env"], "Parameter 'deploy_env' must be a not empty string", true)
 | |
|       @project = BaseRoutes.mongo.project(params[:id])
 | |
|     end
 | |
| 
 | |
|     after %r{\A/project(/[\w]+(/(user|deploy))?)?\z} do
 | |
|       statistic
 | |
|     end
 | |
| 
 | |
|     after "/project/:id/:env/run_list" do
 | |
|       statistic
 | |
|     end
 | |
| 
 | |
|     # Get projects list
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : GET
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   [
 | |
|     #     "project_1"
 | |
|     #   ]
 | |
|     # TODO: list with environments
 | |
|     get "/projects" do
 | |
|       check_headers :accept
 | |
|       check_privileges("project", "r")
 | |
|       json BaseRoutes.mongo.projects.map {|p| p.id}
 | |
|     end
 | |
| 
 | |
|     # Get project by id
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : GET
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   {
 | |
|     #     "deploy_envs": [
 | |
|     #       {
 | |
|     #         "flavor": "flavor",
 | |
|     #         "identifier": "prod",
 | |
|     #         "image": "image id",
 | |
|     #         "run_list": [
 | |
|     #           "role[project_1-prod]"
 | |
|     #         ],
 | |
|     #         "subnets": [
 | |
|     #           "private"
 | |
|     #         ],
 | |
|     #         "expires": null,
 | |
|     #         "provider": "openstack",
 | |
|     #         "groups": [
 | |
|     #           "default"
 | |
|     #         ],
 | |
|     #         "users": [
 | |
|     #           "user"
 | |
|     #         ]
 | |
|     #       }
 | |
|     #     ],
 | |
|     #     "name": "project_1"
 | |
|     #   }
 | |
|     get "/project/:project" do
 | |
|       check_headers :accept
 | |
|       check_privileges("project", "r")
 | |
|       json BaseRoutes.mongo.project(params[:project])
 | |
|     end
 | |
| 
 | |
|     # Get project servers
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : GET
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #   - parameters :
 | |
|     #     - deploy_env=:env -> show servers with environment :env
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   [
 | |
|     #     {
 | |
|     #       "provider": "openstack",
 | |
|     #       "chef_node_name": "project_1_server",
 | |
|     #       "remote_user": "root",
 | |
|     #       "project": "project_1",
 | |
|     #       "deploy_env": "prod",
 | |
|     #       "private_ip": "10.8.8.8",
 | |
|     #       "public_ip": null,
 | |
|     #       "created_at": "2014-04-23 13:35:18 UTC",
 | |
|     #       "created_by": "user",
 | |
|     #       "static": false,
 | |
|     #       "key": "ssh key",
 | |
|     #       "id": "nstance id"
 | |
|     #     }
 | |
|     #   ]
 | |
|     get "/project/:project/servers" do
 | |
|       check_headers :accept
 | |
|       check_privileges("project", "r")
 | |
|       BaseRoutes.mongo.project(params[:project])
 | |
|       json BaseRoutes.mongo.servers(params[:project], params[:deploy_env]).map{|s| s.to_hash}
 | |
|     end
 | |
| 
 | |
|     # Create project and chef roles
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : POST
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #     - Content-Type: application/json
 | |
|     #   - body :
 | |
|     #     {
 | |
|     #       "deploy_envs": [
 | |
|     #         {
 | |
|     #           "identifier": "prod",
 | |
|     #           "provider": "openstack",
 | |
|     #           "flavor": "m1.small",
 | |
|     #           "image": "image id",
 | |
|     #           "subnets": [
 | |
|     #             "private"
 | |
|     #           ],
 | |
|     #           "groups": [
 | |
|     #             "default"
 | |
|     #           ],
 | |
|     #           "users": [
 | |
|     #             "user"
 | |
|     #           ],
 | |
|     #           "run_list": [
 | |
|     #
 | |
|     #           ],
 | |
|     #           "expires": null
 | |
|     #         }
 | |
|     #       ],
 | |
|     #       "name": "project_1"
 | |
|     #     }
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   201 - Created
 | |
|     # TODO: multi project
 | |
|     post "/project" do
 | |
|       check_headers :accept, :content_type
 | |
|       check_privileges("project", "w")
 | |
|       body = create_object_from_json_body
 | |
|       check_string(body["name"], "Parameter 'name' must be a not empty string")
 | |
|       check_array(body["deploy_envs"], "Parameter 'deploy_envs' must be a not empty array of objects", Hash)
 | |
|       p = Project.new(body)
 | |
|       halt_response("Project '#{p.id}' already exist") if BaseRoutes.mongo.is_project_exists?(p)
 | |
|       p.add_authorized_user [request.env['REMOTE_USER']]
 | |
|       BaseRoutes.mongo.project_insert p
 | |
|       roles_res = ""
 | |
|       if p.multi?
 | |
|         logger.info "Project '#{p.id}' with type 'multi' created"
 | |
|       else
 | |
|         logger.info "Project '#{p.id}' created"
 | |
|         roles = create_roles p.id, p.deploy_envs, logger
 | |
|         roles_res = ". " + create_roles_response(roles)
 | |
|       end
 | |
|       res = "Created" + roles_res
 | |
|       create_response(res, nil, 201)
 | |
|     end
 | |
| 
 | |
|     # Update project and create chef roles
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : PUT
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #     - Content-Type: application/json
 | |
|     #   - body :
 | |
|     #     {
 | |
|     #       "deploy_envs": [
 | |
|     #         {
 | |
|     #           "identifier": "dev",
 | |
|     #           "provider": "openstack",
 | |
|     #           "flavor": "m1.small",
 | |
|     #           "image": "image id",
 | |
|     #           "subnets": [
 | |
|     #             "private"
 | |
|     #           ],
 | |
|     #           "groups": [
 | |
|     #             "default"
 | |
|     #           ],
 | |
|     #           "users": [
 | |
|     #             "user"
 | |
|     #           ],
 | |
|     #           "run_list": [
 | |
|     #
 | |
|     #           ],
 | |
|     #           "expires": null
 | |
|     #         }
 | |
|     #       ],
 | |
|     #       "name": "project_1"
 | |
|     #     }
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   200 - Updated
 | |
|     # TODO: multi project
 | |
|     put "/project/:id" do
 | |
|       check_headers
 | |
|       check_privileges("project", "w")
 | |
|       project = Project.new(create_object_from_json_body)
 | |
|       project.id = params[:id]
 | |
|       old_project = BaseRoutes.mongo.project params[:id]
 | |
|       BaseRoutes.mongo.project_update project
 | |
|       roles = create_new_roles(old_project, project, logger)
 | |
|       info = "Project '#{project.id}' has been updated." + create_roles_response(roles)
 | |
|       create_response(info)
 | |
|     end
 | |
| 
 | |
|     # Add users to project environment
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : PUT
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #     - Content-Type: application/json
 | |
|     #   - body :
 | |
|     #     {
 | |
|     #       "users": [
 | |
|     #         "user1"
 | |
|     #       ],
 | |
|     #       "deploy_env": "env" -> if null, users will be added to all environments
 | |
|     #     }
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   200 - Updated
 | |
|     # TODO: multi project
 | |
|     put "/project/:id/user" do
 | |
|       users = BaseRoutes.mongo.users(@users).map{|u| u.id}
 | |
|       buf = @users - users
 | |
|       @project.add_authorized_user users, @deploy_env
 | |
|       BaseRoutes.mongo.project_update(@project)
 | |
|       info = "Users '#{users.join("', '")}' have been added to '#{params[:id]}' project's authorized users"
 | |
|       info << ", invalid users: '#{buf.join("', '")}'" unless buf.empty?
 | |
|       create_response(info)
 | |
|     end
 | |
| 
 | |
|     # Delete users from project environment
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : DELETE
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #     - Content-Type: application/json
 | |
|     #   - body :
 | |
|     #     {
 | |
|     #       "users": [
 | |
|     #         "user1"
 | |
|     #       ],
 | |
|     #       "deploy_env": "env" -> if null, users will be deleted from all environments
 | |
|     #     }
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   200 - Updated
 | |
|     # TODO: multi project
 | |
|     delete "/project/:id/user" do
 | |
|       @project.remove_authorized_user @users, @deploy_env
 | |
|       BaseRoutes.mongo.project_update @project
 | |
|       info = "Users '#{@users.join("', '")}' have been removed from '#{params[:id]}' project's authorized users"
 | |
|       create_response(info)
 | |
|     end
 | |
| 
 | |
|     # Set run_list to project environment
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : PUT
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #     - Content-Type: application/json
 | |
|     #   - body :
 | |
|     #     [
 | |
|     #       "role[role_1]",
 | |
|     #       "recipe[recipe_1]"
 | |
|     #     ]
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   200 - Updated
 | |
|     # TODO: multi project
 | |
|     put "/project/:id/:env/run_list" do
 | |
|       check_headers :accept, :content_type
 | |
|       check_privileges("project", "w")
 | |
|       list = create_object_from_json_body(Array)
 | |
|       check_array(list, "Body must contains not empty array of strings")
 | |
|       project = BaseRoutes.mongo.project(params[:id])
 | |
|       env = project.deploy_env params[:env]
 | |
|       env.run_list = list
 | |
|       BaseRoutes.mongo.project_update project
 | |
|       create_response("Updated environment '#{env.identifier}' with run_list '#{env.run_list.inspect}' in project '#{project.id}'")
 | |
|     end
 | |
| 
 | |
|     # Delete project
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : DELETE
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #     - Content-Type: application/json
 | |
|     #   - body :
 | |
|     #     {
 | |
|     #       "deploy_env": "env" -> if not null, will be deleted environment only
 | |
|     #     }
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   200 - Deleted
 | |
|     delete "/project/:id" do
 | |
|       check_headers :accept, :content_type
 | |
|       check_privileges("project", "w")
 | |
|       servers = BaseRoutes.mongo.servers params[:id]
 | |
|       raise DependencyError.new "Deleting #{params[:id]} is forbidden: Project has servers" if !servers.empty?
 | |
|       body = create_object_from_json_body(Hash, true)
 | |
|       deploy_env = unless body.nil?
 | |
|         check_string(body["deploy_env"], "Parameter 'deploy_env' should be a not empty string", true)
 | |
|       end
 | |
|       info = if deploy_env.nil?
 | |
|         BaseRoutes.mongo.project_delete(params[:id])
 | |
|         "Project '#{params[:id]}' is deleted"
 | |
|       else
 | |
|         project = BaseRoutes.mongo.project(params[:id])
 | |
|         project.remove_env params[:deploy_env]
 | |
|         BaseRoutes.mongo.project_update project
 | |
|         "Project '#{params[:id]}'. Deploy environment '#{params[:deploy_env]}' has been deleted"
 | |
|       end
 | |
|       create_response(info)
 | |
|     end
 | |
| 
 | |
|     # Run chef-client on reserved project servers
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : POST
 | |
|     #   - headers :
 | |
|     #     - Content-Type: application/json
 | |
|     #   - body :
 | |
|     #     {
 | |
|     #       "servers": [
 | |
|     #         "server_1"
 | |
|     #       ], -> deploy servers from list, all servers if null
 | |
|     #       "deploy_env": "env" -> deploy servers with environment 'env' or all project servers if null
 | |
|     #     }
 | |
|     #
 | |
|     # * *Returns* : text stream
 | |
|     post "/project/:id/deploy" do
 | |
|       check_headers :content_type
 | |
|       check_privileges("project", "x")
 | |
|       obj = create_object_from_json_body
 | |
|       check_string(obj["deploy_env"], "Parameter 'deploy_env' should be a not empty string", true)
 | |
|       check_array(obj["servers"], "Parameter 'servers' should be a not empty array of strings", String, true)
 | |
|       project = BaseRoutes.mongo.project(params[:id])
 | |
|       servers = BaseRoutes.mongo.servers(params[:id], obj["deploy_env"])
 | |
|       servers.delete_if{|s| s.reserved_by.nil?}
 | |
|       unless obj["servers"].nil?
 | |
|         logger.debug "Servers in params: #{obj["servers"].inspect}\nServers: #{servers.map{|s| s.chef_node_name}.inspect}"
 | |
|         servers.select!{|ps| obj["servers"].include?(ps.chef_node_name)}
 | |
|       end
 | |
|       keys = {}
 | |
|       stream() do |out|
 | |
|         begin
 | |
|           out << (servers.empty? ? "No reserved servers to deploy\n" : "Deploy servers: '#{servers.map{|s| s.chef_node_name}.join("', '")}'\n")
 | |
|           status = []
 | |
|           servers.each do |s|
 | |
| 
 | |
|             begin
 | |
|               BaseRoutes.mongo.check_project_auth s.project, s.deploy_env, request.env['REMOTE_USER']
 | |
|             rescue InvalidPrivileges, RecordNotFound  => e
 | |
|               out << e.message + "\n"
 | |
|               status.push 2
 | |
|               next
 | |
|             end
 | |
|             unless keys.key? s.key
 | |
|               k = BaseRoutes.mongo.key s.key
 | |
|               keys[s.key] = k.path
 | |
|             end
 | |
|             status.push(deploy_server out, s, keys[s.key])
 | |
|           end
 | |
|           out << create_status(status)
 | |
|         rescue IOError => e
 | |
|           logger.error e.message
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # Test project environment
 | |
|     #
 | |
|     # Run tests:
 | |
|     #   - run server
 | |
|     #   - bootstrap server
 | |
|     #   - delete server
 | |
|     #
 | |
|     # * *Request*
 | |
|     #   - method : DELETE
 | |
|     #   - headers :
 | |
|     #     - Accept: application/json
 | |
|     #     - Content-Type: application/json
 | |
|     #
 | |
|     # * *Returns* :
 | |
|     #   200 -
 | |
|     #   {
 | |
|     #     "servers": [
 | |
|     #       {
 | |
|     #         "id": "132958f0-61c5-4665-8cc3-66e1bacd285b",
 | |
|     #         "create": {
 | |
|     #           "status": true,
 | |
|     #           "time": "155s"
 | |
|     #         },
 | |
|     #         "chef_node_name": "chef name",
 | |
|     #         "bootstrap": {
 | |
|     #           "status": true,
 | |
|     #           "log": "\nWaiting for SSH...\n"
 | |
|     #           "return_code": 0
 | |
|     #         },
 | |
|     #         "delete": {
 | |
|     #           "status": true,
 | |
|     #           "time": "2s"
 | |
|     #           "log": {
 | |
|     #             "chef_node": "Deleted node[chef name]",
 | |
|     #             "chef_client": "Deleted client[chef name]",
 | |
|     #             "server": "Server with id '132958f0-61c5-4665-8cc3-66e1bacd285b' terminated"
 | |
|     #           }
 | |
|     #         },
 | |
|     #       }
 | |
|     #     ],
 | |
|     #     "project": {
 | |
|     #       "deploy_envs": [
 | |
|     #         {
 | |
|     #           "flavor": "flavor",
 | |
|     #           "identifier": "prod",
 | |
|     #           "image": "image id",
 | |
|     #           "run_list": [
 | |
|     #             "role[prod]"
 | |
|     #           ],
 | |
|     #           "subnets": [
 | |
|     #             "private"
 | |
|     #           ],
 | |
|     #           "expires": null,
 | |
|     #           "provider": "openstack",
 | |
|     #           "groups": [
 | |
|     #             "default"
 | |
|     #           ],
 | |
|     #           "users": [
 | |
|     #             "root"
 | |
|     #           ]
 | |
|     #         }
 | |
|     #       ],
 | |
|     #       "name": "prject_1"
 | |
|     #     },
 | |
|     #     "message": "Test project 'project_1' and environment 'prod'"
 | |
|     #   }
 | |
|     post "/project/test/:id/:env" do
 | |
|       check_headers :accept, :content_type
 | |
|       check_privileges("project", "r")
 | |
|       project = BaseRoutes.mongo.project(params[:id])
 | |
|       env = project.deploy_env params[:env]
 | |
|       user = request.env['REMOTE_USER']
 | |
|       provider = ::Provider::ProviderFactory.get(env.provider)
 | |
|       header = "Test project '#{project.id}' and environment '#{env.identifier}'"
 | |
|       logger.info header
 | |
|       servers = extract_servers(provider, project, env, {}, user, BaseRoutes.mongo)
 | |
|       result = {:servers => []}
 | |
|       project.deploy_envs = [ env ]
 | |
|       result[:project] = project.to_hash
 | |
|       servers.each do |s|
 | |
|         sr = {}
 | |
|         t1 = Time.now
 | |
|         out = ""
 | |
|         if provider.create_server(s, out)
 | |
|           t2 = Time.now
 | |
|           sr[:id] = s.id
 | |
|           sr[:create] = {:status => true}
 | |
|           sr[:create][:time] = time_diff_s(t1, t2)
 | |
|           logger.info "Server with parameters: #{s.to_hash.inspect} is running"
 | |
|           key = BaseRoutes.mongo.key(s.key)
 | |
|           b_out = ""
 | |
|           r = bootstrap(s, b_out, key.path, logger)
 | |
|           t1 = Time.now
 | |
|           sr[:chef_node_name] = s.chef_node_name
 | |
|           if r == 0
 | |
|             sr[:bootstrap] = {:status => true}
 | |
|             sr[:bootstrap][:time] = time_diff_s(t2, t1)
 | |
|             logger.info "Server with id '#{s.id}' is bootstraped"
 | |
|             if check_server(s)
 | |
|               BaseRoutes.mongo.server_insert s
 | |
|             end
 | |
|           else
 | |
|             sr[:bootstrap] = {:status => false}
 | |
|             sr[:bootstrap][:log] = b_out
 | |
|             sr[:bootstrap][:return_code] = r
 | |
|           end
 | |
| 
 | |
|           t1 = Time.now
 | |
|           r = delete_from_chef_server(s.chef_node_name)
 | |
|           begin
 | |
|             r[:server] = provider.delete_server s.id
 | |
|           rescue Fog::Compute::OpenStack::NotFound, Fog::Compute::AWS::Error
 | |
|             r[:server] = "Server with id '#{s.id}' not found in '#{provider.name}' servers"
 | |
|             logger.warn r[:server]
 | |
|           end
 | |
|           BaseRoutes.mongo.server_delete s.id
 | |
|           t2 = Time.now
 | |
|           sr[:delete] = {:status => true}
 | |
|           sr[:delete][:time] = time_diff_s(t1, t2)
 | |
|           sr[:delete][:log] = r
 | |
|         else
 | |
|           sr[:create] = {:status => false}
 | |
|           sr[:create][:log] = out
 | |
|         end
 | |
|         result[:servers].push sr
 | |
|       end
 | |
|       create_response(header, result)
 | |
|     end
 | |
| 
 | |
|   private
 | |
|     def create_roles project_id, envs, logger
 | |
|       all_roles = KnifeCommands.roles
 | |
|       return "Can't get roles list" if all_roles.nil?
 | |
|       roles = {:new => [], :error => [], :exist => []}
 | |
|       envs.each do |e|
 | |
|         role_name = KnifeCommands.role_name(project_id, e.identifier)
 | |
|         begin
 | |
|           if all_roles.include? role_name
 | |
|             roles[:exist].push role_name
 | |
|           else
 | |
|             KnifeCommands.create_role role_name, project_id, e.identifier
 | |
|             roles[:new].push role_name
 | |
|             logger.info "Role '#{role_name}' created"
 | |
|           end
 | |
|         rescue => er
 | |
|           roles[:error].push role_name
 | |
|           logger.error "Role '#{role_name}' can not be created: #{er.message}"
 | |
|         end
 | |
|       end
 | |
|       roles
 | |
|     end
 | |
| 
 | |
|     def create_new_roles old_project, new_project, logger
 | |
|       old_project.deploy_envs.each do |e|
 | |
|         new_project.remove_env(e.identifier)
 | |
|       end
 | |
|       create_roles new_project.id, new_project.deploy_envs, logger
 | |
|     end
 | |
| 
 | |
|     def create_roles_response roles
 | |
|       info = ""
 | |
|       info += " Project roles '#{roles[:new].join("', '")}' have been automaticaly created" unless roles[:new].empty?
 | |
|       info += " Project roles '#{roles[:exist].join("', '")}' weren't created because they exist" unless roles[:exist].empty?
 | |
|       info += " Project roles '#{roles[:error].join("', '")}' weren't created because of internal error" unless roles[:error].empty?
 | |
|       info
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | 
