Skip to content

Commit 4ee508b

Browse files
authored
Merge pull request #2067 from DinglyCoder/allow-s3-backups
Allow for direct S3 push of instance and custom volume backups
2 parents 5efa7d3 + 165a112 commit 4ee508b

File tree

10 files changed

+205
-2
lines changed

10 files changed

+205
-2
lines changed

cmd/incusd/instance_backup.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,31 @@ func instanceBackupsPost(d *Daemon, r *http.Request) response.Response {
320320
CompressionAlgorithm: req.CompressionAlgorithm,
321321
}
322322

323+
// Create the backup.
323324
err := backupCreate(s, args, inst, op)
324325
if err != nil {
325-
return fmt.Errorf("Create backup: %w", err)
326+
return err
327+
}
328+
329+
// Upload it if requested.
330+
if req.Target != nil {
331+
// Load the backup.
332+
entry, err := instance.BackupLoadByName(s, projectName, fullName)
333+
if err != nil {
334+
return err
335+
}
336+
337+
// Upload it.
338+
err = entry.Upload(req.Target)
339+
if err != nil {
340+
return err
341+
}
342+
343+
// Delete the backup on successful upload.
344+
err = entry.Delete()
345+
if err != nil {
346+
return err
347+
}
326348
}
327349

328350
return nil

cmd/incusd/storage_volumes_backup.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,31 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response
443443
CompressionAlgorithm: req.CompressionAlgorithm,
444444
}
445445

446+
// Create the backup.
446447
err := volumeBackupCreate(s, args, projectName, poolName, volumeName)
447448
if err != nil {
448-
return fmt.Errorf("Create volume backup: %w", err)
449+
return err
450+
}
451+
452+
// Upload it if requested.
453+
if req.Target != nil {
454+
// Load the backup.
455+
entry, err := storagePoolVolumeBackupLoadByName(r.Context(), s, projectName, poolName, volumeName)
456+
if err != nil {
457+
return err
458+
}
459+
460+
// Upload it.
461+
err = entry.Upload(req.Target)
462+
if err != nil {
463+
return err
464+
}
465+
466+
// Delete the backup on successful upload.
467+
err = entry.Delete()
468+
if err != nil {
469+
return err
470+
}
449471
}
450472

451473
s.Events.SendLifecycle(projectName, lifecycle.StorageVolumeBackupCreated.Event(poolName, volumeTypeName, args.Name, projectName, op.Requestor(), logger.Ctx{"type": volumeTypeName}))

doc/api-extensions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2821,3 +2821,7 @@ network forward.
28212821
## `network_physical_gateway_hwaddr`
28222822

28232823
Allows setting the MAC address of the IPv4 and IPv6 gateways when used with OVN.
2824+
2825+
## `backup_s3_upload`
2826+
2827+
Adds support for immediately uploading instance or volume backups to an S3 compatible endpoint.

doc/rest-api.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,41 @@ definitions:
2525
title: AccessEntry represents an entity having access to the resource.
2626
type: object
2727
x-go-package: github.com/lxc/incus/v6/shared/api
28+
BackupTarget:
29+
properties:
30+
access_key:
31+
description: AccessKey is the S3 API access key
32+
example: GOOG1234
33+
type: string
34+
x-go-name: AccessKey
35+
bucket_name:
36+
description: BucketName is the name of the S3 bucket.
37+
example: my_bucket
38+
type: string
39+
x-go-name: BucketName
40+
path:
41+
description: Path is the target path.
42+
example: foo/test.tar
43+
type: string
44+
x-go-name: Path
45+
protocol:
46+
description: Protocol is the upload protocol.
47+
example: S3
48+
type: string
49+
x-go-name: Protocol
50+
secret_key:
51+
description: SecretKey is the S3 API access key
52+
example: secret123
53+
type: string
54+
x-go-name: SecretKey
55+
url:
56+
description: URL is the HTTPS URL for the backup
57+
example: https://storage.googleapis.com
58+
type: string
59+
x-go-name: URL
60+
title: BackupTarget represents the target storage server for an instance or volume backup.
61+
type: object
62+
x-go-package: github.com/lxc/incus/v6/shared/api
2863
Certificate:
2964
description: Certificate represents a certificate
3065
properties:
@@ -1619,6 +1654,8 @@ definitions:
16191654
example: true
16201655
type: boolean
16211656
x-go-name: OptimizedStorage
1657+
target:
1658+
$ref: '#/definitions/BackupTarget'
16221659
title: InstanceBackupsPost represents the fields available for a new instance backup.
16231660
type: object
16241661
x-go-package: github.com/lxc/incus/v6/shared/api
@@ -6550,6 +6587,8 @@ definitions:
65506587
example: true
65516588
type: boolean
65526589
x-go-name: OptimizedStorage
6590+
target:
6591+
$ref: '#/definitions/BackupTarget'
65536592
volume_only:
65546593
description: Whether to ignore snapshots
65556594
example: false

internal/server/backup/backup_common.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
package backup
22

33
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
"os"
410
"time"
511

12+
"github.com/minio/minio-go/v7"
13+
"github.com/minio/minio-go/v7/pkg/credentials"
14+
615
"github.com/lxc/incus/v6/internal/server/state"
16+
"github.com/lxc/incus/v6/shared/api"
717
)
818

919
// WorkingDirPrefix is used when temporary working directories are needed.
@@ -40,3 +50,53 @@ func (b *CommonBackup) SetCompressionAlgorithm(compression string) {
4050
func (b *CommonBackup) OptimizedStorage() bool {
4151
return b.optimizedStorage
4252
}
53+
54+
// upload handles backup uploads.
55+
func (b *CommonBackup) upload(filePath string, req *api.BackupTarget) error {
56+
if req.Protocol != "s3" {
57+
return fmt.Errorf("Unsupported backup target protocol %q", req.Protocol)
58+
}
59+
60+
// Set up an S3 client.
61+
uri, err := url.Parse(req.URL)
62+
if err != nil {
63+
return err
64+
}
65+
66+
creds := credentials.NewStaticV4(req.AccessKey, req.SecretKey, "")
67+
68+
ts := &http.Transport{
69+
MaxIdleConns: 10,
70+
IdleConnTimeout: 30 * time.Second,
71+
DisableCompression: true,
72+
TLSClientConfig: &tls.Config{
73+
InsecureSkipVerify: true,
74+
MinVersion: tls.VersionTLS12,
75+
},
76+
}
77+
78+
client, err := minio.New(uri.Host, &minio.Options{
79+
BucketLookup: minio.BucketLookupPath,
80+
Creds: creds,
81+
Secure: uri.Scheme == "https",
82+
Transport: ts,
83+
})
84+
if err != nil {
85+
return err
86+
}
87+
88+
// Upload the object.
89+
tr, err := os.Open(filePath)
90+
if err != nil {
91+
return err
92+
}
93+
94+
defer tr.Close()
95+
96+
_, err = client.PutObject(context.Background(), req.BucketName, req.Path, tr, -1, minio.PutObjectOptions{})
97+
if err != nil {
98+
return err
99+
}
100+
101+
return nil
102+
}

internal/server/backup/backup_instance.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,9 @@ func (b *InstanceBackup) Render() *api.InstanceBackup {
153153
OptimizedStorage: b.optimizedStorage,
154154
}
155155
}
156+
157+
// Upload pushes the backup to external storage.
158+
func (b *InstanceBackup) Upload(req *api.BackupTarget) error {
159+
backupPath := internalUtil.VarPath("backups", "instances", project.Instance(b.instance.Project().Name, b.name))
160+
return b.upload(backupPath, req)
161+
}

internal/server/backup/backup_volume.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,9 @@ func (b *VolumeBackup) Render() *api.StorageVolumeBackup {
148148
OptimizedStorage: b.optimizedStorage,
149149
}
150150
}
151+
152+
// Upload pushes the backup to external storage.
153+
func (b *VolumeBackup) Upload(req *api.BackupTarget) error {
154+
backupPath := internalUtil.VarPath("backups", "custom", b.poolName, project.StorageVolume(b.projectName, b.name))
155+
return b.upload(backupPath, req)
156+
}

internal/version/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ var APIExtensions = []string{
486486
"custom_volume_sftp",
487487
"network_ovn_external_nic_address",
488488
"network_physical_gateway_hwaddr",
489+
"backup_s3_upload",
489490
}
490491

491492
// APIExtensionsCount returns the number of available API extensions.

shared/api/instance_backup.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@ import (
44
"time"
55
)
66

7+
// BackupTarget represents the target storage server for an instance or volume backup.
8+
//
9+
// swagger:model
10+
//
11+
// API extension: backup_s3_upload.
12+
type BackupTarget struct {
13+
// Protocol is the upload protocol.
14+
// Example: S3
15+
Protocol string `json:"protocol" yaml:"protocol"`
16+
17+
// URL is the HTTPS URL for the backup
18+
// Example: https://storage.googleapis.com
19+
URL string `json:"url" yaml:"url"`
20+
21+
// BucketName is the name of the S3 bucket.
22+
// Example: my_bucket
23+
BucketName string `json:"bucket_name" yaml:"bucket_name"`
24+
25+
// Path is the target path.
26+
// Example: foo/test.tar
27+
Path string `json:"path" yaml:"path"`
28+
29+
// AccessKey is the S3 API access key
30+
// Example: GOOG1234
31+
AccessKey string `json:"access_key" yaml:"access_key"`
32+
33+
// SecretKey is the S3 API access key
34+
// Example: secret123
35+
SecretKey string `json:"secret_key" yaml:"secret_key"`
36+
}
37+
738
// InstanceBackupsPost represents the fields available for a new instance backup.
839
//
940
// swagger:model
@@ -31,6 +62,12 @@ type InstanceBackupsPost struct {
3162
//
3263
// API extension: backup_compression_algorithm
3364
CompressionAlgorithm string `json:"compression_algorithm" yaml:"compression_algorithm"`
65+
66+
// External upload target
67+
// The backup will be uploaded and then deleted from local storage.
68+
//
69+
// API extension: backup_s3_upload
70+
Target *BackupTarget `json:"target" yaml:"target"`
3471
}
3572

3673
// InstanceBackup represents an instance backup.

shared/api/storage_pool_volume_backup.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ type StorageVolumeBackupsPost struct {
5656
// What compression algorithm to use
5757
// Example: gzip
5858
CompressionAlgorithm string `json:"compression_algorithm" yaml:"compression_algorithm"`
59+
60+
// External upload target
61+
// The backup will be uploaded and then deleted from local storage.
62+
//
63+
// API extension: backup_s3_upload
64+
Target *BackupTarget `json:"target" yaml:"target"`
5965
}
6066

6167
// StorageVolumeBackupPost represents the fields available for the renaming of a volume backup

0 commit comments

Comments
 (0)