mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 17:28:53 +02:00
feat(proxmox): add tail endpoint and enhance journalctl with multi-service support
Add new `/proxmox/tail` API endpoint for streaming file contents from Proxmox nodes and LXC containers via WebSocket. Extend journalctl endpoint to support filtering by multiple services simultaneously. Changes: - Add `GET /proxmox/tail` endpoint supporting node-level and LXC container file tailing - Change `service` parameter from string to array in journalctl endpoints - Add input validation (`checkValidInput`) to prevent command injection - Refactor command formatting with proper shell quoting Security: All command inputs are validated for dangerous characters before
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("/tail", proxmoxApi.Tail)
|
||||||
proxmox.GET("/journalctl", proxmoxApi.Journalctl)
|
proxmox.GET("/journalctl", proxmoxApi.Journalctl)
|
||||||
proxmox.GET("/journalctl/:node", proxmoxApi.Journalctl)
|
proxmox.GET("/journalctl/:node", proxmoxApi.Journalctl)
|
||||||
proxmox.GET("/journalctl/:node/:vmid", proxmoxApi.Journalctl)
|
proxmox.GET("/journalctl/:node/:vmid", proxmoxApi.Journalctl)
|
||||||
|
|||||||
@@ -2109,8 +2109,12 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)",
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "Service names",
|
||||||
"name": "service",
|
"name": "service",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -2189,8 +2193,12 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)",
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "Service names",
|
||||||
"name": "service",
|
"name": "service",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -2276,8 +2284,12 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)",
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "Service names",
|
||||||
"name": "service",
|
"name": "service",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -2369,8 +2381,12 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)",
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "Service names",
|
||||||
"name": "service",
|
"name": "service",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -2388,8 +2404,12 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)",
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "Service names",
|
||||||
"name": "service",
|
"name": "service",
|
||||||
"in": "path"
|
"in": "path"
|
||||||
},
|
},
|
||||||
@@ -2715,6 +2735,91 @@
|
|||||||
"operationId": "vmStats"
|
"operationId": "vmStats"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/proxmox/tail": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get tail output for node or LXC container. If vmid is not provided, streams node tail.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"proxmox",
|
||||||
|
"websocket"
|
||||||
|
],
|
||||||
|
"summary": "Get tail output",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "File paths",
|
||||||
|
"name": "file",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maximum": 1000,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer",
|
||||||
|
"default": 100,
|
||||||
|
"description": "Limit output lines (1-1000)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node name",
|
||||||
|
"name": "node",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||||
|
"name": "vmid",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Tail 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": "tail",
|
||||||
|
"operationId": "tail"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/reload": {
|
"/reload": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Reload config",
|
"description": "Reload config",
|
||||||
@@ -4972,26 +5077,38 @@
|
|||||||
},
|
},
|
||||||
"ProxmoxNodeConfig": {
|
"ProxmoxNodeConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"node",
|
|
||||||
"vmid"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
"node": {
|
"node": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"service": {
|
"services": {
|
||||||
"type": "string"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"vmid": {
|
"vmid": {
|
||||||
|
"description": "unset: auto discover; explicit 0: node-level route; >0: lxc/qemu resource route",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"vmname": {
|
"vmname": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
|
|||||||
@@ -945,17 +945,22 @@ definitions:
|
|||||||
- ProviderTypeAgent
|
- ProviderTypeAgent
|
||||||
ProxmoxNodeConfig:
|
ProxmoxNodeConfig:
|
||||||
properties:
|
properties:
|
||||||
|
files:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
node:
|
node:
|
||||||
type: string
|
type: string
|
||||||
service:
|
services:
|
||||||
type: string
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
vmid:
|
vmid:
|
||||||
|
description: 'unset: auto discover; explicit 0: node-level route; >0: lxc/qemu
|
||||||
|
resource route'
|
||||||
type: integer
|
type: integer
|
||||||
vmname:
|
vmname:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- node
|
|
||||||
- vmid
|
|
||||||
type: object
|
type: object
|
||||||
ProxyStats:
|
ProxyStats:
|
||||||
properties:
|
properties:
|
||||||
@@ -3421,11 +3426,13 @@ paths:
|
|||||||
name: node
|
name: node
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: Service name (e.g., 'pveproxy' for node, 'container@.service'
|
- collectionFormat: csv
|
||||||
format for LXC)
|
description: Service names
|
||||||
in: query
|
in: query
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
name: service
|
name: service
|
||||||
type: string
|
type: array
|
||||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||||
in: query
|
in: query
|
||||||
name: vmid
|
name: vmid
|
||||||
@@ -3477,11 +3484,13 @@ paths:
|
|||||||
name: node
|
name: node
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: Service name (e.g., 'pveproxy' for node, 'container@.service'
|
- collectionFormat: csv
|
||||||
format for LXC)
|
description: Service names
|
||||||
in: query
|
in: query
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
name: service
|
name: service
|
||||||
type: string
|
type: array
|
||||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||||
in: query
|
in: query
|
||||||
name: vmid
|
name: vmid
|
||||||
@@ -3538,11 +3547,13 @@ paths:
|
|||||||
name: node
|
name: node
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: Service name (e.g., 'pveproxy' for node, 'container@.service'
|
- collectionFormat: csv
|
||||||
format for LXC)
|
description: Service names
|
||||||
in: query
|
in: query
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
name: service
|
name: service
|
||||||
type: string
|
type: array
|
||||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||||
in: query
|
in: query
|
||||||
name: vmid
|
name: vmid
|
||||||
@@ -3603,11 +3614,13 @@ paths:
|
|||||||
name: node
|
name: node
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: Service name (e.g., 'pveproxy' for node, 'container@.service'
|
- collectionFormat: csv
|
||||||
format for LXC)
|
description: Service names
|
||||||
in: query
|
in: query
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
name: service
|
name: service
|
||||||
type: string
|
type: array
|
||||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||||
in: query
|
in: query
|
||||||
name: vmid
|
name: vmid
|
||||||
@@ -3617,11 +3630,13 @@ paths:
|
|||||||
name: node
|
name: node
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: Service name (e.g., 'pveproxy' for node, 'container@.service'
|
- collectionFormat: csv
|
||||||
format for LXC)
|
description: Service names
|
||||||
in: path
|
in: path
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
name: service
|
name: service
|
||||||
type: string
|
type: array
|
||||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||||
in: path
|
in: path
|
||||||
name: vmid
|
name: vmid
|
||||||
@@ -3837,6 +3852,65 @@ paths:
|
|||||||
- proxmox
|
- proxmox
|
||||||
- websocket
|
- websocket
|
||||||
x-id: vmStats
|
x-id: vmStats
|
||||||
|
/proxmox/tail:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get tail output for node or LXC container. If vmid is not provided,
|
||||||
|
streams node tail.
|
||||||
|
parameters:
|
||||||
|
- collectionFormat: csv
|
||||||
|
description: File paths
|
||||||
|
in: query
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
name: file
|
||||||
|
required: true
|
||||||
|
type: array
|
||||||
|
- default: 100
|
||||||
|
description: Limit output lines (1-1000)
|
||||||
|
in: query
|
||||||
|
maximum: 1000
|
||||||
|
minimum: 1
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- description: Node name
|
||||||
|
in: query
|
||||||
|
name: node
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||||
|
in: query
|
||||||
|
name: vmid
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Tail 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 tail output
|
||||||
|
tags:
|
||||||
|
- proxmox
|
||||||
|
- websocket
|
||||||
|
x-id: tail
|
||||||
/reload:
|
/reload:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ import (
|
|||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl?node=pve&vmid=127&service=pveproxy&service=pvedaemon&limit=10
|
||||||
|
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
|
||||||
|
|
||||||
type JournalctlRequest struct {
|
type JournalctlRequest struct {
|
||||||
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
||||||
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||||
Service string `form:"service" uri:"service"` // Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)
|
Services []string `form:"service" uri:"service"` // Service names
|
||||||
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||||
} // @name ProxmoxJournalctlRequest
|
} // @name ProxmoxJournalctlRequest
|
||||||
|
|
||||||
// @x-id "journalctl"
|
// @x-id "journalctl"
|
||||||
@@ -56,9 +59,9 @@ func Journalctl(c *gin.Context) {
|
|||||||
var reader io.ReadCloser
|
var reader io.ReadCloser
|
||||||
var err error
|
var err error
|
||||||
if request.VMID == nil {
|
if request.VMID == nil {
|
||||||
reader, err = node.NodeJournalctl(c.Request.Context(), request.Service, *request.Limit)
|
reader, err = node.NodeJournalctl(c.Request.Context(), request.Services, *request.Limit)
|
||||||
} else {
|
} else {
|
||||||
reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Service, *request.Limit)
|
reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Services, *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"))
|
||||||
|
|||||||
77
internal/api/v1/proxmox/tail.go
Normal file
77
internal/api/v1/proxmox/tail.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
"github.com/yusing/goutils/apitypes"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// e.g. ws://localhost:8889/api/v1/proxmox/tail?node=pve&vmid=127&file=/var/log/immich/web.log&file=/var/log/immich/ml.log&limit=10
|
||||||
|
|
||||||
|
type TailRequest struct {
|
||||||
|
Node string `form:"node" binding:"required"` // Node name
|
||||||
|
VMID *int `form:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||||
|
Files []string `form:"file" binding:"required,dive,filepath"` // File paths
|
||||||
|
Limit int `form:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||||
|
} // @name ProxmoxTailRequest
|
||||||
|
|
||||||
|
// @x-id "tail"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Get tail output
|
||||||
|
// @Description Get tail output for node or LXC container. If vmid is not provided, streams node tail.
|
||||||
|
// @Tags proxmox,websocket
|
||||||
|
// @Accept json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param query query TailRequest true "Request"
|
||||||
|
// @Success 200 string plain "Tail output"
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
|
// @Router /proxmox/tail [get]
|
||||||
|
func Tail(c *gin.Context) {
|
||||||
|
var request TailRequest
|
||||||
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(request.Node)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusContinue)
|
||||||
|
|
||||||
|
var reader io.ReadCloser
|
||||||
|
var err error
|
||||||
|
if request.VMID == nil {
|
||||||
|
reader, err = node.NodeTail(c.Request.Context(), request.Files, request.Limit)
|
||||||
|
} else {
|
||||||
|
reader, err = node.LXCTail(c.Request.Context(), *request.VMID, request.Files, request.Limit)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
writer := manager.NewWriter(websocket.TextMessage)
|
||||||
|
_, err = io.Copy(writer, reader)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy journalctl output"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule internal/go-proxmox updated: bcae3065be...d28335df14
59
internal/proxmox/command_common.go
Normal file
59
internal/proxmox/command_common.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package proxmox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkValidInput checks if the input contains invalid characters.
|
||||||
|
//
|
||||||
|
// The characters are: & | $ ; ' " ` $( ${ < >
|
||||||
|
// These characters are used in the command line to escape the input or to expand variables.
|
||||||
|
// We need to check if the input contains these characters and return an error if it does.
|
||||||
|
// This is to prevent command injection.
|
||||||
|
func checkValidInput(input string) error {
|
||||||
|
if strings.ContainsAny(input, "&|$;'\"`<>") {
|
||||||
|
return fmt.Errorf("input contains invalid characters: %q", input)
|
||||||
|
}
|
||||||
|
if strings.Contains(input, "$(") {
|
||||||
|
return fmt.Errorf("input contains $(: %q", input)
|
||||||
|
}
|
||||||
|
if strings.Contains(input, "${") {
|
||||||
|
return fmt.Errorf("input contains ${: %q", input)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTail(files []string, limit int) (string, error) {
|
||||||
|
for _, file := range files {
|
||||||
|
if err := checkValidInput(file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var command strings.Builder
|
||||||
|
command.WriteString("tail -f -q --retry ")
|
||||||
|
for _, file := range files {
|
||||||
|
fmt.Fprintf(&command, " %q ", file)
|
||||||
|
}
|
||||||
|
if limit > 0 {
|
||||||
|
fmt.Fprintf(&command, " -n %d", limit)
|
||||||
|
}
|
||||||
|
return command.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatJournalctl(services []string, limit int) (string, error) {
|
||||||
|
for _, service := range services {
|
||||||
|
if err := checkValidInput(service); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var command strings.Builder
|
||||||
|
command.WriteString("journalctl -f")
|
||||||
|
for _, service := range services {
|
||||||
|
fmt.Fprintf(&command, " -u %q ", service)
|
||||||
|
}
|
||||||
|
if limit > 0 {
|
||||||
|
fmt.Fprintf(&command, " -n %d", limit)
|
||||||
|
}
|
||||||
|
return command.String(), nil
|
||||||
|
}
|
||||||
@@ -39,15 +39,23 @@ 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.
|
||||||
//
|
//
|
||||||
// If service is not empty, it will be used to filter the output by service.
|
// If services are 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.
|
// 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) {
|
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, services []string, limit int) (io.ReadCloser, error) {
|
||||||
command := "journalctl -f"
|
command, err := formatJournalctl(services, limit)
|
||||||
if service != "" {
|
if err != nil {
|
||||||
command = fmt.Sprintf("journalctl -u %q -f", service)
|
return nil, err
|
||||||
}
|
}
|
||||||
if limit > 0 {
|
return n.LXCCommand(ctx, vmid, command)
|
||||||
command = fmt.Sprintf("%s -n %d", command, limit)
|
}
|
||||||
|
|
||||||
|
// LXCTail streams tail output for the given file.
|
||||||
|
//
|
||||||
|
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||||
|
func (n *Node) LXCTail(ctx context.Context, vmid int, files []string, limit int) (io.ReadCloser, error) {
|
||||||
|
command, err := formatTail(files, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return n.LXCCommand(ctx, vmid, command)
|
return n.LXCCommand(ctx, vmid, command)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type NodeConfig struct {
|
|||||||
VMID *int `json:"vmid"` // unset: auto discover; explicit 0: node-level route; >0: lxc/qemu resource route
|
VMID *int `json:"vmid"` // unset: auto discover; explicit 0: node-level route; >0: lxc/qemu resource route
|
||||||
VMName string `json:"vmname,omitempty"`
|
VMName string `json:"vmname,omitempty"`
|
||||||
Services []string `json:"services,omitempty" aliases:"service"`
|
Services []string `json:"services,omitempty" aliases:"service"`
|
||||||
|
Files []string `json:"files,omitempty" aliases:"file"`
|
||||||
} // @name ProxmoxNodeConfig
|
} // @name ProxmoxNodeConfig
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
|
|||||||
@@ -120,13 +120,25 @@ func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser,
|
|||||||
return pr, nil
|
return pr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) NodeJournalctl(ctx context.Context, service string, limit int) (io.ReadCloser, error) {
|
// NodeJournalctl streams journalctl output for the given service.
|
||||||
command := "journalctl -f"
|
//
|
||||||
if service != "" {
|
// If services are not empty, it will be used to filter the output by services.
|
||||||
command = fmt.Sprintf("journalctl -u %q -f", service)
|
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||||
}
|
func (n *Node) NodeJournalctl(ctx context.Context, services []string, limit int) (io.ReadCloser, error) {
|
||||||
if limit > 0 {
|
command, err := formatJournalctl(services, limit)
|
||||||
command = fmt.Sprintf("%s -n %d", command, limit)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return n.NodeCommand(ctx, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeTail streams tail output for the given file.
|
||||||
|
//
|
||||||
|
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||||
|
func (n *Node) NodeTail(ctx context.Context, files []string, limit int) (io.ReadCloser, error) {
|
||||||
|
command, err := formatTail(files, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return n.NodeCommand(ctx, command)
|
return n.NodeCommand(ctx, command)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user