Merge pull request #73 from ClusterCockpit/feature-62-rest-endpoint-delete-jobs

Feature 62 rest endpoint delete jobs
Fix #62
This commit is contained in:
Jan Eitzinger 2022-11-30 14:48:44 +01:00 committed by GitHub
commit c7eaccd3b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1102 additions and 219 deletions

View File

@ -1,9 +1,8 @@
{ {
"swagger": "2.0", "swagger": "2.0",
"info": { "info": {
"description": "Defines a tag using name and type.", "description": "API for batch job control.",
"title": "ClusterCockpit REST API", "title": "ClusterCockpit REST API",
"termsOfService": "https://monitoring.nhr.fau.de/imprint",
"contact": { "contact": {
"name": "ClusterCockpit Project", "name": "ClusterCockpit Project",
"url": "https://github.com/ClusterCockpit", "url": "https://github.com/ClusterCockpit",
@ -13,9 +12,9 @@
"name": "MIT License", "name": "MIT License",
"url": "https://opensource.org/licenses/MIT" "url": "https://opensource.org/licenses/MIT"
}, },
"version": "0.1.0" "version": "0.2.0"
}, },
"host": "clustercockpit.localhost:8082", "host": "localhost:8080",
"basePath": "/api", "basePath": "/api",
"paths": { "paths": {
"/jobs/": { "/jobs/": {
@ -26,12 +25,12 @@
} }
], ],
"description": "Get a list of all jobs. Filters can be applied using query parameters.\nNumber of results can be limited by page. Results are sorted by descending startTime.", "description": "Get a list of all jobs. Filters can be applied using query parameters.\nNumber of results can be limited by page. Results are sorted by descending startTime.",
"consumes": [
"application/json"
],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"query"
],
"summary": "Lists all jobs", "summary": "Lists all jobs",
"parameters": [ "parameters": [
{ {
@ -62,13 +61,13 @@
}, },
{ {
"type": "integer", "type": "integer",
"description": "Items per page (If empty: No Limit)", "description": "Items per page (Default: 25)",
"name": "items-per-page", "name": "items-per-page",
"in": "query" "in": "query"
}, },
{ {
"type": "integer", "type": "integer",
"description": "Page Number (If empty: No Paging)", "description": "Page Number (Default: 1)",
"name": "page", "name": "page",
"in": "query" "in": "query"
}, },
@ -110,6 +109,221 @@
} }
} }
}, },
"/jobs/delete_job/": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to delete is specified by request body. All fields are required in this case.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"description": "All fields required",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.DeleteJobApiRequest"
}
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job/{id}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to remove is specified by database ID. This will not remove the job from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Database ID of Job",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job_before/{ts}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Unix epoch timestamp",
"name": "ts",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/start_job/": { "/jobs/start_job/": {
"post": { "post": {
"security": [ "security": [
@ -124,6 +338,9 @@
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Adds a new job as \"running\"", "summary": "Adds a new job as \"running\"",
"parameters": [ "parameters": [
{ {
@ -187,6 +404,9 @@
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving", "summary": "Marks job as completed and triggers archiving",
"parameters": [ "parameters": [
{ {
@ -201,7 +421,7 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Job resource", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/schema.JobMeta" "$ref": "#/definitions/schema.JobMeta"
} }
@ -259,6 +479,9 @@
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving", "summary": "Marks job as completed and triggers archiving",
"parameters": [ "parameters": [
{ {
@ -338,6 +561,9 @@
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Adds one or more tags to a job", "summary": "Adds one or more tags to a job",
"parameters": [ "parameters": [
{ {
@ -355,7 +581,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/api.Tag" "$ref": "#/definitions/api.ApiTag"
} }
} }
} }
@ -396,8 +622,53 @@
} }
}, },
"definitions": { "definitions": {
"api.ApiTag": {
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"api.DeleteJobApiRequest": {
"type": "object",
"required": [
"jobId"
],
"properties": {
"cluster": {
"description": "Cluster of job",
"type": "string",
"example": "fritz"
},
"jobId": {
"description": "Cluster Job ID of job",
"type": "integer",
"example": 123000
},
"startTime": {
"description": "Start Time of job as epoch",
"type": "integer",
"example": 1649723812
}
}
},
"api.DeleteJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.ErrorResponse": { "api.ErrorResponse": {
"description": "Error message as returned from backend.",
"type": "object", "type": "object",
"properties": { "properties": {
"error": { "error": {
@ -411,7 +682,6 @@
} }
}, },
"api.StartJobApiResponse": { "api.StartJobApiResponse": {
"description": "Successful job start response with database id of new job.",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -421,7 +691,6 @@
} }
}, },
"api.StopJobApiRequest": { "api.StopJobApiRequest": {
"description": "Request to stop running job using stoptime and final state. They are only required if no database id was provided with endpoint.",
"type": "object", "type": "object",
"required": [ "required": [
"jobState", "jobState",
@ -462,22 +731,6 @@
} }
} }
}, },
"api.Tag": {
"description": "Defines a tag using name and type.",
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"schema.Job": { "schema.Job": {
"description": "Information of a HPC job.", "description": "Information of a HPC job.",
"type": "object", "type": "object",
@ -831,10 +1084,14 @@
}, },
"securityDefinitions": { "securityDefinitions": {
"ApiKeyAuth": { "ApiKeyAuth": {
"description": "JWT based authentification for general API endpoint use.",
"type": "apiKey", "type": "apiKey",
"name": "X-Auth-Token", "name": "X-Auth-Token",
"in": "header" "in": "header"
} }
} },
"tags": [
{
"name": "Job API"
}
]
} }

View File

@ -1,7 +1,39 @@
basePath: /api basePath: /api
definitions: definitions:
api.ApiTag:
properties:
name:
description: Tag Name
example: Testjob
type: string
type:
description: Tag Type
example: Debug
type: string
type: object
api.DeleteJobApiRequest:
properties:
cluster:
description: Cluster of job
example: fritz
type: string
jobId:
description: Cluster Job ID of job
example: 123000
type: integer
startTime:
description: Start Time of job as epoch
example: 1649723812
type: integer
required:
- jobId
type: object
api.DeleteJobApiResponse:
properties:
msg:
type: string
type: object
api.ErrorResponse: api.ErrorResponse:
description: Error message as returned from backend.
properties: properties:
error: error:
description: Error Message description: Error Message
@ -11,15 +43,12 @@ definitions:
type: string type: string
type: object type: object
api.StartJobApiResponse: api.StartJobApiResponse:
description: Successful job start response with database id of new job.
properties: properties:
id: id:
description: Database ID of new job description: Database ID of new job
type: integer type: integer
type: object type: object
api.StopJobApiRequest: api.StopJobApiRequest:
description: Request to stop running job using stoptime and final state. They
are only required if no database id was provided with endpoint.
properties: properties:
cluster: cluster:
description: Cluster of job description: Cluster of job
@ -51,18 +80,6 @@ definitions:
- jobState - jobState
- stopTime - stopTime
type: object type: object
api.Tag:
description: Defines a tag using name and type.
properties:
name:
description: Tag Name
example: Testjob
type: string
type:
description: Tag Type
example: Debug
type: string
type: object
schema.Job: schema.Job:
description: Information of a HPC job. description: Information of a HPC job.
properties: properties:
@ -344,24 +361,21 @@ definitions:
example: Debug example: Debug
type: string type: string
type: object type: object
host: clustercockpit.localhost:8082 host: localhost:8080
info: info:
contact: contact:
email: support@clustercockpit.org email: support@clustercockpit.org
name: ClusterCockpit Project name: ClusterCockpit Project
url: https://github.com/ClusterCockpit url: https://github.com/ClusterCockpit
description: Defines a tag using name and type. description: API for batch job control.
license: license:
name: MIT License name: MIT License
url: https://opensource.org/licenses/MIT url: https://opensource.org/licenses/MIT
termsOfService: https://monitoring.nhr.fau.de/imprint
title: ClusterCockpit REST API title: ClusterCockpit REST API
version: 0.1.0 version: 0.2.0
paths: paths:
/jobs/: /jobs/:
get: get:
consumes:
- application/json
description: |- description: |-
Get a list of all jobs. Filters can be applied using query parameters. Get a list of all jobs. Filters can be applied using query parameters.
Number of results can be limited by page. Results are sorted by descending startTime. Number of results can be limited by page. Results are sorted by descending startTime.
@ -385,11 +399,11 @@ paths:
in: query in: query
name: start-time name: start-time
type: string type: string
- description: 'Items per page (If empty: No Limit)' - description: 'Items per page (Default: 25)'
in: query in: query
name: items-per-page name: items-per-page
type: integer type: integer
- description: 'Page Number (If empty: No Paging)' - description: 'Page Number (Default: 1)'
in: query in: query
name: page name: page
type: integer type: integer
@ -421,6 +435,152 @@ paths:
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Lists all jobs summary: Lists all jobs
tags:
- query
/jobs/delete_job/:
delete:
consumes:
- application/json
description: Job to delete is specified by request body. All fields are required
in this case.
parameters:
- description: All fields required
in: body
name: request
required: true
schema:
$ref: '#/definitions/api.DeleteJobApiRequest'
produces:
- application/json
responses:
"200":
description: Success message
schema:
$ref: '#/definitions/api.DeleteJobApiResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/api.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/api.ErrorResponse'
"404":
description: Resource not found
schema:
$ref: '#/definitions/api.ErrorResponse'
"422":
description: 'Unprocessable Entity: finding job failed: sql: no rows in
result set'
schema:
$ref: '#/definitions/api.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Remove a job from the sql database
tags:
- remove
/jobs/delete_job/{id}:
delete:
description: Job to remove is specified by database ID. This will not remove
the job from the job archive.
parameters:
- description: Database ID of Job
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Success message
schema:
$ref: '#/definitions/api.DeleteJobApiResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/api.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/api.ErrorResponse'
"404":
description: Resource not found
schema:
$ref: '#/definitions/api.ErrorResponse'
"422":
description: 'Unprocessable Entity: finding job failed: sql: no rows in
result set'
schema:
$ref: '#/definitions/api.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Remove a job from the sql database
tags:
- remove
/jobs/delete_job_before/{ts}:
delete:
description: Remove all jobs with start time before timestamp. The jobs will
not be removed from the job archive.
parameters:
- description: Unix epoch timestamp
in: path
name: ts
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Success message
schema:
$ref: '#/definitions/api.DeleteJobApiResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/api.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/api.ErrorResponse'
"404":
description: Resource not found
schema:
$ref: '#/definitions/api.ErrorResponse'
"422":
description: 'Unprocessable Entity: finding job failed: sql: no rows in
result set'
schema:
$ref: '#/definitions/api.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Remove a job from the sql database
tags:
- remove
/jobs/start_job/: /jobs/start_job/:
post: post:
consumes: consumes:
@ -466,6 +626,8 @@ paths:
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Adds a new job as "running" summary: Adds a new job as "running"
tags:
- add and modify
/jobs/stop_job/: /jobs/stop_job/:
post: post:
description: |- description: |-
@ -482,7 +644,7 @@ paths:
- application/json - application/json
responses: responses:
"200": "200":
description: Job resource description: Success message
schema: schema:
$ref: '#/definitions/schema.JobMeta' $ref: '#/definitions/schema.JobMeta'
"400": "400":
@ -513,6 +675,8 @@ paths:
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Marks job as completed and triggers archiving summary: Marks job as completed and triggers archiving
tags:
- add and modify
/jobs/stop_job/{id}: /jobs/stop_job/{id}:
post: post:
consumes: consumes:
@ -567,6 +731,8 @@ paths:
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Marks job as completed and triggers archiving summary: Marks job as completed and triggers archiving
tags:
- add and modify
/jobs/tag_job/{id}: /jobs/tag_job/{id}:
post: post:
consumes: consumes:
@ -586,7 +752,7 @@ paths:
required: true required: true
schema: schema:
items: items:
$ref: '#/definitions/api.Tag' $ref: '#/definitions/api.ApiTag'
type: array type: array
produces: produces:
- application/json - application/json
@ -614,10 +780,13 @@ paths:
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Adds one or more tags to a job summary: Adds one or more tags to a job
tags:
- add and modify
securityDefinitions: securityDefinitions:
ApiKeyAuth: ApiKeyAuth:
description: JWT based authentification for general API endpoint use.
in: header in: header
name: X-Auth-Token name: X-Auth-Token
type: apiKey type: apiKey
swagger: "2.0" swagger: "2.0"
tags:
- name: Job API

View File

@ -1,6 +1,5 @@
// Package api GENERATED BY SWAG; DO NOT EDIT // Package api GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag at // This file was generated by swaggo/swag
// 2022-09-22 13:31:53.353204065 +0200 CEST m=+0.139444562
package api package api
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@ -11,7 +10,6 @@ const docTemplate = `{
"info": { "info": {
"description": "{{escape .Description}}", "description": "{{escape .Description}}",
"title": "{{.Title}}", "title": "{{.Title}}",
"termsOfService": "https://monitoring.nhr.fau.de/imprint",
"contact": { "contact": {
"name": "ClusterCockpit Project", "name": "ClusterCockpit Project",
"url": "https://github.com/ClusterCockpit", "url": "https://github.com/ClusterCockpit",
@ -34,12 +32,12 @@ const docTemplate = `{
} }
], ],
"description": "Get a list of all jobs. Filters can be applied using query parameters.\nNumber of results can be limited by page. Results are sorted by descending startTime.", "description": "Get a list of all jobs. Filters can be applied using query parameters.\nNumber of results can be limited by page. Results are sorted by descending startTime.",
"consumes": [
"application/json"
],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"query"
],
"summary": "Lists all jobs", "summary": "Lists all jobs",
"parameters": [ "parameters": [
{ {
@ -70,13 +68,13 @@ const docTemplate = `{
}, },
{ {
"type": "integer", "type": "integer",
"description": "Items per page (If empty: No Limit)", "description": "Items per page (Default: 25)",
"name": "items-per-page", "name": "items-per-page",
"in": "query" "in": "query"
}, },
{ {
"type": "integer", "type": "integer",
"description": "Page Number (If empty: No Paging)", "description": "Page Number (Default: 1)",
"name": "page", "name": "page",
"in": "query" "in": "query"
}, },
@ -118,6 +116,221 @@ const docTemplate = `{
} }
} }
}, },
"/jobs/delete_job/": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to delete is specified by request body. All fields are required in this case.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"description": "All fields required",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.DeleteJobApiRequest"
}
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job/{id}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to remove is specified by database ID. This will not remove the job from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Database ID of Job",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job_before/{ts}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Unix epoch timestamp",
"name": "ts",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/start_job/": { "/jobs/start_job/": {
"post": { "post": {
"security": [ "security": [
@ -132,6 +345,9 @@ const docTemplate = `{
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Adds a new job as \"running\"", "summary": "Adds a new job as \"running\"",
"parameters": [ "parameters": [
{ {
@ -195,6 +411,9 @@ const docTemplate = `{
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving", "summary": "Marks job as completed and triggers archiving",
"parameters": [ "parameters": [
{ {
@ -209,7 +428,7 @@ const docTemplate = `{
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Job resource", "description": "Success message",
"schema": { "schema": {
"$ref": "#/definitions/schema.JobMeta" "$ref": "#/definitions/schema.JobMeta"
} }
@ -267,6 +486,9 @@ const docTemplate = `{
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving", "summary": "Marks job as completed and triggers archiving",
"parameters": [ "parameters": [
{ {
@ -346,6 +568,9 @@ const docTemplate = `{
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"add and modify"
],
"summary": "Adds one or more tags to a job", "summary": "Adds one or more tags to a job",
"parameters": [ "parameters": [
{ {
@ -363,7 +588,7 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/api.Tag" "$ref": "#/definitions/api.ApiTag"
} }
} }
} }
@ -404,8 +629,53 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"api.ApiTag": {
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"api.DeleteJobApiRequest": {
"type": "object",
"required": [
"jobId"
],
"properties": {
"cluster": {
"description": "Cluster of job",
"type": "string",
"example": "fritz"
},
"jobId": {
"description": "Cluster Job ID of job",
"type": "integer",
"example": 123000
},
"startTime": {
"description": "Start Time of job as epoch",
"type": "integer",
"example": 1649723812
}
}
},
"api.DeleteJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.ErrorResponse": { "api.ErrorResponse": {
"description": "Error message as returned from backend.",
"type": "object", "type": "object",
"properties": { "properties": {
"error": { "error": {
@ -419,7 +689,6 @@ const docTemplate = `{
} }
}, },
"api.StartJobApiResponse": { "api.StartJobApiResponse": {
"description": "Successful job start response with database id of new job.",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -429,7 +698,6 @@ const docTemplate = `{
} }
}, },
"api.StopJobApiRequest": { "api.StopJobApiRequest": {
"description": "Request to stop running job using stoptime and final state. They are only required if no database id was provided with endpoint.",
"type": "object", "type": "object",
"required": [ "required": [
"jobState", "jobState",
@ -470,22 +738,6 @@ const docTemplate = `{
} }
} }
}, },
"api.Tag": {
"description": "Defines a tag using name and type.",
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"schema.Job": { "schema.Job": {
"description": "Information of a HPC job.", "description": "Information of a HPC job.",
"type": "object", "type": "object",
@ -839,22 +1091,26 @@ const docTemplate = `{
}, },
"securityDefinitions": { "securityDefinitions": {
"ApiKeyAuth": { "ApiKeyAuth": {
"description": "JWT based authentification for general API endpoint use.",
"type": "apiKey", "type": "apiKey",
"name": "X-Auth-Token", "name": "X-Auth-Token",
"in": "header" "in": "header"
} }
} },
"tags": [
{
"name": "Job API"
}
]
}` }`
// SwaggerInfo holds exported Swagger Info so clients can modify it // SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{ var SwaggerInfo = &swag.Spec{
Version: "0.1.0", Version: "0.2.0",
Host: "clustercockpit.localhost:8082", Host: "localhost:8080",
BasePath: "/api", BasePath: "/api",
Schemes: []string{}, Schemes: []string{},
Title: "ClusterCockpit REST API", Title: "ClusterCockpit REST API",
Description: "Defines a tag using name and type.", Description: "API for batch job control.",
InfoInstanceName: "swagger", InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate, SwaggerTemplate: docTemplate,
} }

View File

@ -32,9 +32,10 @@ import (
) )
// @title ClusterCockpit REST API // @title ClusterCockpit REST API
// @version 0.1.0 // @version 0.2.0
// @description API for batch job control. // @description API for batch job control.
// @termsOfService https://monitoring.nhr.fau.de/imprint
// @tag.name Job API
// @contact.name ClusterCockpit Project // @contact.name ClusterCockpit Project
// @contact.url https://github.com/ClusterCockpit // @contact.url https://github.com/ClusterCockpit
@ -43,13 +44,12 @@ import (
// @license.name MIT License // @license.name MIT License
// @license.url https://opensource.org/licenses/MIT // @license.url https://opensource.org/licenses/MIT
// @host clustercockpit.localhost:8082 // @host localhost:8080
// @BasePath /api // @basePath /api
// @securityDefinitions.apikey ApiKeyAuth // @securityDefinitions.apikey ApiKeyAuth
// @in header // @in header
// @name X-Auth-Token // @name X-Auth-Token
// @description JWT based authentification for general API endpoint use.
type RestApi struct { type RestApi struct {
JobRepository *repository.JobRepository JobRepository *repository.JobRepository
@ -73,6 +73,9 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
// r.HandleFunc("/jobs/{id}", api.getJob).Methods(http.MethodGet) // r.HandleFunc("/jobs/{id}", api.getJob).Methods(http.MethodGet)
r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch) r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch)
r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet) r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet)
r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete)
r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete)
r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete)
if api.Authentication != nil { if api.Authentication != nil {
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet) r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
@ -90,15 +93,17 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
} }
// StartJobApiResponse model // StartJobApiResponse model
// @Description Successful job start response with database id of new job.
type StartJobApiResponse struct { type StartJobApiResponse struct {
// Database ID of new job // Database ID of new job
DBID int64 `json:"id"` DBID int64 `json:"id"`
} }
// DeleteJobApiResponse model
type DeleteJobApiResponse struct {
Message string `json:"msg"`
}
// StopJobApiRequest model // StopJobApiRequest model
// @Description Request to stop running job using stoptime and final state.
// @Description They are only required if no database id was provided with endpoint.
type StopJobApiRequest struct { type StopJobApiRequest struct {
// Stop Time of job as epoch // Stop Time of job as epoch
StopTime int64 `json:"stopTime" validate:"required" example:"1649763839"` StopTime int64 `json:"stopTime" validate:"required" example:"1649763839"`
@ -108,23 +113,28 @@ type StopJobApiRequest struct {
StartTime *int64 `json:"startTime" example:"1649723812"` // Start Time of job as epoch StartTime *int64 `json:"startTime" example:"1649723812"` // Start Time of job as epoch
} }
// DeleteJobApiRequest model
type DeleteJobApiRequest struct {
JobId *int64 `json:"jobId" validate:"required" example:"123000"` // Cluster Job ID of job
Cluster *string `json:"cluster" example:"fritz"` // Cluster of job
StartTime *int64 `json:"startTime" example:"1649723812"` // Start Time of job as epoch
}
// ErrorResponse model // ErrorResponse model
// @Description Error message as returned from backend.
type ErrorResponse struct { type ErrorResponse struct {
// Statustext of Errorcode // Statustext of Errorcode
Status string `json:"status"` Status string `json:"status"`
Error string `json:"error"` // Error Message Error string `json:"error"` // Error Message
} }
// Tag model // ApiTag model
// @Description Defines a tag using name and type. type ApiTag struct {
type Tag struct {
// Tag Type // Tag Type
Type string `json:"type" example:"Debug"` Type string `json:"type" example:"Debug"`
Name string `json:"name" example:"Testjob"` // Tag Name Name string `json:"name" example:"Testjob"` // Tag Name
} }
type TagJobApiRequest []*Tag type TagJobApiRequest []*ApiTag
func handleError(err error, statusCode int, rw http.ResponseWriter) { func handleError(err error, statusCode int, rw http.ResponseWriter) {
log.Warnf("REST API: %s", err.Error()) log.Warnf("REST API: %s", err.Error())
@ -143,28 +153,34 @@ func decode(r io.Reader, val interface{}) error {
} }
// getJobs godoc // getJobs godoc
// @Summary Lists all jobs // @summary Lists all jobs
// @Description Get a list of all jobs. Filters can be applied using query parameters. // @tags query
// @Description Number of results can be limited by page. Results are sorted by descending startTime. // @description Get a list of all jobs. Filters can be applied using query parameters.
// @Accept json // @description Number of results can be limited by page. Results are sorted by descending startTime.
// @Produce json // @produce json
// @Param state query string false "Job State" Enums(running, completed, failed, cancelled, stopped, timeout) // @param state query string false "Job State" Enums(running, completed, failed, cancelled, stopped, timeout)
// @Param cluster query string false "Job Cluster" // @param cluster query string false "Job Cluster"
// @Param start-time query string false "Syntax: '$from-$to', as unix epoch timestamps in seconds" // @param start-time query string false "Syntax: '$from-$to', as unix epoch timestamps in seconds"
// @Param items-per-page query int false "Items per page (If empty: No Limit)" // @param items-per-page query int false "Items per page (Default: 25)"
// @Param page query int false "Page Number (If empty: No Paging)" // @param page query int false "Page Number (Default: 1)"
// @Param with-metadata query bool false "Include metadata (e.g. jobScript) in response" // @param with-metadata query bool false "Include metadata (e.g. jobScript) in response"
// @Success 200 {array} schema.Job "Array of matching jobs" // @success 200 {array} schema.Job "Array of matching jobs"
// @Failure 400 {object} api.ErrorResponse "Bad Request" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error" // @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Security ApiKeyAuth // @security ApiKeyAuth
// @Router /jobs/ [get] // @router /jobs/ [get]
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
withMetadata := false withMetadata := false
filter := &model.JobFilter{} filter := &model.JobFilter{}
page := &model.PageRequest{ItemsPerPage: -1, Page: 1} page := &model.PageRequest{ItemsPerPage: 25, Page: 1}
order := &model.OrderByInput{Field: "startTime", Order: model.SortDirectionEnumDesc} order := &model.OrderByInput{Field: "startTime", Order: model.SortDirectionEnumDesc}
for key, vals := range r.URL.Query() { for key, vals := range r.URL.Query() {
switch key { switch key {
case "state": case "state":
@ -270,21 +286,27 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
} }
// tagJob godoc // tagJob godoc
// @Summary Adds one or more tags to a job // @summary Adds one or more tags to a job
// @Description Adds tag(s) to a job specified by DB ID. Name and Type of Tag(s) can be chosen freely. // @tags add and modify
// @Description If tagged job is already finished: Tag will be written directly to respective archive files. // @description Adds tag(s) to a job specified by DB ID. Name and Type of Tag(s) can be chosen freely.
// @Accept json // @description If tagged job is already finished: Tag will be written directly to respective archive files.
// @Produce json // @accept json
// @Param id path int true "Job Database ID" // @produce json
// @Param request body api.TagJobApiRequest true "Array of tag-objects to add" // @param id path int true "Job Database ID"
// @Success 200 {object} schema.Job "Updated job resource" // @param request body api.TagJobApiRequest true "Array of tag-objects to add"
// @Failure 400 {object} api.ErrorResponse "Bad Request" // @success 200 {object} schema.Job "Updated job resource"
// @Failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 404 {object} api.ErrorResponse "Job or tag does not exist" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error" // @failure 404 {object} api.ErrorResponse "Job or tag does not exist"
// @Security ApiKeyAuth // @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Router /jobs/tag_job/{id} [post] // @security ApiKeyAuth
// @router /jobs/tag_job/{id} [post]
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
iid, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) iid, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest) http.Error(rw, err.Error(), http.StatusBadRequest)
@ -329,20 +351,21 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
} }
// startJob godoc // startJob godoc
// @Summary Adds a new job as "running" // @summary Adds a new job as "running"
// @Description Job specified in request body will be saved to database as "running" with new DB ID. // @tags add and modify
// @Description Job specifications follow the 'JobMeta' scheme, API will fail to execute if requirements are not met. // @description Job specified in request body will be saved to database as "running" with new DB ID.
// @Accept json // @description Job specifications follow the 'JobMeta' scheme, API will fail to execute if requirements are not met.
// @Produce json // @accept json
// @Param request body schema.JobMeta true "Job to add" // @produce json
// @Success 201 {object} api.StartJobApiResponse "Job added successfully" // @param request body schema.JobMeta true "Job to add"
// @Failure 400 {object} api.ErrorResponse "Bad Request" // @success 201 {object} api.StartJobApiResponse "Job added successfully"
// @Failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 403 {object} api.ErrorResponse "Forbidden" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 422 {object} api.ErrorResponse "Unprocessable Entity: The combination of jobId, clusterId and startTime does already exist" // @failure 403 {object} api.ErrorResponse "Forbidden"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error" // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: The combination of jobId, clusterId and startTime does already exist"
// @Security ApiKeyAuth // @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Router /jobs/start_job/ [post] // @security ApiKeyAuth
// @router /jobs/start_job/ [post]
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw) handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
@ -407,22 +430,23 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
} }
// stopJobById godoc // stopJobById godoc
// @Summary Marks job as completed and triggers archiving // @summary Marks job as completed and triggers archiving
// @Description Job to stop is specified by database ID. Only stopTime and final state are required in request body. // @tags add and modify
// @Description Returns full job resource information according to 'JobMeta' scheme. // @description Job to stop is specified by database ID. Only stopTime and final state are required in request body.
// @Accept json // @description Returns full job resource information according to 'JobMeta' scheme.
// @Produce json // @accept json
// @Param id path int true "Database ID of Job" // @produce json
// @Param request body api.StopJobApiRequest true "stopTime and final state in request body" // @param id path int true "Database ID of Job"
// @Success 200 {object} schema.JobMeta "Job resource" // @param request body api.StopJobApiRequest true "stopTime and final state in request body"
// @Failure 400 {object} api.ErrorResponse "Bad Request" // @success 200 {object} schema.JobMeta "Job resource"
// @Failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 403 {object} api.ErrorResponse "Forbidden" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 404 {object} api.ErrorResponse "Resource not found" // @failure 403 {object} api.ErrorResponse "Forbidden"
// @Failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 404 {object} api.ErrorResponse "Resource not found"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error" // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @Security ApiKeyAuth // @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Router /jobs/stop_job/{id} [post] // @security ApiKeyAuth
// @router /jobs/stop_job/{id} [post]
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw) handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
@ -461,20 +485,21 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
} }
// stopJobByRequest godoc // stopJobByRequest godoc
// @Summary Marks job as completed and triggers archiving // @summary Marks job as completed and triggers archiving
// @Description Job to stop is specified by request body. All fields are required in this case. // @tags add and modify
// @Description Returns full job resource information according to 'JobMeta' scheme. // @description Job to stop is specified by request body. All fields are required in this case.
// @Produce json // @description Returns full job resource information according to 'JobMeta' scheme.
// @Param request body api.StopJobApiRequest true "All fields required" // @produce json
// @Success 200 {object} schema.JobMeta "Job resource" // @param request body api.StopJobApiRequest true "All fields required"
// @Failure 400 {object} api.ErrorResponse "Bad Request" // @success 200 {object} schema.JobMeta "Success message"
// @Failure 401 {object} api.ErrorResponse "Unauthorized" // @failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 403 {object} api.ErrorResponse "Forbidden" // @failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 404 {object} api.ErrorResponse "Resource not found" // @failure 403 {object} api.ErrorResponse "Forbidden"
// @Failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 404 {object} api.ErrorResponse "Resource not found"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error" // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @Security ApiKeyAuth // @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Router /jobs/stop_job/ [post] // @security ApiKeyAuth
// @router /jobs/stop_job/ [post]
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw) handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
@ -506,6 +531,159 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
api.checkAndHandleStopJob(rw, job, req) api.checkAndHandleStopJob(rw, job, req)
} }
// deleteJobById godoc
// @summary Remove a job from the sql database
// @tags remove
// @description Job to remove is specified by database ID. This will not remove the job from the job archive.
// @produce json
// @param id path int true "Database ID of Job"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/{id} [delete]
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
// Fetch job (that will be stopped) from db
id, ok := mux.Vars(r)["id"]
var err error
if ok {
id, e := strconv.ParseInt(id, 10, 64)
if e != nil {
handleError(fmt.Errorf("integer expected in path for id: %w", e), http.StatusBadRequest, rw)
return
}
err = api.JobRepository.DeleteJobById(id)
} else {
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
return
}
if err != nil {
handleError(fmt.Errorf("deleting job failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(DeleteJobApiResponse{
Message: fmt.Sprintf("Successfully deleted job %s", id),
})
}
// deleteJobByRequest godoc
// @summary Remove a job from the sql database
// @tags remove
// @description Job to delete is specified by request body. All fields are required in this case.
// @accept json
// @produce json
// @param request body api.DeleteJobApiRequest true "All fields required"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/ [delete]
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
// Parse request body
req := DeleteJobApiRequest{}
if err := decode(r.Body, &req); err != nil {
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
return
}
// Fetch job (that will be deleted) from db
var job *schema.Job
var err error
if req.JobId == nil {
handleError(errors.New("the field 'jobId' is required"), http.StatusBadRequest, rw)
return
}
job, err = api.JobRepository.Find(req.JobId, req.Cluster, req.StartTime)
if err != nil {
handleError(fmt.Errorf("finding job failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
err = api.JobRepository.DeleteJobById(job.ID)
if err != nil {
handleError(fmt.Errorf("deleting job failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(DeleteJobApiResponse{
Message: fmt.Sprintf("Successfully deleted job %d", job.ID),
})
}
// deleteJobBefore godoc
// @summary Remove a job from the sql database
// @tags remove
// @description Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.
// @produce json
// @param ts path int true "Unix epoch timestamp"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job_before/{ts} [delete]
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
var cnt int
// Fetch job (that will be stopped) from db
id, ok := mux.Vars(r)["ts"]
var err error
if ok {
ts, e := strconv.ParseInt(id, 10, 64)
if e != nil {
handleError(fmt.Errorf("integer expected in path for ts: %w", e), http.StatusBadRequest, rw)
return
}
cnt, err = api.JobRepository.DeleteJobsBefore(ts)
} else {
handleError(errors.New("the parameter 'ts' is required"), http.StatusBadRequest, rw)
return
}
if err != nil {
handleError(fmt.Errorf("deleting jobs failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(DeleteJobApiResponse{
Message: fmt.Sprintf("Successfully deleted %d jobs", cnt),
})
}
func (api *RestApi) checkAndHandleStopJob(rw http.ResponseWriter, job *schema.Job, req StopJobApiRequest) { func (api *RestApi) checkAndHandleStopJob(rw http.ResponseWriter, job *schema.Job, req StopJobApiRequest) {
// Sanity checks // Sanity checks

View File

@ -246,6 +246,29 @@ func (r *JobRepository) Stop(
return return
} }
func (r *JobRepository) DeleteJobsBefore(startTime int64) (int, error) {
var cnt int
qs := fmt.Sprintf("SELECT count(*) FROM job WHERE job.start_time < %d", startTime)
err := r.DB.Get(&cnt, qs) //ignore error as it will also occur in delete statement
_, err = r.DB.Exec(`DELETE FROM job WHERE job.start_time < ?`, startTime)
if err != nil {
log.Warnf(" DeleteJobsBefore(%d): error %v", startTime, err)
} else {
log.Infof("DeleteJobsBefore(%d): Deleted %d jobs", startTime, cnt)
}
return cnt, err
}
func (r *JobRepository) DeleteJobById(id int64) error {
_, err := r.DB.Exec(`DELETE FROM job WHERE job.id = ?`, id)
if err != nil {
log.Warnf("DeleteJobById(%d): error %v", id, err)
} else {
log.Infof("DeleteJobById(%d): Success", id)
}
return err
}
// TODO: Use node hours instead: SELECT job.user, sum(job.num_nodes * (CASE WHEN job.job_state = "running" THEN CAST(strftime('%s', 'now') AS INTEGER) - job.start_time ELSE job.duration END)) as x FROM job GROUP BY user ORDER BY x DESC; // TODO: Use node hours instead: SELECT job.user, sum(job.num_nodes * (CASE WHEN job.job_state = "running" THEN CAST(strftime('%s', 'now') AS INTEGER) - job.start_time ELSE job.duration END)) as x FROM job GROUP BY user ORDER BY x DESC;
func (r *JobRepository) CountGroupedJobs(ctx context.Context, aggreg model.Aggregate, filters []*model.JobFilter, weight *model.Weights, limit *int) (map[string]int, error) { func (r *JobRepository) CountGroupedJobs(ctx context.Context, aggreg model.Aggregate, filters []*model.JobFilter, weight *model.Weights, limit *int) (map[string]int, error) {
if !aggreg.IsValid() { if !aggreg.IsValid() {

View File

@ -17,26 +17,26 @@ import (
type BaseJob struct { type BaseJob struct {
// The unique identifier of a job // The unique identifier of a job
JobID int64 `json:"jobId" db:"job_id" example:"123000"` JobID int64 `json:"jobId" db:"job_id" example:"123000"`
User string `json:"user" db:"user" example:"abcd100h"` // The unique identifier of a user User string `json:"user" db:"user" example:"abcd100h"` // The unique identifier of a user
Project string `json:"project" db:"project" example:"abcd200"` // The unique identifier of a project Project string `json:"project" db:"project" example:"abcd200"` // The unique identifier of a project
Cluster string `json:"cluster" db:"cluster" example:"fritz"` // The unique identifier of a cluster Cluster string `json:"cluster" db:"cluster" example:"fritz"` // The unique identifier of a cluster
SubCluster string `json:"subCluster" db:"subcluster" example:"main"` // The unique identifier of a sub cluster SubCluster string `json:"subCluster" db:"subcluster" example:"main"` // The unique identifier of a sub cluster
Partition string `json:"partition" db:"partition" example:"main"` // The Slurm partition to which the job was submitted Partition string `json:"partition" db:"partition" example:"main"` // The Slurm partition to which the job was submitted
ArrayJobId int64 `json:"arrayJobId" db:"array_job_id" example:"123000"` // The unique identifier of an array job ArrayJobId int64 `json:"arrayJobId" db:"array_job_id" example:"123000"` // The unique identifier of an array job
NumNodes int32 `json:"numNodes" db:"num_nodes" example:"2" minimum:"1"` // Number of nodes used (Min > 0) NumNodes int32 `json:"numNodes" db:"num_nodes" example:"2" minimum:"1"` // Number of nodes used (Min > 0)
NumHWThreads int32 `json:"numHwthreads" db:"num_hwthreads" example:"20" minimum:"1"` // Number of HWThreads used (Min > 0) NumHWThreads int32 `json:"numHwthreads" db:"num_hwthreads" example:"20" minimum:"1"` // Number of HWThreads used (Min > 0)
NumAcc int32 `json:"numAcc" db:"num_acc" example:"2" minimum:"1"` // Number of accelerators used (Min > 0) NumAcc int32 `json:"numAcc" db:"num_acc" example:"2" minimum:"1"` // Number of accelerators used (Min > 0)
Exclusive int32 `json:"exclusive" db:"exclusive" example:"1" minimum:"0" maximum:"2"` // Specifies how nodes are shared: 0 - Shared among multiple jobs of multiple users, 1 - Job exclusive (Default), 2 - Shared among multiple jobs of same user Exclusive int32 `json:"exclusive" db:"exclusive" example:"1" minimum:"0" maximum:"2"` // Specifies how nodes are shared: 0 - Shared among multiple jobs of multiple users, 1 - Job exclusive (Default), 2 - Shared among multiple jobs of same user
MonitoringStatus int32 `json:"monitoringStatus" db:"monitoring_status" example:"1" minimum:"0" maximum:"3"` // State of monitoring system during job run: 0 - Disabled, 1 - Running or Archiving (Default), 2 - Archiving Failed, 3 - Archiving Successfull MonitoringStatus int32 `json:"monitoringStatus" db:"monitoring_status" example:"1" minimum:"0" maximum:"3"` // State of monitoring system during job run: 0 - Disabled, 1 - Running or Archiving (Default), 2 - Archiving Failed, 3 - Archiving Successfull
SMT int32 `json:"smt" db:"smt" example:"4"` // SMT threads used by job SMT int32 `json:"smt" db:"smt" example:"4"` // SMT threads used by job
State JobState `json:"jobState" db:"job_state" example:"completed" enums:"completed,failed,cancelled,stopped,timeout,out_of_memory"` // Final state of job State JobState `json:"jobState" db:"job_state" example:"completed" enums:"completed,failed,cancelled,stopped,timeout,out_of_memory"` // Final state of job
Duration int32 `json:"duration" db:"duration" example:"43200" minimum:"1"` // Duration of job in seconds (Min > 0) Duration int32 `json:"duration" db:"duration" example:"43200" minimum:"1"` // Duration of job in seconds (Min > 0)
Walltime int64 `json:"walltime" db:"walltime" example:"86400" minimum:"1"` // Requested walltime of job in seconds (Min > 0) Walltime int64 `json:"walltime" db:"walltime" example:"86400" minimum:"1"` // Requested walltime of job in seconds (Min > 0)
Tags []*Tag `json:"tags"` // List of tags Tags []*Tag `json:"tags"` // List of tags
RawResources []byte `json:"-" db:"resources"` // Resources used by job [As Bytes] RawResources []byte `json:"-" db:"resources"` // Resources used by job [As Bytes]
Resources []*Resource `json:"resources"` // Resources used by job Resources []*Resource `json:"resources"` // Resources used by job
RawMetaData []byte `json:"-" db:"meta_data"` // Additional information about the job [As Bytes] RawMetaData []byte `json:"-" db:"meta_data"` // Additional information about the job [As Bytes]
MetaData map[string]string `json:"metaData"` // Additional information about the job MetaData map[string]string `json:"metaData"` // Additional information about the job
} }
// Non-Swaggered Comment: Job // Non-Swaggered Comment: Job
@ -49,15 +49,15 @@ type Job struct {
ID int64 `json:"id" db:"id"` ID int64 `json:"id" db:"id"`
BaseJob BaseJob
StartTimeUnix int64 `json:"-" db:"start_time" example:"1649723812"` // Start epoch time stamp in seconds StartTimeUnix int64 `json:"-" db:"start_time" example:"1649723812"` // Start epoch time stamp in seconds
StartTime time.Time `json:"startTime"` // Start time as 'time.Time' data type StartTime time.Time `json:"startTime"` // Start time as 'time.Time' data type
MemUsedMax float64 `json:"-" db:"mem_used_max"` // MemUsedMax as Float64 MemUsedMax float64 `json:"-" db:"mem_used_max"` // MemUsedMax as Float64
FlopsAnyAvg float64 `json:"-" db:"flops_any_avg"` // FlopsAnyAvg as Float64 FlopsAnyAvg float64 `json:"-" db:"flops_any_avg"` // FlopsAnyAvg as Float64
MemBwAvg float64 `json:"-" db:"mem_bw_avg"` // MemBwAvg as Float64 MemBwAvg float64 `json:"-" db:"mem_bw_avg"` // MemBwAvg as Float64
LoadAvg float64 `json:"-" db:"load_avg"` // LoadAvg as Float64 LoadAvg float64 `json:"-" db:"load_avg"` // LoadAvg as Float64
NetBwAvg float64 `json:"-" db:"net_bw_avg"` // NetBwAvg as Float64 NetBwAvg float64 `json:"-" db:"net_bw_avg"` // NetBwAvg as Float64
NetDataVolTotal float64 `json:"-" db:"net_data_vol_total"` // NetDataVolTotal as Float64 NetDataVolTotal float64 `json:"-" db:"net_data_vol_total"` // NetDataVolTotal as Float64
FileBwAvg float64 `json:"-" db:"file_bw_avg"` // FileBwAvg as Float64 FileBwAvg float64 `json:"-" db:"file_bw_avg"` // FileBwAvg as Float64
FileDataVolTotal float64 `json:"-" db:"file_data_vol_total"` // FileDataVolTotal as Float64 FileDataVolTotal float64 `json:"-" db:"file_data_vol_total"` // FileDataVolTotal as Float64
} }
// Non-Swaggered Comment: JobMeta // Non-Swaggered Comment: JobMeta
@ -70,11 +70,11 @@ type Job struct {
// JobMeta model // JobMeta model
// @Description Meta data information of a HPC job. // @Description Meta data information of a HPC job.
type JobMeta struct { type JobMeta struct {
// The unique identifier of a job in the database // The unique identifier of a job in the database
ID *int64 `json:"id,omitempty"` ID *int64 `json:"id,omitempty"`
BaseJob BaseJob
StartTime int64 `json:"startTime" db:"start_time" example:"1649723812" minimum:"1"` // Start epoch time stamp in seconds (Min > 0) StartTime int64 `json:"startTime" db:"start_time" example:"1649723812" minimum:"1"` // Start epoch time stamp in seconds (Min > 0)
Statistics map[string]JobStatistics `json:"statistics,omitempty"` // Metric statistics of job Statistics map[string]JobStatistics `json:"statistics,omitempty"` // Metric statistics of job
} }
const ( const (
@ -102,18 +102,18 @@ type JobStatistics struct {
// Tag model // Tag model
// @Description Defines a tag using name and type. // @Description Defines a tag using name and type.
type Tag struct { type Tag struct {
// The unique DB identifier of a tag // The unique DB identifier of a tag
ID int64 `json:"id" db:"id"` ID int64 `json:"id" db:"id"`
Type string `json:"type" db:"tag_type" example:"Debug"` // Tag Type Type string `json:"type" db:"tag_type" example:"Debug"` // Tag Type
Name string `json:"name" db:"tag_name" example:"Testjob"` // Tag Name Name string `json:"name" db:"tag_name" example:"Testjob"` // Tag Name
} }
// Resource model // Resource model
// @Description A resource used by a job // @Description A resource used by a job
type Resource struct { type Resource struct {
Hostname string `json:"hostname"` // Name of the host (= node) Hostname string `json:"hostname"` // Name of the host (= node)
HWThreads []int `json:"hwthreads,omitempty"` // List of OS processor ids HWThreads []int `json:"hwthreads,omitempty"` // List of OS processor ids
Accelerators []string `json:"accelerators,omitempty"` // List of of accelerator device ids Accelerators []string `json:"accelerators,omitempty"` // List of of accelerator device ids
Configuration string `json:"configuration,omitempty"` // The configuration options of the node Configuration string `json:"configuration,omitempty"` // The configuration options of the node
} }

View File

@ -343,7 +343,7 @@ func TestRestApi(t *testing.T) {
restapi.MountRoutes(r) restapi.MountRoutes(r)
const startJobBody string = `{ const startJobBody string = `{
"jobId": 123, "jobId": 123,
"user": "testuser", "user": "testuser",
"project": "testproj", "project": "testproj",
"cluster": "testcluster", "cluster": "testcluster",
@ -542,7 +542,7 @@ func subtestLetJobFail(t *testing.T, restapi *api.RestApi, r *mux.Router) {
} }
const stopJobBody string = `{ const stopJobBody string = `{
"jobId": 12345, "jobId": 12345,
"cluster": "testcluster", "cluster": "testcluster",
"jobState": "failed", "jobState": "failed",