Add a new backend store that enables iSCSI targets backed by S3-compatible object storage (AWS S3, MinIO, Ceph RGW, etc.). The implementation uses a chunked storage strategy where the virtual block device is divided into fixed-size chunks (default 4 MiB), each stored as an independent S3 object. This enables efficient random read/write access on top of object storage. Key features: - Chunked storage with configurable chunk size - Sparse device support (unwritten chunks treated as zeros) - Concurrent multi-chunk I/O via errgroup - Per-chunk locking for safe read-modify-write - AWS SDK v2 with default credential chain - In-process gofakes3 test server (no Docker needed) - 12 unit tests + 2 integration tests Also updates CI workflow to run S3 backend tests and updates README with S3 backend documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
231 lines
6.1 KiB
Go
231 lines
6.1 KiB
Go
/*
|
|
Copyright 2016 The GoStor Authors All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package config contains how to get/save config parameters from file.
|
|
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/mitchellh/go-homedir"
|
|
)
|
|
|
|
/*
|
|
Format of configuration file
|
|
|
|
{
|
|
"storages": [
|
|
{
|
|
"deviceID": integer, uniqu device id,
|
|
"path": string, <protocol>:<absolute/file/path>",
|
|
"online": bool, online/offline
|
|
}
|
|
],
|
|
|
|
"portals": [
|
|
{
|
|
"id": integer, uniqu portal id
|
|
"portal":string, <IP>:<PORT>
|
|
}
|
|
],
|
|
|
|
"targets": {
|
|
<target name >: {
|
|
"tpgts":{
|
|
<tpgt number>: [<portal id[,portal id....]]
|
|
}
|
|
//n shoud be an value from 0 ~ 65535
|
|
|
|
"luns": {
|
|
<lu number for the target>: <mappingd with the device ID>
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Example of the configuration file
|
|
|
|
{
|
|
"storages": [
|
|
{
|
|
"deviceID": 1000,
|
|
"path": "file:/tmp/disk.img",
|
|
"online": true
|
|
}
|
|
],
|
|
"iscsiportals":[
|
|
{
|
|
"id":0,
|
|
"portal":"192.168.159.1:3260"
|
|
}
|
|
],
|
|
"iscsitargets": {
|
|
"iqn.2016-09.com.gotgt.gostor:example_tgt_0": {
|
|
"tpgts": {
|
|
"1":[0]
|
|
}
|
|
,
|
|
"luns": {
|
|
"1": 1000
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
*/
|
|
|
|
const (
|
|
// ConfigFileName is the name of config file
|
|
ConfigFileName = "config.json"
|
|
)
|
|
|
|
var (
|
|
configDir = os.Getenv("GOSTOR_CONFIG")
|
|
config *Config
|
|
)
|
|
|
|
type BackendStorage struct {
|
|
DeviceID uint64 `json:"deviceID"`
|
|
Path string `json:"path"`
|
|
Online bool `json:"online"`
|
|
ThinProvisioning bool `json:"thinProvisioning"`
|
|
BlockShift uint `json:"blockShift"`
|
|
// BackendType specifies the backend storage type (file, iouring, etc.)
|
|
BackendType string `json:"backendType,omitempty"`
|
|
// EnableNUMA enables NUMA-aware memory allocation for this storage
|
|
EnableNUMA bool `json:"enableNUMA,omitempty"`
|
|
// NumaNode specifies the preferred NUMA node for this storage (-1 for auto)
|
|
NumaNode int `json:"numaNode,omitempty"`
|
|
// IoUringQueueDepth specifies the io_uring queue depth (0 for default)
|
|
IoUringQueueDepth uint32 `json:"ioUringQueueDepth,omitempty"`
|
|
// DeviceSize specifies the virtual device size in bytes (used by S3 backend)
|
|
DeviceSize uint64 `json:"deviceSize,omitempty"`
|
|
// S3ChunkSize specifies the chunk size in bytes for S3 backend (default 4MiB)
|
|
S3ChunkSize int64 `json:"s3ChunkSize,omitempty"`
|
|
// S3Endpoint specifies a custom S3 endpoint URL (for MinIO, etc.)
|
|
S3Endpoint string `json:"s3Endpoint,omitempty"`
|
|
// S3Region specifies the AWS region
|
|
S3Region string `json:"s3Region,omitempty"`
|
|
// S3ForcePathStyle uses path-style addressing (required for MinIO)
|
|
S3ForcePathStyle bool `json:"s3ForcePathStyle,omitempty"`
|
|
}
|
|
|
|
type ISCSIPortalInfo struct {
|
|
ID uint16 `json:"id"`
|
|
Portal string `json:"portal"`
|
|
}
|
|
|
|
type ISCSITarget struct {
|
|
TPGTs map[string][]uint64 `json:"tpgts"`
|
|
LUNs map[string]uint64 `json:"luns"`
|
|
}
|
|
|
|
type PerformanceConfig struct {
|
|
// EnableNUMA enables NUMA-aware memory allocation
|
|
EnableNUMA bool `json:"enableNUMA,omitempty"`
|
|
// EnableIoUring enables io_uring backend storage support (Linux 5.1+)
|
|
EnableIoUring bool `json:"enableIoUring,omitempty"`
|
|
// IoUringQueueDepth sets the io_uring queue depth
|
|
IoUringQueueDepth uint32 `json:"ioUringQueueDepth,omitempty"`
|
|
// NUMABufferPoolSize sets the number of buffers per NUMA node
|
|
NUMABufferPoolSize int `json:"numaBufferPoolSize,omitempty"`
|
|
// NUMABufferSize sets the size of NUMA-local buffers
|
|
NUMABufferSize int `json:"numaBufferSize,omitempty"`
|
|
}
|
|
|
|
type Config struct {
|
|
Storages []BackendStorage `json:"storages"`
|
|
ISCSIPortals []ISCSIPortalInfo `json:"iscsiportals"`
|
|
ISCSITargets map[string]ISCSITarget `json:"iscsitargets"`
|
|
// Performance settings
|
|
Performance PerformanceConfig `json:"performance,omitempty"`
|
|
}
|
|
|
|
func init() {
|
|
if configDir == "" {
|
|
homeDir, _ := homedir.Dir()
|
|
configDir = filepath.Join(homeDir, ".gotgt")
|
|
}
|
|
}
|
|
|
|
// ConfigDir returns the directory the configuration file is stored in
|
|
func ConfigDir() string {
|
|
return configDir
|
|
|
|
}
|
|
func GetConfig() *Config {
|
|
return config
|
|
}
|
|
|
|
// Load reads the configuration files in the given directory and return values.
|
|
func Load(configDir string) (*Config, error) {
|
|
if configDir == "" {
|
|
configDir = ConfigDir()
|
|
}
|
|
|
|
filename := filepath.Join(configDir, ConfigFileName)
|
|
config = &Config{
|
|
ISCSITargets: make(map[string]ISCSITarget),
|
|
}
|
|
|
|
// Try happy path first - latest config file
|
|
if _, err := os.Stat(filename); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return config, nil
|
|
}
|
|
// if file is there but we can't stat it for any reason other
|
|
// than it doesn't exist then stop
|
|
return config, fmt.Errorf("%s - %v", filename, err)
|
|
}
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return config, fmt.Errorf("%s - %v", filename, err)
|
|
}
|
|
defer file.Close()
|
|
if err = json.NewDecoder(file).Decode(config); err != nil {
|
|
return config, err
|
|
}
|
|
if err != nil {
|
|
err = fmt.Errorf("%s - %v", filename, err)
|
|
}
|
|
return config, err
|
|
}
|
|
|
|
// Save encodes and writes out all the authorization information
|
|
func (config *Config) Save(filename string) error {
|
|
if filename == "" {
|
|
return fmt.Errorf("Can't save config with empty filename")
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
|
return err
|
|
}
|
|
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
data, err := json.MarshalIndent(config, "", "\t")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = f.Write(data)
|
|
return err
|
|
}
|