mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
996 lines
28 KiB
Go
996 lines
28 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/lxc/incus/v7/internal/filter"
|
|
"github.com/lxc/incus/v7/internal/server/auth"
|
|
"github.com/lxc/incus/v7/internal/server/db"
|
|
dbCluster "github.com/lxc/incus/v7/internal/server/db/cluster"
|
|
"github.com/lxc/incus/v7/internal/server/lifecycle"
|
|
"github.com/lxc/incus/v7/internal/server/project"
|
|
"github.com/lxc/incus/v7/internal/server/request"
|
|
"github.com/lxc/incus/v7/internal/server/response"
|
|
localUtil "github.com/lxc/incus/v7/internal/server/util"
|
|
"github.com/lxc/incus/v7/internal/version"
|
|
"github.com/lxc/incus/v7/shared/api"
|
|
"github.com/lxc/incus/v7/shared/logger"
|
|
"github.com/lxc/incus/v7/shared/validate"
|
|
)
|
|
|
|
var networkIntegrationsCmd = APIEndpoint{
|
|
Path: "network-integrations",
|
|
|
|
Get: APIEndpointAction{Handler: networkIntegrationsGet, AccessHandler: allowAuthenticated},
|
|
Post: APIEndpointAction{Handler: networkIntegrationsPost, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanCreateNetworkIntegrations)},
|
|
}
|
|
|
|
var networkIntegrationCmd = APIEndpoint{
|
|
Path: "network-integrations/{integration}",
|
|
|
|
Delete: APIEndpointAction{Handler: networkIntegrationDelete, AccessHandler: allowPermission(auth.ObjectTypeNetworkIntegration, auth.EntitlementCanEdit, "integration")},
|
|
Get: APIEndpointAction{Handler: networkIntegrationGet, AccessHandler: allowPermission(auth.ObjectTypeNetworkIntegration, auth.EntitlementCanView, "integration")},
|
|
Put: APIEndpointAction{Handler: networkIntegrationPut, AccessHandler: allowPermission(auth.ObjectTypeNetworkIntegration, auth.EntitlementCanEdit, "integration")},
|
|
Patch: APIEndpointAction{Handler: networkIntegrationPut, AccessHandler: allowPermission(auth.ObjectTypeNetworkIntegration, auth.EntitlementCanEdit, "integration")},
|
|
Post: APIEndpointAction{Handler: networkIntegrationPost, AccessHandler: allowPermission(auth.ObjectTypeNetworkIntegration, auth.EntitlementCanEdit, "integration")},
|
|
}
|
|
|
|
// API endpoints.
|
|
|
|
// swagger:operation GET /1.0/network-integrations network-integrations network_integrations_get
|
|
//
|
|
// Get the network integrations
|
|
//
|
|
// Returns a list of network integrations (URLs).
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: query
|
|
// name: filter
|
|
// description: Collection filter
|
|
// type: string
|
|
// example: default
|
|
// responses:
|
|
// "200":
|
|
// description: API endpoints
|
|
// schema:
|
|
// type: object
|
|
// description: Sync response
|
|
// properties:
|
|
// type:
|
|
// type: string
|
|
// description: Response type
|
|
// example: sync
|
|
// status:
|
|
// type: string
|
|
// description: Status description
|
|
// example: Success
|
|
// status_code:
|
|
// type: integer
|
|
// description: Status code
|
|
// example: 200
|
|
// metadata:
|
|
// type: array
|
|
// description: List of endpoints
|
|
// items:
|
|
// type: string
|
|
// example: |-
|
|
// [
|
|
// "/1.0/network-integrations/region2",
|
|
// "/1.0/network-integrations/region3"
|
|
// ]
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
|
|
// swagger:operation GET /1.0/network-integrations?recursion=1 network-integrations network_integrations_get_recursion1
|
|
//
|
|
// Get the network integrations
|
|
//
|
|
// Returns a list of network integrations (structs).
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: query
|
|
// name: filter
|
|
// description: Collection filter
|
|
// type: string
|
|
// example: default
|
|
// responses:
|
|
// "200":
|
|
// description: API endpoints
|
|
// schema:
|
|
// type: object
|
|
// description: Sync response
|
|
// properties:
|
|
// type:
|
|
// type: string
|
|
// description: Response type
|
|
// example: sync
|
|
// status:
|
|
// type: string
|
|
// description: Status description
|
|
// example: Success
|
|
// status_code:
|
|
// type: integer
|
|
// description: Status code
|
|
// example: 200
|
|
// metadata:
|
|
// type: array
|
|
// description: List of network integrations
|
|
// items:
|
|
// $ref: "#/definitions/NetworkIntegration"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
|
|
func networkIntegrationsGet(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
recursion := localUtil.IsRecursionRequest(r)
|
|
|
|
// Parse filter value.
|
|
filterStr := r.FormValue("filter")
|
|
clauses, err := filter.Parse(filterStr, filter.QueryOperatorSet())
|
|
if err != nil {
|
|
return response.BadRequest(fmt.Errorf("Invalid filter: %w", err))
|
|
}
|
|
|
|
mustLoadObjects := recursion || (clauses != nil && len(clauses.Clauses) > 0)
|
|
|
|
// Network integrations aren't project aware, we only load the per-project data to apply name restrictions.
|
|
projectName := request.ProjectParam(r)
|
|
|
|
// Get list of Network integrations.
|
|
linkResults := make([]string, 0)
|
|
fullResults := make([]api.NetworkIntegration, 0)
|
|
|
|
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
var err error
|
|
|
|
// Load the project.
|
|
dbProject, err := dbCluster.GetProject(ctx, tx.Tx(), projectName)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network restrictions from project %q: %w", projectName, err)
|
|
}
|
|
|
|
p, err := dbProject.ToAPI(ctx, tx.Tx())
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network restrictions from project %q: %w", projectName, err)
|
|
}
|
|
|
|
// Load the integrations.
|
|
integrations, err := dbCluster.GetNetworkIntegrations(ctx, tx.Tx())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, integration := range integrations {
|
|
// Filter for project restrictions.
|
|
if !project.NetworkIntegrationAllowed(p.Config, integration.Name) {
|
|
continue
|
|
}
|
|
|
|
if mustLoadObjects {
|
|
// Get the integration.
|
|
result, err := integration.ToAPI(r.Context(), tx.Tx())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the user should see the configuration.
|
|
err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectNetworkIntegration(result.Name), auth.EntitlementCanEdit)
|
|
if err != nil {
|
|
result.Config = map[string]string{}
|
|
}
|
|
|
|
// Add UsedBy field.
|
|
integrationID := integration.ID
|
|
allPeers, err := dbCluster.GetNetworkPeers(ctx, tx.Tx()) // Fetch all peers
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network peers: %w", err)
|
|
}
|
|
|
|
usedBy := []string{}
|
|
for _, peer := range allPeers {
|
|
if peer.TargetNetworkIntegrationID.Valid && peer.TargetNetworkIntegrationID.Int64 == int64(integrationID) {
|
|
// Fetch the network associated with the peer
|
|
networkName, networkProjectName, err := tx.GetNetworkNameAndProjectWithID(ctx, int(peer.NetworkID))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
_, network, _, err := tx.GetNetworkInAnyState(ctx, networkProjectName, networkName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Fetch the project associated with the network
|
|
project, err := dbCluster.GetProject(ctx, tx.Tx(), networkProjectName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Construct the URL
|
|
url := api.NewURL().Path(version.APIVersion, "networks", network.Name, "peers", peer.Name).Project(project.Name).String()
|
|
usedBy = append(usedBy, url)
|
|
}
|
|
}
|
|
// Assign the collected URLs (original 'err' check is implicitly handled by checks above)
|
|
result.UsedBy = usedBy
|
|
|
|
if clauses != nil && len(clauses.Clauses) > 0 {
|
|
match, err := filter.Match(*result, *clauses)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !match {
|
|
continue
|
|
}
|
|
}
|
|
|
|
fullResults = append(fullResults, *result)
|
|
}
|
|
|
|
linkResults = append(linkResults, api.NewURL().Path(version.APIVersion, "network-integrations", integration.Name).String())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return response.InternalError(err)
|
|
}
|
|
|
|
if !recursion {
|
|
return response.SyncResponse(true, linkResults)
|
|
}
|
|
|
|
return response.SyncResponse(true, fullResults)
|
|
}
|
|
|
|
// swagger:operation POST /1.0/network-integrations network-integrations network_integrations_post
|
|
//
|
|
// Add a network integration
|
|
//
|
|
// Creates a new network integration.
|
|
//
|
|
// ---
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: body
|
|
// name: integration
|
|
// description: integration
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/NetworkIntegrationsPost"
|
|
// responses:
|
|
// "201":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func networkIntegrationsPost(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
req := api.NetworkIntegrationsPost{}
|
|
|
|
// Parse the request into a record.
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
// Quick checks.
|
|
err = validate.IsAPIName(req.Name, false)
|
|
if err != nil {
|
|
return response.BadRequest(fmt.Errorf("Invalid network integration name: %w", err))
|
|
}
|
|
|
|
// Validate the config.
|
|
err = networkIntegrationValidate(req.Type, false, nil, req.Config)
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
// Convert the API type to DB type.
|
|
dbType := -1
|
|
for k, v := range dbCluster.NetworkIntegrationTypeNames {
|
|
if v == req.Type {
|
|
dbType = k
|
|
break
|
|
}
|
|
}
|
|
|
|
if dbType == -1 {
|
|
return response.BadRequest(fmt.Errorf("Unsupported integration type %q", req.Type))
|
|
}
|
|
|
|
// Create the DB record.
|
|
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
dbRecord := dbCluster.NetworkIntegration{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Type: dbType,
|
|
}
|
|
|
|
id, err := dbCluster.CreateNetworkIntegration(ctx, tx.Tx(), dbRecord)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = dbCluster.CreateNetworkIntegrationConfig(ctx, tx.Tx(), id, req.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Add the integration to the auth backend.
|
|
err = s.Authorizer.AddNetworkIntegration(r.Context(), req.Name)
|
|
if err != nil {
|
|
logger.Error("Failed to add network integration to authorizer", logger.Ctx{"name": req.Name, "error": err})
|
|
}
|
|
|
|
// Emit the lifecycle event.
|
|
lc := lifecycle.NetworkIntegrationCreated.Event(req.Name, request.CreateRequestor(r), nil)
|
|
s.Events.SendLifecycle(api.ProjectDefaultName, lc)
|
|
|
|
return response.SyncResponseLocation(true, nil, lc.Source)
|
|
}
|
|
|
|
// swagger:operation DELETE /1.0/network-integrations/{integration} network-integrations network_integration_delete
|
|
//
|
|
// Delete the network integration
|
|
//
|
|
// Removes the network integration.
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: integration
|
|
// description: Integration name
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func networkIntegrationDelete(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
// Get the integration name.
|
|
integrationName, err := pathVar(r, "integration")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Delete the DB record.
|
|
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Get UsedBy for the integration.
|
|
integrationID, err := dbCluster.GetNetworkIntegrationID(ctx, tx.Tx(), integrationName)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get network integration ID: %w", err)
|
|
}
|
|
|
|
allPeers, err := dbCluster.GetNetworkPeers(ctx, tx.Tx()) // Fetch all peers
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network peers: %w", err)
|
|
}
|
|
|
|
usedBy := []string{}
|
|
for _, peer := range allPeers {
|
|
if peer.TargetNetworkIntegrationID.Valid && peer.TargetNetworkIntegrationID.Int64 == int64(integrationID) {
|
|
// Fetch the network associated with the peer
|
|
networkName, networkProjectName, err := tx.GetNetworkNameAndProjectWithID(ctx, int(peer.NetworkID))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
_, network, _, err := tx.GetNetworkInAnyState(ctx, networkProjectName, networkName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Fetch the project associated with the network
|
|
project, err := dbCluster.GetProject(ctx, tx.Tx(), networkProjectName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Construct the URL
|
|
url := api.NewURL().Path(version.APIVersion, "networks", network.Name, "peers", peer.Name).Project(project.Name).String()
|
|
usedBy = append(usedBy, url)
|
|
}
|
|
}
|
|
|
|
if len(usedBy) > 0 {
|
|
return errors.New("Network integration is currently in use")
|
|
}
|
|
|
|
err = dbCluster.DeleteNetworkIntegration(ctx, tx.Tx(), integrationName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Delete the integration from the auth backend.
|
|
err = s.Authorizer.DeleteNetworkIntegration(r.Context(), integrationName)
|
|
if err != nil {
|
|
logger.Error("Failed to remove network integration from authorizer", logger.Ctx{"name": integrationName, "error": err})
|
|
}
|
|
|
|
// Emit the lifecycle event.
|
|
s.Events.SendLifecycle(api.ProjectDefaultName, lifecycle.NetworkIntegrationDeleted.Event(integrationName, request.CreateRequestor(r), nil))
|
|
|
|
return response.EmptySyncResponse
|
|
}
|
|
|
|
// swagger:operation GET /1.0/network-integrations/{integration} network-integrations network_integration_get
|
|
//
|
|
// Get the network integration
|
|
//
|
|
// Gets a specific network integration.
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: integration
|
|
// description: Integration name
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// description: integration
|
|
// schema:
|
|
// type: object
|
|
// description: Sync response
|
|
// properties:
|
|
// type:
|
|
// type: string
|
|
// description: Response type
|
|
// example: sync
|
|
// status:
|
|
// type: string
|
|
// description: Status description
|
|
// example: Success
|
|
// status_code:
|
|
// type: integer
|
|
// description: Status code
|
|
// example: 200
|
|
// metadata:
|
|
// $ref: "#/definitions/NetworkIntegration"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func networkIntegrationGet(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
// Get the integration name.
|
|
integrationName, err := pathVar(r, "integration")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Network integrations aren't project aware, we only load the per-project data to apply name restrictions.
|
|
projectName := request.ProjectParam(r)
|
|
|
|
// Get the integration.
|
|
var info *api.NetworkIntegration
|
|
|
|
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Get the project.
|
|
dbProject, err := dbCluster.GetProject(ctx, tx.Tx(), projectName)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network restrictions from project %q: %w", projectName, err)
|
|
}
|
|
|
|
p, err := dbProject.ToAPI(ctx, tx.Tx())
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network restrictions from project %q: %w", projectName, err)
|
|
}
|
|
|
|
// Filter for project restrictions.
|
|
if !project.NetworkIntegrationAllowed(p.Config, integrationName) {
|
|
return nil
|
|
}
|
|
|
|
// Get the integration.
|
|
dbRecord, err := dbCluster.GetNetworkIntegration(ctx, tx.Tx(), integrationName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
info, err = dbRecord.ToAPI(ctx, tx.Tx())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the user should see the configuration.
|
|
err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectNetworkIntegration(info.Name), auth.EntitlementCanEdit)
|
|
if err != nil {
|
|
info.Config = map[string]string{}
|
|
}
|
|
|
|
// Add UsedBy field.
|
|
integrationID := dbRecord.ID
|
|
allPeers, err := dbCluster.GetNetworkPeers(ctx, tx.Tx()) // Fetch all peers
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network peers: %w", err)
|
|
}
|
|
|
|
usedBy := []string{}
|
|
for _, peer := range allPeers {
|
|
if peer.TargetNetworkIntegrationID.Valid && peer.TargetNetworkIntegrationID.Int64 == int64(integrationID) {
|
|
// Fetch the network associated with the peer
|
|
networkName, networkProjectName, err := tx.GetNetworkNameAndProjectWithID(ctx, int(peer.NetworkID))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
_, network, _, err := tx.GetNetworkInAnyState(ctx, networkProjectName, networkName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Fetch the project associated with the network
|
|
project, err := dbCluster.GetProject(ctx, tx.Tx(), networkProjectName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Construct the URL
|
|
url := api.NewURL().Path(version.APIVersion, "networks", network.Name, "peers", peer.Name).Project(project.Name).String()
|
|
usedBy = append(usedBy, url)
|
|
}
|
|
}
|
|
// Assign the collected URLs (original 'err' check is implicitly handled by checks above)
|
|
info.UsedBy = usedBy
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
if info == nil {
|
|
return response.NotFound(nil)
|
|
}
|
|
|
|
return response.SyncResponseETag(true, info, info.Writable())
|
|
}
|
|
|
|
// swagger:operation PATCH /1.0/network-integrations/{integration} network-integrations network_integration_patch
|
|
//
|
|
// Partially update the network integration
|
|
//
|
|
// Updates a subset of the network integration configuration.
|
|
//
|
|
// ---
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: integration
|
|
// description: Integration name
|
|
// type: string
|
|
// required: true
|
|
// - in: body
|
|
// name: integration
|
|
// description: integration configuration
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/NetworkIntegrationPut"
|
|
// responses:
|
|
// "200":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "412":
|
|
// $ref: "#/responses/PreconditionFailed"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
|
|
// swagger:operation PUT /1.0/network-integrations/{integration} network-integrations network_integration_put
|
|
//
|
|
// Update the network integration
|
|
//
|
|
// Updates the entire network integration configuration.
|
|
//
|
|
// ---
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: integration
|
|
// description: Integration name
|
|
// type: string
|
|
// required: true
|
|
// - in: body
|
|
// name: integration
|
|
// description: integration configuration
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/NetworkIntegrationPut"
|
|
// responses:
|
|
// "200":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "412":
|
|
// $ref: "#/responses/PreconditionFailed"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func networkIntegrationPut(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
integrationName, err := pathVar(r, "integration")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Get the existing network integration.
|
|
var dbRecord *dbCluster.NetworkIntegration
|
|
var info *api.NetworkIntegration
|
|
var usedBy []string
|
|
|
|
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
dbRecord, err = dbCluster.GetNetworkIntegration(ctx, tx.Tx(), integrationName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
info, err = dbRecord.ToAPI(ctx, tx.Tx())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
integrationID := dbRecord.ID
|
|
allPeers, err := dbCluster.GetNetworkPeers(ctx, tx.Tx()) // Fetch all peers
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load network peers: %w", err)
|
|
}
|
|
|
|
// Build usedBy slice in two passes to avoid SA4010.
|
|
count := 0
|
|
for _, peer := range allPeers {
|
|
if peer.TargetNetworkIntegrationID.Valid && peer.TargetNetworkIntegrationID.Int64 == int64(integrationID) {
|
|
count++
|
|
}
|
|
}
|
|
usedBy = make([]string, count)
|
|
idx := 0
|
|
for _, peer := range allPeers {
|
|
if peer.TargetNetworkIntegrationID.Valid && peer.TargetNetworkIntegrationID.Int64 == int64(integrationID) {
|
|
// Fetch the network associated with the peer
|
|
networkName, networkProjectName, err := tx.GetNetworkNameAndProjectWithID(ctx, int(peer.NetworkID))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
_, network, _, err := tx.GetNetworkInAnyState(ctx, networkProjectName, networkName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Fetch the project associated with the network
|
|
project, err := dbCluster.GetProject(ctx, tx.Tx(), networkProjectName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Construct the URL
|
|
usedBy[idx] = api.NewURL().Path(version.APIVersion, "networks", network.Name, "peers", peer.Name).Project(project.Name).String()
|
|
idx++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Validate the ETag.
|
|
err = localUtil.EtagCheck(r, info.Writable())
|
|
if err != nil {
|
|
return response.PreconditionFailed(err)
|
|
}
|
|
|
|
// Decode the request.
|
|
req := api.NetworkIntegrationPut{}
|
|
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
if r.Method == http.MethodPatch {
|
|
// If config being updated via "patch" method, then merge all existing config with the keys that
|
|
// are present in the request config.
|
|
for k, v := range info.Config {
|
|
_, ok := req.Config[k]
|
|
if !ok {
|
|
req.Config[k] = v
|
|
}
|
|
}
|
|
|
|
if req.Description == "" {
|
|
req.Description = info.Description
|
|
}
|
|
}
|
|
|
|
// Validate the resulting config.
|
|
err = networkIntegrationValidate(info.Type, len(usedBy) > 0, info.Config, req.Config)
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
// Update the database record.
|
|
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Update the description if needed.
|
|
if dbRecord.Description != req.Description {
|
|
dbRecord.Description = req.Description
|
|
err := dbCluster.UpdateNetworkIntegration(ctx, tx.Tx(), integrationName, *dbRecord)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update the configuration.
|
|
err := dbCluster.UpdateNetworkIntegrationConfig(ctx, tx.Tx(), int64(dbRecord.ID), req.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Emit the lifecycle event.
|
|
s.Events.SendLifecycle(api.ProjectDefaultName, lifecycle.NetworkIntegrationUpdated.Event(integrationName, request.CreateRequestor(r), nil))
|
|
|
|
return response.EmptySyncResponse
|
|
}
|
|
|
|
// swagger:operation POST /1.0/network-integrations/{integration} network-integrations network_integration_post
|
|
//
|
|
// Rename the network integration
|
|
//
|
|
// Renames the network integration.
|
|
//
|
|
// ---
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - in: path
|
|
// name: integration
|
|
// description: Integration name
|
|
// type: string
|
|
// required: true
|
|
// - in: body
|
|
// name: integration
|
|
// description: integration configuration
|
|
// required: true
|
|
// schema:
|
|
// $ref: "#/definitions/NetworkIntegrationPost"
|
|
// responses:
|
|
// "201":
|
|
// $ref: "#/responses/EmptySyncResponse"
|
|
// "400":
|
|
// $ref: "#/responses/BadRequest"
|
|
// "403":
|
|
// $ref: "#/responses/Forbidden"
|
|
// "404":
|
|
// $ref: "#/responses/NotFound"
|
|
// "409":
|
|
// $ref: "#/responses/Conflict"
|
|
// "500":
|
|
// $ref: "#/responses/InternalServerError"
|
|
func networkIntegrationPost(d *Daemon, r *http.Request) response.Response {
|
|
s := d.State()
|
|
|
|
// Get the integration name.
|
|
integrationName, err := pathVar(r, "integration")
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Decode the request.
|
|
req := api.NetworkIntegrationPost{}
|
|
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
return response.BadRequest(err)
|
|
}
|
|
|
|
// Quick checks.
|
|
err = validate.IsAPIName(req.Name, false)
|
|
if err != nil {
|
|
return response.BadRequest(fmt.Errorf("Invalid network integration name: %w", err))
|
|
}
|
|
|
|
// Rename the DB record.
|
|
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
err := dbCluster.RenameNetworkIntegration(ctx, tx.Tx(), integrationName, req.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return response.SmartError(err)
|
|
}
|
|
|
|
// Rename the integration in the auth backend.
|
|
err = s.Authorizer.RenameNetworkIntegration(r.Context(), integrationName, req.Name)
|
|
if err != nil {
|
|
logger.Error("Failed to remove network integration from authorizer", logger.Ctx{"name": integrationName, "error": err})
|
|
}
|
|
|
|
// Emit the lifecycle event.
|
|
lc := lifecycle.NetworkIntegrationRenamed.Event(req.Name, request.CreateRequestor(r), logger.Ctx{"old_name": integrationName})
|
|
s.Events.SendLifecycle(api.ProjectDefaultName, lc)
|
|
|
|
return response.SyncResponseLocation(true, nil, lc.Source)
|
|
}
|
|
|
|
// networkIntegrationValidate validates the configuration keys/values for network integration.
|
|
func networkIntegrationValidate(integrationType string, inUse bool, oldConfig map[string]string, config map[string]string) error {
|
|
if integrationType != "ovn" {
|
|
return fmt.Errorf("Invalid integration type %q", integrationType)
|
|
}
|
|
|
|
configKeys := map[string]func(value string) error{
|
|
// gendoc:generate(entity=network_integration, group=ovn, key=ovn.northbound_connection)
|
|
//
|
|
// ---
|
|
// type: string
|
|
// scope: global
|
|
// shortdesc: OVN northbound inter-connection connection string
|
|
"ovn.northbound_connection": validate.IsAny,
|
|
|
|
// gendoc:generate(entity=network_integration, group=ovn, key=ovn.southbound_connection)
|
|
//
|
|
// ---
|
|
// type: string
|
|
// scope: global
|
|
// shortdesc: OVN southbound inter-connection connection string
|
|
"ovn.southbound_connection": validate.IsAny,
|
|
|
|
// gendoc:generate(entity=network_integration, group=ovn, key=ovn.ca_cert)
|
|
//
|
|
// ---
|
|
// type: string
|
|
// scope: global
|
|
// shortdesc: OVN SSL certificate authority for the inter-connection database
|
|
"ovn.ca_cert": validate.Optional(validate.IsAny),
|
|
|
|
// gendoc:generate(entity=network_integration, group=ovn, key=ovn.client_cert)
|
|
//
|
|
// ---
|
|
// type: string
|
|
// scope: global
|
|
// shortdesc: OVN SSL client certificate
|
|
"ovn.client_cert": validate.Optional(validate.IsAny),
|
|
|
|
// gendoc:generate(entity=network_integration, group=ovn, key=ovn.client_key)
|
|
//
|
|
// ---
|
|
// type: string
|
|
// scope: global
|
|
// shortdesc: OVN SSL client key
|
|
"ovn.client_key": validate.Optional(validate.IsAny),
|
|
|
|
// gendoc:generate(entity=network_integration, group=ovn, key=ovn.transit.pattern)
|
|
// Specify a Pongo2 template string that represents the transit switch name.
|
|
// This template gets access to the project name (`projectName`),
|
|
// integration name (`integrationName`), network name (`networkName`)
|
|
// and peer name (`peerName`).
|
|
//
|
|
// ---
|
|
// type: string
|
|
// defaultdesc: `ts-incus-{{ integrationName }}-{{ projectName }}-{{ networkName }}`
|
|
// shortdesc: Template for the transit switch name
|
|
"ovn.transit.pattern": validate.IsAny,
|
|
}
|
|
|
|
for k, v := range config {
|
|
// User keys are free for all.
|
|
|
|
// gendoc:generate(entity=network_integration, group=common, key=user.*)
|
|
// User keys can be used in search.
|
|
// ---
|
|
// type: string
|
|
// shortdesc: Free form user key/value storage
|
|
if strings.HasPrefix(k, "user.") {
|
|
continue
|
|
}
|
|
|
|
validator, ok := configKeys[k]
|
|
if !ok {
|
|
return fmt.Errorf("Invalid network integration configuration key %q", k)
|
|
}
|
|
|
|
err := validator(v)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid network integration configuration key %q value", k)
|
|
}
|
|
}
|
|
|
|
if oldConfig != nil && oldConfig["ovn.transit.pattern"] != config["ovn.transit.pattern"] && inUse {
|
|
return errors.New("The OVN transit switch pattern cannot be changed while the integration is in use")
|
|
}
|
|
|
|
return nil
|
|
}
|