mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 00:38:33 +02:00
feat(proxmox): add journalctl endpoint without service; add limit parameter
Added new Proxmox journalctl endpoint `/journalctl/:node/:vmid` for viewing all journalctl output without requiring a service name. Made the service parameter optional across both endpoints. Introduced configurable `limit` query parameter (1-1000, default 100) to both proxmox journalctl and docker logs APIs, replacing hardcoded 100-line tail. Added container status check in LXCCommand to prevent command execution on stopped containers, returning a clear status message instead. Refactored route validation to use pre-fetched IPs and improved References() method for proxmox routes with better alias handling.
This commit is contained in:
@@ -146,6 +146,7 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
|||||||
|
|
||||||
proxmox := v1.Group("/proxmox")
|
proxmox := v1.Group("/proxmox")
|
||||||
{
|
{
|
||||||
|
proxmox.GET("/journalctl/:node/:vmid", proxmoxApi.Journalctl)
|
||||||
proxmox.GET("/journalctl/:node/:vmid/:service", proxmoxApi.Journalctl)
|
proxmox.GET("/journalctl/:node/:vmid/:service", proxmoxApi.Journalctl)
|
||||||
proxmox.GET("/stats/:node/:vmid", proxmoxApi.Stats)
|
proxmox.GET("/stats/:node/:vmid", proxmoxApi.Stats)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/api/pkg/stdcopy"
|
"github.com/moby/moby/api/pkg/stdcopy"
|
||||||
@@ -22,6 +23,7 @@ type LogsQueryParams struct {
|
|||||||
Since string `form:"from"`
|
Since string `form:"from"`
|
||||||
Until string `form:"to"`
|
Until string `form:"to"`
|
||||||
Levels string `form:"levels"`
|
Levels string `form:"levels"`
|
||||||
|
Limit int `form:"limit,default=100" binding:"omitempty,min=1,max=1000"`
|
||||||
} // @name LogsQueryParams
|
} // @name LogsQueryParams
|
||||||
|
|
||||||
// @x-id "logs"
|
// @x-id "logs"
|
||||||
@@ -34,9 +36,10 @@ type LogsQueryParams struct {
|
|||||||
// @Param id path string true "container id"
|
// @Param id path string true "container id"
|
||||||
// @Param stdout query bool false "show stdout"
|
// @Param stdout query bool false "show stdout"
|
||||||
// @Param stderr query bool false "show stderr"
|
// @Param stderr query bool false "show stderr"
|
||||||
// @Param from query string false "from timestamp"
|
// @Param from query string false "from timestamp"
|
||||||
// @Param to query string false "to timestamp"
|
// @Param to query string false "to timestamp"
|
||||||
// @Param levels query string false "levels"
|
// @Param levels query string false "levels"
|
||||||
|
// @Param limit query int false "limit"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
// @Failure 400 {object} apitypes.ErrorResponse
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
@@ -77,7 +80,7 @@ func Logs(c *gin.Context) {
|
|||||||
Until: queryParams.Until,
|
Until: queryParams.Until,
|
||||||
Timestamps: true,
|
Timestamps: true,
|
||||||
Follow: true,
|
Follow: true,
|
||||||
Tail: "100",
|
Tail: strconv.Itoa(queryParams.Limit),
|
||||||
}
|
}
|
||||||
if queryParams.Levels != "" {
|
if queryParams.Levels != "" {
|
||||||
opts.Details = true
|
opts.Details = true
|
||||||
|
|||||||
@@ -165,6 +165,76 @@
|
|||||||
"operationId": "verify"
|
"operationId": "verify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/proxmox/journalctl/{node}/{vmid}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get journalctl output",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"proxmox",
|
||||||
|
"websocket"
|
||||||
|
],
|
||||||
|
"summary": "Get journalctl output",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"name": "node",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"name": "vmid",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "limit",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Journalctl output",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Node not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-id": "journalctl",
|
||||||
|
"operationId": "journalctl"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/proxmox/journalctl/{node}/{vmid}/{service}": {
|
"/api/v1/proxmox/journalctl/{node}/{vmid}/{service}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get journalctl output",
|
"description": "Get journalctl output",
|
||||||
@@ -189,14 +259,19 @@
|
|||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"name": "service",
|
"name": "service",
|
||||||
"in": "path",
|
"in": "path"
|
||||||
"required": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"name": "vmid",
|
"name": "vmid",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "limit",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -703,6 +778,12 @@
|
|||||||
"description": "levels",
|
"description": "levels",
|
||||||
"name": "levels",
|
"name": "levels",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "limit",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
@@ -2088,6 +2088,52 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- agent
|
- agent
|
||||||
x-id: verify
|
x-id: verify
|
||||||
|
/api/v1/proxmox/journalctl/{node}/{vmid}:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get journalctl output
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: node
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- in: path
|
||||||
|
name: vmid
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: limit
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Journalctl output
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: Invalid request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"403":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Node not found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
summary: Get journalctl output
|
||||||
|
tags:
|
||||||
|
- proxmox
|
||||||
|
- websocket
|
||||||
|
x-id: journalctl
|
||||||
/api/v1/proxmox/journalctl/{node}/{vmid}/{service}:
|
/api/v1/proxmox/journalctl/{node}/{vmid}/{service}:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -2100,12 +2146,15 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
- in: path
|
- in: path
|
||||||
name: service
|
name: service
|
||||||
required: true
|
|
||||||
type: string
|
type: string
|
||||||
- in: path
|
- in: path
|
||||||
name: vmid
|
name: vmid
|
||||||
required: true
|
required: true
|
||||||
type: integer
|
type: integer
|
||||||
|
- description: limit
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -2438,6 +2487,10 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
name: levels
|
name: levels
|
||||||
type: string
|
type: string
|
||||||
|
- description: limit
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import (
|
|||||||
type JournalctlRequest struct {
|
type JournalctlRequest struct {
|
||||||
Node string `uri:"node" binding:"required"`
|
Node string `uri:"node" binding:"required"`
|
||||||
VMID int `uri:"vmid" binding:"required"`
|
VMID int `uri:"vmid" binding:"required"`
|
||||||
Service string `uri:"service" binding:"required"`
|
Service string `uri:"service"`
|
||||||
|
Limit int `query:"limit" binding:"omitempty,min=1,max=1000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @x-id "journalctl"
|
// @x-id "journalctl"
|
||||||
@@ -24,12 +25,14 @@ type JournalctlRequest struct {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce application/json
|
// @Produce application/json
|
||||||
// @Param path path JournalctlRequest true "Request"
|
// @Param path path JournalctlRequest true "Request"
|
||||||
|
// @Param limit query int false "limit"
|
||||||
// @Success 200 string plain "Journalctl output"
|
// @Success 200 string plain "Journalctl output"
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
// @Router /api/v1/proxmox/journalctl/{node}/{vmid}/{service} [get]
|
// @Router /api/v1/proxmox/journalctl/{node}/{vmid} [get]
|
||||||
|
// @Router /api/v1/proxmox/journalctl/{node}/{vmid}/{service} [get]
|
||||||
func Journalctl(c *gin.Context) {
|
func Journalctl(c *gin.Context) {
|
||||||
var request JournalctlRequest
|
var request JournalctlRequest
|
||||||
if err := c.ShouldBindUri(&request); err != nil {
|
if err := c.ShouldBindUri(&request); err != nil {
|
||||||
@@ -50,7 +53,7 @@ func Journalctl(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer manager.Close()
|
defer manager.Close()
|
||||||
|
|
||||||
reader, err := node.LXCJournalctl(c.Request.Context(), request.VMID, request.Service)
|
reader, err := node.LXCJournalctl(c.Request.Context(), request.VMID, request.Service, request.Limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.Rea
|
|||||||
return nil, fmt.Errorf("failed to get node: %w", err)
|
return nil, fmt.Errorf("failed to get node: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lxc, err := node.Container(ctx, vmid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lxc.Status != "running" {
|
||||||
|
return io.NopCloser(bytes.NewReader(fmt.Appendf(nil, "container %d is not running, status: %s\n", vmid, lxc.Status))), nil
|
||||||
|
}
|
||||||
|
|
||||||
term, err := node.TermProxy(ctx)
|
term, err := node.TermProxy(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get term proxy: %w", err)
|
return nil, fmt.Errorf("failed to get term proxy: %w", err)
|
||||||
@@ -117,6 +126,16 @@ func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.Rea
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LXCJournalctl streams journalctl output for the given service.
|
// LXCJournalctl streams journalctl output for the given service.
|
||||||
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, service string) (io.ReadCloser, error) {
|
//
|
||||||
return n.LXCCommand(ctx, vmid, fmt.Sprintf("journalctl -u %q -f", service))
|
// If service is not empty, it will be used to filter the output by service.
|
||||||
|
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||||
|
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, service string, limit int) (io.ReadCloser, error) {
|
||||||
|
command := "journalctl -f"
|
||||||
|
if service != "" {
|
||||||
|
command = fmt.Sprintf("journalctl -u %q -f", service)
|
||||||
|
}
|
||||||
|
if limit > 0 {
|
||||||
|
command = fmt.Sprintf("%s -n %d", command, limit)
|
||||||
|
}
|
||||||
|
return n.LXCCommand(ctx, vmid, command)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,11 +219,7 @@ func (r *Route) validate() gperr.Error {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
ips, err := node.LXCGetIPs(ctx, vmid)
|
ips := res.IPs
|
||||||
if err != nil {
|
|
||||||
return gperr.Errorf("failed to get ip addresses of vmid %d: %w", vmid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
return gperr.Multiline().
|
return gperr.Multiline().
|
||||||
Addf("no ip addresses found for %s", containerName).
|
Addf("no ip addresses found for %s", containerName).
|
||||||
@@ -345,10 +341,9 @@ func (r *Route) validate() gperr.Error {
|
|||||||
// reverse lookup resource by ip address, hostname or alias
|
// reverse lookup resource by ip address, hostname or alias
|
||||||
if resource != nil {
|
if resource != nil {
|
||||||
r.Proxmox = &proxmox.NodeConfig{
|
r.Proxmox = &proxmox.NodeConfig{
|
||||||
Node: resource.Node,
|
Node: resource.Node,
|
||||||
VMID: int(resource.VMID),
|
VMID: int(resource.VMID),
|
||||||
VMName: resource.Name,
|
VMName: resource.Name,
|
||||||
Service: r.Alias,
|
|
||||||
}
|
}
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("node", resource.Node).
|
Str("node", resource.Node).
|
||||||
@@ -535,17 +530,23 @@ func (r *Route) References() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.Container != nil {
|
if r.Container != nil {
|
||||||
if r.Container.ContainerName != r.Alias {
|
if r.Container.ContainerName != aliasRef {
|
||||||
return []string{r.Container.ContainerName, aliasRef, r.Container.Image.Name, r.Container.Image.Author}
|
return []string{r.Container.ContainerName, aliasRef, r.Container.Image.Name, r.Container.Image.Author}
|
||||||
}
|
}
|
||||||
return []string{r.Container.Image.Name, aliasRef, r.Container.Image.Author}
|
return []string{r.Container.Image.Name, aliasRef, r.Container.Image.Author}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Proxmox != nil {
|
if r.Proxmox != nil {
|
||||||
if r.Proxmox.VMName != r.Alias {
|
if r.Proxmox.Service != "" && r.Proxmox.Service != aliasRef {
|
||||||
return []string{r.Proxmox.VMName, aliasRef, r.Proxmox.Service}
|
if r.Proxmox.VMName != aliasRef {
|
||||||
|
return []string{r.Proxmox.VMName, aliasRef, r.Proxmox.Service}
|
||||||
|
}
|
||||||
|
return []string{r.Proxmox.Service, aliasRef}
|
||||||
|
} else {
|
||||||
|
if r.Proxmox.VMName != aliasRef {
|
||||||
|
return []string{r.Proxmox.VMName, aliasRef}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return []string{r.Proxmox.Service, aliasRef}
|
|
||||||
}
|
}
|
||||||
return []string{aliasRef}
|
return []string{aliasRef}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user