mirror of
https://github.com/pgsty/minio.git
synced 2026-08-05 15:12:01 +03:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b18c0478e7 | |||
| 2d9860e875 | |||
| d3553f8dfc | |||
| e1ae90c12b | |||
| 34e7259f95 | |||
| d0015b4d66 | |||
| 3467460456 | |||
| 64b5701971 | |||
| fad59da29d | |||
| 91c839ad28 | |||
| 2786055df4 | |||
| 0a28c28a8c | |||
| 526546d588 | |||
| 2053b3414f | |||
| ce870466ff | |||
| 964e354d06 | |||
| 8ee8ad777c | |||
| 82af0be1aa | |||
| e8c18bc145 | |||
| bd25f31100 | |||
| ee7dcc2903 | |||
| 55ef51a99d | |||
| dc2348daa5 | |||
| 042d7f25e4 | |||
| 8c1b649b2d | |||
| f03ccec912 | |||
| 0bb65f84bb | |||
| 8e0910ab3e | |||
| 5353edcc38 | |||
| 3265112d04 | |||
| 4fdacb8b14 | |||
| 267f183fc8 | |||
| 3d22a9d84f | |||
| 51ec61ee94 | |||
| 74c2048ea9 | |||
| 730ac5381c | |||
| 6dd8a83c5a | |||
| 1a7e6d4768 | |||
| 98c950aacd | |||
| 94c52e3816 | |||
| 8766c5eb22 | |||
| e0d22359e7 | |||
| 6dd13e68c2 | |||
| 633001c8ba | |||
| e23a42305c | |||
| 63d2583e91 | |||
| a2f66abbe8 | |||
| b28661b673 | |||
| e8791ae274 | |||
| 309975d477 | |||
| 6571641735 | |||
| 8757c963ba | |||
| 9a71f2fdfa | |||
| 9c26fe47b0 | |||
| f3f47d8cd3 | |||
| de1d39e436 |
@@ -23,6 +23,8 @@ matrix:
|
||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||
- for d in $(go list ./... | grep -v browser); do CGO_ENABLED=1 go test -v -race --timeout 15m "$d"; done
|
||||
- make verifiers
|
||||
- make crosscompile
|
||||
- make verify
|
||||
- make coverage
|
||||
- cd browser && yarn && yarn test && cd ..
|
||||
|
||||
+1
-1
@@ -68,4 +68,4 @@ To remove a dependency
|
||||
- Run `make pkg-remove PKG=foo/bar` from top-level directory
|
||||
|
||||
### What are the coding guidelines for Minio?
|
||||
``Minio`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](slack.minio.io).
|
||||
``Minio`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](https://slack.minio.io).
|
||||
|
||||
@@ -19,6 +19,9 @@ getdeps:
|
||||
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
|
||||
|
||||
crosscompile:
|
||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||
|
||||
verifiers: getdeps vet fmt lint cyclo deadcode spelling
|
||||
|
||||
vet:
|
||||
|
||||
@@ -53,6 +53,15 @@ chmod +x minio
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|ppc64le|https://dl.minio.io/server/minio/release/linux-ppc64le/minio |
|
||||
```sh
|
||||
wget https://dl.minio.io/server/minio/release/linux-ppc64le/minio
|
||||
chmod +x minio
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
## Microsoft Windows
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Minio Browser</title>
|
||||
<link rel="stylesheet" href="loader.css" type="text/css">
|
||||
<link rel="stylesheet" href="/minio/loader.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-load">
|
||||
<div class="pl-inner">
|
||||
<img src="logo.svg" alt="">
|
||||
<img src="/minio/logo.svg" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
@@ -51,6 +51,6 @@
|
||||
<![endif]-->
|
||||
|
||||
<script>currentUiVersion = 'MINIO_UI_VERSION'</script>
|
||||
<script src="index_bundle.js"></script>
|
||||
<script src="/minio/index_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+35
-24
File diff suppressed because one or more lines are too long
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Enable tracing if set.
|
||||
[ -n "$BASH_XTRACEFD" ] && set -ex
|
||||
|
||||
function _init() {
|
||||
## All binaries are static make sure to disable CGO.
|
||||
export CGO_ENABLED=0
|
||||
|
||||
## List of architectures and OS to test coss compilation.
|
||||
SUPPORTED_OSARCH="linux/ppc64le linux/arm64 linux/s390x darwin/amd64 freebsd/amd64"
|
||||
}
|
||||
|
||||
function _build_and_sign() {
|
||||
local osarch=$1
|
||||
IFS=/ read -r -a arr <<<"$osarch"
|
||||
os="${arr[0]}"
|
||||
arch="${arr[1]}"
|
||||
package=$(go list -f '{{.ImportPath}}')
|
||||
printf -- "--> %15s:%s\n" "${osarch}" "${package}"
|
||||
|
||||
# Go build to build the binary.
|
||||
export GOOS=$os
|
||||
export GOARCH=$arch
|
||||
go build -tags kqueue -o /dev/null
|
||||
}
|
||||
|
||||
function main() {
|
||||
echo "Testing builds for OS/Arch: ${SUPPORTED_OSARCH}"
|
||||
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||
_build_and_sign "${each_osarch}"
|
||||
done
|
||||
}
|
||||
|
||||
_init && main "$@"
|
||||
+313
-135
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,31 +17,33 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/mem"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -95,6 +97,11 @@ func (a adminAPIHandlers) VersionHandler(w http.ResponseWriter, r *http.Request)
|
||||
func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServiceStatus")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
@@ -107,13 +114,8 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re
|
||||
CommitID: CommitID,
|
||||
}
|
||||
|
||||
// Fetch uptimes from all peers. This may fail to due to lack
|
||||
// of read-quorum availability.
|
||||
uptime, err := getPeerUptimes(globalAdminPeers)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Fetch uptimes from all peers and pick the latest.
|
||||
uptime := getPeerUptimes(globalNotificationSys.ServerInfo(ctx))
|
||||
|
||||
// Create API response
|
||||
serverStatus := madmin.ServiceStatus{
|
||||
@@ -127,6 +129,7 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with storage information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
@@ -140,6 +143,11 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re
|
||||
func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServiceStopNRestart")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
@@ -149,7 +157,6 @@ func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *h
|
||||
var sa madmin.ServiceAction
|
||||
err := json.NewDecoder(r.Body).Decode(&sa)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -169,7 +176,15 @@ func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *h
|
||||
// Reply to the client before restarting minio server.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
sendServiceCmd(globalAdminPeers, serviceSig)
|
||||
// Notify all other Minio peers signal service.
|
||||
for _, nerr := range globalNotificationSys.SignalService(serviceSig) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
globalServiceSignalCh <- serviceSig
|
||||
}
|
||||
|
||||
// ServerProperties holds some server information such as, version, region
|
||||
@@ -233,6 +248,12 @@ type ServerInfo struct {
|
||||
func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServerInfo")
|
||||
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate request
|
||||
|
||||
// Setting the region as empty so as the mc server info command is irrespective to the region.
|
||||
@@ -242,39 +263,33 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Web service response
|
||||
reply := make([]ServerInfo, len(globalAdminPeers))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Gather server information for all nodes
|
||||
for i, p := range globalAdminPeers {
|
||||
wg.Add(1)
|
||||
|
||||
// Gather information from a peer in a goroutine
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
|
||||
// Initialize server info at index
|
||||
reply[idx] = ServerInfo{Addr: peer.addr}
|
||||
|
||||
serverInfoData, err := peer.cmdRunner.ServerInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peer.addr)
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
reply[idx].Error = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
reply[idx].Data = &serverInfoData
|
||||
}(i, p)
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
serverInfo := globalNotificationSys.ServerInfo(ctx)
|
||||
// Once we have received all the ServerInfo from peers
|
||||
// add the local peer server info as well.
|
||||
serverInfo = append(serverInfo, ServerInfo{
|
||||
Addr: thisAddr.String(),
|
||||
Data: &ServerInfoData{
|
||||
StorageInfo: objectAPI.StorageInfo(ctx),
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: globalNotificationSys.GetARNList(),
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(reply)
|
||||
jsonBytes, err := json.Marshal(serverInfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
@@ -285,7 +300,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// ServerDrivesPerfInfo holds informantion about address, performance
|
||||
// ServerDrivesPerfInfo holds information about address, performance
|
||||
// of all drives on one server. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
type ServerDrivesPerfInfo struct {
|
||||
@@ -294,6 +309,26 @@ type ServerDrivesPerfInfo struct {
|
||||
Perf []disk.Performance `json:"perf"`
|
||||
}
|
||||
|
||||
// ServerCPULoadInfo holds informantion about cpu utilization
|
||||
// of one minio node. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
type ServerCPULoadInfo struct {
|
||||
Addr string `json:"addr"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Load []cpu.Load `json:"load"`
|
||||
HistoricLoad []cpu.Load `json:"historicLoad"`
|
||||
}
|
||||
|
||||
// ServerMemUsageInfo holds informantion about memory utilization
|
||||
// of one minio node. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
type ServerMemUsageInfo struct {
|
||||
Addr string `json:"addr"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Usage []mem.Usage `json:"usage"`
|
||||
HistoricUsage []mem.Usage `json:"historicUsage"`
|
||||
}
|
||||
|
||||
// PerfInfoHandler - GET /minio/admin/v1/performance?perfType={perfType}
|
||||
// ----------
|
||||
// Get all performance information based on input type
|
||||
@@ -301,6 +336,13 @@ type ServerDrivesPerfInfo struct {
|
||||
func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PerfInfo")
|
||||
|
||||
// Get object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate request
|
||||
// Setting the region as empty so as the mc server info command is irrespective to the region.
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
@@ -313,8 +355,14 @@ func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request
|
||||
perfType := vars["perfType"]
|
||||
|
||||
if perfType == "drive" {
|
||||
info := objLayer.StorageInfo(ctx)
|
||||
if !(info.Backend.Type == BackendFS || info.Backend.Type == BackendErasure) {
|
||||
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
// Get drive performance details from local server's drive(s)
|
||||
dp := localEndpointsPerf(globalEndpoints)
|
||||
dp := localEndpointsDrivePerf(globalEndpoints)
|
||||
|
||||
// Notify all other Minio peers to report drive performance numbers
|
||||
dps := globalNotificationSys.DrivePerfInfo()
|
||||
@@ -330,12 +378,143 @@ func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request
|
||||
// Reply with performance information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
} else if perfType == "cpu" {
|
||||
// Get CPU load details from local server's cpu(s)
|
||||
cpu := localEndpointsCPULoad(globalEndpoints)
|
||||
// Notify all other Minio peers to report cpu load numbers
|
||||
cpus := globalNotificationSys.CPULoadInfo()
|
||||
cpus = append(cpus, cpu)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(cpus)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with cpu load information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
} else if perfType == "mem" {
|
||||
// Get mem usage details from local server(s)
|
||||
m := localEndpointsMemUsage(globalEndpoints)
|
||||
// Notify all other Minio peers to report mem usage numbers
|
||||
mems := globalNotificationSys.MemUsageInfo()
|
||||
mems = append(mems, m)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(mems)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with mem usage information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
} else {
|
||||
writeErrorResponseJSON(w, ErrNotImplemented, r.URL)
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEntry {
|
||||
entry := &madmin.LockEntry{Timestamp: l.Timestamp, Resource: resource, ServerList: []string{server}, Owner: l.Node, Source: l.Source, ID: l.UID}
|
||||
if l.Writer {
|
||||
entry.Type = "Write"
|
||||
} else {
|
||||
entry.Type = "Read"
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func topLockEntries(peerLocks []*PeerLocks) madmin.LockEntries {
|
||||
const listCount int = 10
|
||||
entryMap := make(map[string]*madmin.LockEntry)
|
||||
for _, peerLock := range peerLocks {
|
||||
if peerLock == nil {
|
||||
continue
|
||||
}
|
||||
for k, v := range peerLock.Locks {
|
||||
for _, lockReqInfo := range v {
|
||||
if val, ok := entryMap[lockReqInfo.UID]; ok {
|
||||
val.ServerList = append(val.ServerList, peerLock.Addr)
|
||||
} else {
|
||||
entryMap[lockReqInfo.UID] = newLockEntry(lockReqInfo, k, peerLock.Addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var lockEntries = make(madmin.LockEntries, 0)
|
||||
for _, v := range entryMap {
|
||||
lockEntries = append(lockEntries, *v)
|
||||
}
|
||||
sort.Sort(lockEntries)
|
||||
if len(lockEntries) > listCount {
|
||||
lockEntries = lockEntries[:listCount]
|
||||
}
|
||||
return lockEntries
|
||||
}
|
||||
|
||||
// PeerLocks holds server information result of one node
|
||||
type PeerLocks struct {
|
||||
Addr string
|
||||
Locks GetLocksResp
|
||||
}
|
||||
|
||||
// TopLocksHandler Get list of locks in use
|
||||
func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "TopLocks")
|
||||
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Method only allowed in Distributed XL mode.
|
||||
if globalIsDistXL == false {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate request
|
||||
// Setting the region as empty so as the mc server info command is irrespective to the region.
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
peerLocks := globalNotificationSys.GetLocks(ctx)
|
||||
// Once we have received all the locks currently used from peers
|
||||
// add the local peer locks list as well.
|
||||
localLocks := globalLockServer.ll.DupLockMap()
|
||||
peerLocks = append(peerLocks, &PeerLocks{
|
||||
Addr: thisAddr.String(),
|
||||
Locks: localLocks,
|
||||
})
|
||||
|
||||
topLocks := topLockEntries(peerLocks)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(topLocks)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with storage information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// StartProfilingResult contains the status of the starting
|
||||
// profiling action in a given server
|
||||
type StartProfilingResult struct {
|
||||
@@ -350,6 +529,11 @@ type StartProfilingResult struct {
|
||||
func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "StartProfiling")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
@@ -359,31 +543,53 @@ func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.R
|
||||
vars := mux.Vars(r)
|
||||
profiler := vars["profilerType"]
|
||||
|
||||
startProfilingResult := make([]StartProfilingResult, len(globalAdminPeers))
|
||||
|
||||
// Call StartProfiling function on all nodes and save results
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range globalAdminPeers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
result := StartProfilingResult{NodeName: peer.addr}
|
||||
if err := peer.cmdRunner.StartProfiling(profiler); err != nil {
|
||||
result.Error = err.Error()
|
||||
return
|
||||
}
|
||||
result.Success = true
|
||||
startProfilingResult[idx] = result
|
||||
}(i, peer)
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Start profiling on remote servers.
|
||||
hostErrs := globalNotificationSys.StartProfiling(profiler)
|
||||
|
||||
// Start profiling locally as well.
|
||||
{
|
||||
if globalProfiler != nil {
|
||||
globalProfiler.Stop()
|
||||
}
|
||||
prof, err := startProfiler(profiler, "")
|
||||
if err != nil {
|
||||
hostErrs = append(hostErrs, NotificationPeerErr{
|
||||
Host: *thisAddr,
|
||||
Err: err,
|
||||
})
|
||||
} else {
|
||||
globalProfiler = prof
|
||||
hostErrs = append(hostErrs, NotificationPeerErr{
|
||||
Host: *thisAddr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var startProfilingResult []StartProfilingResult
|
||||
|
||||
for _, nerr := range hostErrs {
|
||||
result := StartProfilingResult{NodeName: nerr.Host.String()}
|
||||
if nerr.Err != nil {
|
||||
result.Error = nerr.Err.Error()
|
||||
} else {
|
||||
result.Success = true
|
||||
}
|
||||
startProfilingResult = append(startProfilingResult, result)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Create JSON result and send it to the client
|
||||
startProfilingResultInBytes, err := json.Marshal(startProfilingResult)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(w, http.StatusInternalServerError, err.Error(), r.URL)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, []byte(startProfilingResultInBytes))
|
||||
}
|
||||
|
||||
@@ -411,51 +617,18 @@ func (f dummyFileInfo) Sys() interface{} { return f.sys }
|
||||
func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DownloadProfiling")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
profilingDataFound := false
|
||||
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of profiling data of all nodes
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for i, peer := range globalAdminPeers {
|
||||
// Get profiling data from a node
|
||||
data, err := peer.cmdRunner.DownloadProfilingData()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to download profiling data from node `%s`, reason: %s", peer.addr, err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
profilingDataFound = true
|
||||
|
||||
// Send profiling data to zip as file
|
||||
header, err := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profiling-%d", i),
|
||||
size: int64(len(data)),
|
||||
mode: 0600,
|
||||
modTime: time.Now().UTC(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err = io.Copy(writer, bytes.NewBuffer(data)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !profilingDataFound {
|
||||
if !globalNotificationSys.DownloadProfilingData(ctx, w) {
|
||||
writeErrorResponseJSON(w, ErrAdminProfilerNotEnabled, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -885,6 +1058,11 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetUserStatus")
|
||||
|
||||
if globalNotificationSys == nil || globalIAMSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
@@ -921,10 +1099,10 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -935,7 +1113,7 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -989,10 +1167,10 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1003,7 +1181,7 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1035,7 +1213,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1062,10 +1240,10 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1076,7 +1254,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1127,10 +1305,10 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1141,7 +1319,7 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1174,10 +1352,10 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1188,7 +1366,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1405,7 +1583,7 @@ func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter,
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1468,10 +1646,10 @@ func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to update credentials
|
||||
for host, err := range globalNotificationSys.LoadCredentials() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadCredentials() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,11 +119,11 @@ var (
|
||||
"broker": "",
|
||||
"topic": "",
|
||||
"qos": 0,
|
||||
"clientId": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"reconnectInterval": 0,
|
||||
"keepAliveInterval": 0
|
||||
"keepAliveInterval": 0,
|
||||
"queueDir": ""
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
@@ -277,7 +277,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
|
||||
// Setup admin mgmt REST API handlers.
|
||||
adminRouter := mux.NewRouter()
|
||||
registerAdminRouter(adminRouter, true)
|
||||
registerAdminRouter(adminRouter, true, true)
|
||||
|
||||
return &adminXLTestBed{
|
||||
xlDirs: xlDirs,
|
||||
@@ -533,7 +533,6 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||
// single node setup, this degenerates to a simple function
|
||||
// call under the hood.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -607,7 +606,6 @@ func TestServiceSetCreds(t *testing.T) {
|
||||
// single node setup, this degenerates to a simple function
|
||||
// call under the hood.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
credentials := globalServerConfig.GetCredential()
|
||||
|
||||
@@ -706,7 +704,6 @@ func TestGetConfigHandler(t *testing.T) {
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for get-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
@@ -735,7 +732,6 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for set-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
@@ -807,7 +803,6 @@ func TestAdminServerInfo(t *testing.T) {
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for set-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
|
||||
@@ -641,10 +641,10 @@ func (h *healSequence) healDiskFormat() error {
|
||||
// Healing succeeded notify the peers to reload format and re-initialize disks.
|
||||
// We will not notify peers only if healing succeeded.
|
||||
if err == nil {
|
||||
for host, rerr := range globalNotificationSys.ReloadFormat(h.settings.DryRun) {
|
||||
if rerr != nil {
|
||||
logger.GetReqInfo(h.ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(h.ctx, rerr)
|
||||
for _, nerr := range globalNotificationSys.ReloadFormat(h.settings.DryRun) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(h.ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(h.ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -719,7 +719,7 @@ func (h *healSequence) healBucket(bucket string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries := runtime.NumCPU() * globalEndpoints.Nodes()
|
||||
entries := runtime.NumCPU()
|
||||
|
||||
marker := ""
|
||||
isTruncated := true
|
||||
|
||||
+13
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,7 +31,7 @@ type adminAPIHandlers struct {
|
||||
}
|
||||
|
||||
// registerAdminRouter - Add handler functions for each service REST API routes.
|
||||
func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||
func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) {
|
||||
|
||||
adminAPI := adminAPIHandlers{}
|
||||
// Admin router
|
||||
@@ -63,9 +63,9 @@ func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||
|
||||
/// Health operations
|
||||
|
||||
// Performance command - return performance details based on input type
|
||||
adminV1Router.Methods(http.MethodGet).Path("/performance").HandlerFunc(httpTraceAll(adminAPI.PerfInfoHandler)).Queries("perfType", "{perfType:.*}")
|
||||
}
|
||||
// Performance command - return performance details based on input type
|
||||
adminV1Router.Methods(http.MethodGet).Path("/performance").HandlerFunc(httpTraceAll(adminAPI.PerfInfoHandler)).Queries("perfType", "{perfType:.*}")
|
||||
|
||||
// Profiling operations
|
||||
adminV1Router.Methods(http.MethodPost).Path("/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
||||
@@ -73,8 +73,7 @@ func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||
adminV1Router.Methods(http.MethodGet).Path("/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
|
||||
|
||||
/// Config operations
|
||||
|
||||
if enableIAM {
|
||||
if enableConfigOps {
|
||||
// Update credentials
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
|
||||
// Get config
|
||||
@@ -86,11 +85,14 @@ func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
||||
// Set config keys/values
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
||||
}
|
||||
|
||||
if enableIAMOps {
|
||||
// -- IAM APIs --
|
||||
|
||||
// Add policy IAM
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name",
|
||||
"{name:.*}")
|
||||
|
||||
// Add user IAM
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
@@ -112,6 +114,10 @@ func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||
adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
|
||||
}
|
||||
|
||||
// -- Top APIs --
|
||||
// Top locks
|
||||
adminV1Router.Methods(http.MethodGet).Path("/top/locks").HandlerFunc(httpTraceHdrs(adminAPI.TopLocksHandler))
|
||||
|
||||
// If none of the routes match, return error.
|
||||
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandlerJSON))
|
||||
}
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2014, 2015, 2016, 2017, 2018 Minio, Inc.
|
||||
*
|
||||
* 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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
|
||||
|
||||
// AdminRPCClient - admin RPC client talks to admin RPC server.
|
||||
type AdminRPCClient struct {
|
||||
*RPCClient
|
||||
}
|
||||
|
||||
// SignalService - calls SignalService RPC.
|
||||
func (rpcClient *AdminRPCClient) SignalService(signal serviceSignal) (err error) {
|
||||
args := SignalServiceArgs{Sig: signal}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(adminServiceName+".SignalService", &args, &reply)
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
||||
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
||||
return sid, err
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling in the remote server.
|
||||
func (rpcClient *AdminRPCClient) StartProfiling(profiler string) error {
|
||||
args := StartProfilingArgs{Profiler: profiler}
|
||||
reply := VoidReply{}
|
||||
return rpcClient.Call(adminServiceName+".StartProfiling", &args, &reply)
|
||||
}
|
||||
|
||||
// DownloadProfilingData - returns profiling data of the remote server.
|
||||
func (rpcClient *AdminRPCClient) DownloadProfilingData() ([]byte, error) {
|
||||
args := AuthArgs{}
|
||||
var reply []byte
|
||||
|
||||
err := rpcClient.Call(adminServiceName+".DownloadProfilingData", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// NewAdminRPCClient - returns new admin RPC client.
|
||||
func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
||||
scheme := "http"
|
||||
if globalIsSSL {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
serviceURL := &xnet.URL{
|
||||
Scheme: scheme,
|
||||
Host: host.String(),
|
||||
Path: adminServicePath,
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if globalIsSSL {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: host.Name,
|
||||
RootCAs: globalRootCAs,
|
||||
}
|
||||
}
|
||||
|
||||
rpcClient, err := NewRPCClient(
|
||||
RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: adminServiceName,
|
||||
ServiceURL: serviceURL,
|
||||
TLSConfig: tlsConfig,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AdminRPCClient{rpcClient}, nil
|
||||
}
|
||||
|
||||
// adminCmdRunner - abstracts local and remote execution of admin
|
||||
// commands like service stop and service restart.
|
||||
type adminCmdRunner interface {
|
||||
SignalService(s serviceSignal) error
|
||||
ServerInfo() (ServerInfoData, error)
|
||||
StartProfiling(string) error
|
||||
DownloadProfilingData() ([]byte, error)
|
||||
}
|
||||
|
||||
// adminPeer - represents an entity that implements admin API RPCs.
|
||||
type adminPeer struct {
|
||||
addr string
|
||||
cmdRunner adminCmdRunner
|
||||
isLocal bool
|
||||
}
|
||||
|
||||
// type alias for a collection of adminPeer.
|
||||
type adminPeers []adminPeer
|
||||
|
||||
// makeAdminPeers - helper function to construct a collection of adminPeer.
|
||||
func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
||||
localAddr := GetLocalPeer(endpoints)
|
||||
if strings.HasPrefix(localAddr, "127.0.0.1:") {
|
||||
// Use first IPv4 instead of loopback address.
|
||||
localAddr = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
|
||||
}
|
||||
if strings.HasPrefix(localAddr, "[::1]:") {
|
||||
// Use first IPv4 instead of loopback address.
|
||||
localAddr = net.JoinHostPort(localIP6.ToSlice()[0], globalMinioPort)
|
||||
}
|
||||
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: localAddr,
|
||||
cmdRunner: localAdminClient{},
|
||||
isLocal: true,
|
||||
})
|
||||
|
||||
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||
host, err := xnet.ParseHost(hostStr)
|
||||
logger.FatalIf(err, "Unable to parse Admin RPC Host")
|
||||
rpcClient, err := NewAdminRPCClient(host)
|
||||
logger.FatalIf(err, "Unable to initialize Admin RPC Client")
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: hostStr,
|
||||
cmdRunner: rpcClient,
|
||||
})
|
||||
}
|
||||
|
||||
return adminPeerList
|
||||
}
|
||||
|
||||
// Initialize global adminPeer collection.
|
||||
func initGlobalAdminPeers(endpoints EndpointList) {
|
||||
globalAdminPeers = makeAdminPeers(endpoints)
|
||||
}
|
||||
|
||||
// invokeServiceCmd - Invoke Restart/Stop command.
|
||||
func invokeServiceCmd(cp adminPeer, cmd serviceSignal) (err error) {
|
||||
switch cmd {
|
||||
case serviceRestart, serviceStop:
|
||||
err = cp.cmdRunner.SignalService(cmd)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// sendServiceCmd - Invoke Restart command on remote peers
|
||||
// adminPeer followed by on the local peer.
|
||||
func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
|
||||
// Send service command like stop or restart to all remote nodes and finally run on local node.
|
||||
errs := make([]error, len(cps))
|
||||
var wg sync.WaitGroup
|
||||
remotePeers := cps[1:]
|
||||
for i := range remotePeers {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
// we use idx+1 because remotePeers slice is 1 position shifted w.r.t cps
|
||||
errs[idx+1] = invokeServiceCmd(remotePeers[idx], cmd)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
errs[0] = invokeServiceCmd(cps[0], cmd)
|
||||
}
|
||||
|
||||
// uptimeSlice - used to sort uptimes in chronological order.
|
||||
type uptimeSlice []struct {
|
||||
err error
|
||||
uptime time.Duration
|
||||
}
|
||||
|
||||
func (ts uptimeSlice) Len() int {
|
||||
return len(ts)
|
||||
}
|
||||
|
||||
func (ts uptimeSlice) Less(i, j int) bool {
|
||||
return ts[i].uptime < ts[j].uptime
|
||||
}
|
||||
|
||||
func (ts uptimeSlice) Swap(i, j int) {
|
||||
ts[i], ts[j] = ts[j], ts[i]
|
||||
}
|
||||
|
||||
// getPeerUptimes - returns the uptime since the last time read quorum
|
||||
// was established on success. Otherwise returns errXLReadQuorum.
|
||||
func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
// In a single node Erasure or FS backend setup the uptime of
|
||||
// the setup is the uptime of the single minio server
|
||||
// instance.
|
||||
if !globalIsDistXL {
|
||||
return UTCNow().Sub(globalBootTime), nil
|
||||
}
|
||||
|
||||
uptimes := make(uptimeSlice, len(peers))
|
||||
|
||||
// Get up time of all servers.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
serverInfoData, rpcErr := peer.cmdRunner.ServerInfo()
|
||||
uptimes[idx].uptime, uptimes[idx].err = serverInfoData.Properties.Uptime, rpcErr
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Sort uptimes in chronological order.
|
||||
sort.Sort(uptimes)
|
||||
|
||||
// Pick the readQuorum'th uptime in chronological order. i.e,
|
||||
// the time at which read quorum was (re-)established.
|
||||
readQuorum := len(uptimes) / 2
|
||||
validCount := 0
|
||||
latestUptime := time.Duration(0)
|
||||
for _, uptime := range uptimes {
|
||||
if uptime.err != nil {
|
||||
logger.LogIf(context.Background(), uptime.err)
|
||||
continue
|
||||
}
|
||||
|
||||
validCount++
|
||||
if validCount >= readQuorum {
|
||||
latestUptime = uptime.uptime
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Less than readQuorum "Admin.Uptime" RPC call returned
|
||||
// successfully, so read-quorum unavailable.
|
||||
if validCount < readQuorum {
|
||||
return time.Duration(0), InsufficientReadQuorum{}
|
||||
}
|
||||
|
||||
return latestUptime, nil
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||
*
|
||||
* 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 cmd
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xrpc "github.com/minio/minio/cmd/rpc"
|
||||
)
|
||||
|
||||
const adminServiceName = "Admin"
|
||||
const adminServiceSubPath = "/admin"
|
||||
|
||||
var adminServicePath = path.Join(minioReservedBucketPath, adminServiceSubPath)
|
||||
|
||||
// adminRPCReceiver - Admin RPC receiver for admin RPC server.
|
||||
type adminRPCReceiver struct {
|
||||
local *localAdminClient
|
||||
}
|
||||
|
||||
// SignalServiceArgs - provides the signal argument to SignalService RPC
|
||||
type SignalServiceArgs struct {
|
||||
AuthArgs
|
||||
Sig serviceSignal
|
||||
}
|
||||
|
||||
// SignalService - Send a restart or stop signal to the service
|
||||
func (receiver *adminRPCReceiver) SignalService(args *SignalServiceArgs, reply *VoidReply) error {
|
||||
return receiver.local.SignalService(args.Sig)
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info when object layer was initialized on this server.
|
||||
func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) (err error) {
|
||||
*reply, err = receiver.local.ServerInfo()
|
||||
return err
|
||||
}
|
||||
|
||||
// StartProfilingArgs - holds the RPC argument for StartingProfiling RPC call
|
||||
type StartProfilingArgs struct {
|
||||
AuthArgs
|
||||
Profiler string
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling of this server
|
||||
func (receiver *adminRPCReceiver) StartProfiling(args *StartProfilingArgs, reply *VoidReply) error {
|
||||
return receiver.local.StartProfiling(args.Profiler)
|
||||
}
|
||||
|
||||
// DownloadProfilingData - stops and returns profiling data of this server
|
||||
func (receiver *adminRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[]byte) (err error) {
|
||||
*reply, err = receiver.local.DownloadProfilingData()
|
||||
return
|
||||
}
|
||||
|
||||
// NewAdminRPCServer - returns new admin RPC server.
|
||||
func NewAdminRPCServer() (*xrpc.Server, error) {
|
||||
rpcServer := xrpc.NewServer()
|
||||
if err := rpcServer.RegisterName(adminServiceName, &adminRPCReceiver{&localAdminClient{}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rpcServer, nil
|
||||
}
|
||||
|
||||
// registerAdminRPCRouter - creates and registers Admin RPC server and its router.
|
||||
func registerAdminRPCRouter(router *mux.Router) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server")
|
||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* 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 cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// localAdminClient and AdminRPCClient are adminCmdRunner interface compatible,
|
||||
// hence below test functions are available for both clients.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Admin RPC server, adminRPCReceiver and AdminRPCClient are
|
||||
// inter-dependent, below test functions are sufficient to test all of them.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
|
||||
defer func(sigChan chan serviceSignal) { globalServiceSignalCh = sigChan }(globalServiceSignalCh)
|
||||
globalServiceSignalCh = make(chan serviceSignal, 10)
|
||||
|
||||
testCases := []struct {
|
||||
signal serviceSignal
|
||||
expectErr bool
|
||||
}{
|
||||
{serviceRestart, false},
|
||||
{serviceStop, false},
|
||||
{serviceStatus, true},
|
||||
{serviceSignal(100), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := client.SignalService(testCase.signal)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
||||
tmpGlobalBootTime := globalBootTime
|
||||
tmpGlobalObjectAPI := globalObjectAPI
|
||||
tmpGlobalConnStats := globalConnStats
|
||||
tmpGlobalHTTPStats := globalHTTPStats
|
||||
tmpGlobalNotificationSys := globalNotificationSys
|
||||
defer func() {
|
||||
globalBootTime = tmpGlobalBootTime
|
||||
globalObjectAPI = tmpGlobalObjectAPI
|
||||
globalConnStats = tmpGlobalConnStats
|
||||
globalHTTPStats = tmpGlobalHTTPStats
|
||||
globalNotificationSys = tmpGlobalNotificationSys
|
||||
}()
|
||||
|
||||
endpoints := new(EndpointList)
|
||||
|
||||
notificationSys := NewNotificationSys(globalServerConfig, *endpoints)
|
||||
|
||||
testCases := []struct {
|
||||
bootTime time.Time
|
||||
objectAPI ObjectLayer
|
||||
connStats *ConnStats
|
||||
httpStats *HTTPStats
|
||||
notificationSys *NotificationSys
|
||||
expectErr bool
|
||||
}{
|
||||
{UTCNow(), &DummyObjectLayer{}, newConnStats(), newHTTPStats(), notificationSys, false},
|
||||
{time.Time{}, nil, nil, nil, nil, true},
|
||||
{UTCNow(), nil, nil, nil, nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
globalBootTime = testCase.bootTime
|
||||
globalObjectAPI = testCase.objectAPI
|
||||
globalConnStats = testCase.connStats
|
||||
globalHTTPStats = testCase.httpStats
|
||||
globalNotificationSys = testCase.notificationSys
|
||||
_, err := client.ServerInfo()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rpcServer.ServeHTTP(w, r)
|
||||
}))
|
||||
|
||||
url, err := xnet.ParseURL(httpServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
host, err := xnet.ParseHost(url.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
prevGlobalServerConfig := globalServerConfig
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
rpcClient, err := NewAdminRPCClient(host)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
return httpServer, rpcClient, prevGlobalServerConfig
|
||||
}
|
||||
|
||||
func TestAdminRPCClientSignalService(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerSignalService(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientServerInfo(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerServerInfo(t, rpcClient)
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@ func mustGetRequestID(t time.Time) string {
|
||||
|
||||
// Write http common headers
|
||||
func setCommonHeaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Server", globalServerUserAgent)
|
||||
w.Header().Set("Server", "Minio/"+ReleaseTag)
|
||||
// Set `x-amz-bucket-region` only if region is set on the server
|
||||
// by default minio uses an empty region.
|
||||
if region := globalServerConfig.GetRegion(); region != "" {
|
||||
|
||||
+4
-4
@@ -283,7 +283,7 @@ func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Ac
|
||||
AccountName: cred.AccessKey,
|
||||
Action: action,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, locationConstraint),
|
||||
ConditionValues: getConditionValues(r, locationConstraint, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
@@ -296,7 +296,7 @@ func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Ac
|
||||
AccountName: cred.AccessKey,
|
||||
Action: iampolicy.Action(action),
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey),
|
||||
ObjectName: objectName,
|
||||
IsOwner: owner,
|
||||
Claims: claims,
|
||||
@@ -448,7 +448,7 @@ func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request
|
||||
AccountName: cred.AccessKey,
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
@@ -461,7 +461,7 @@ func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request
|
||||
AccountName: cred.AccessKey,
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey),
|
||||
ObjectName: objectName,
|
||||
IsOwner: owner,
|
||||
Claims: claims,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2019 Minio, Inc.
|
||||
*
|
||||
* 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 cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// Calculates bitrot in chunks and writes the hash into the stream.
|
||||
type streamingBitrotWriter struct {
|
||||
iow *io.PipeWriter
|
||||
h hash.Hash
|
||||
shardSize int64
|
||||
canClose chan struct{} // Needed to avoid race explained in Close() call.
|
||||
|
||||
// Following two fields are used only to make sure that Write(p) is called such that
|
||||
// len(p) is always the block size except the last block, i.e prevent programmer errors.
|
||||
currentBlockIdx int
|
||||
verifyTillIdx int
|
||||
}
|
||||
|
||||
func (b *streamingBitrotWriter) Write(p []byte) (int, error) {
|
||||
if b.currentBlockIdx < b.verifyTillIdx && int64(len(p)) != b.shardSize {
|
||||
// All blocks except last should be of the length b.shardSize
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
return 0, errUnexpected
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
b.h.Reset()
|
||||
b.h.Write(p)
|
||||
hashBytes := b.h.Sum(nil)
|
||||
n, err := b.iow.Write(hashBytes)
|
||||
if n != len(hashBytes) {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
n, err = b.iow.Write(p)
|
||||
b.currentBlockIdx++
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *streamingBitrotWriter) Close() error {
|
||||
err := b.iow.Close()
|
||||
// Wait for all data to be written before returning else it causes race conditions.
|
||||
// Race condition is because of io.PipeWriter implementation. i.e consider the following
|
||||
// sequent of operations:
|
||||
// 1) pipe.Write()
|
||||
// 2) pipe.Close()
|
||||
// Now pipe.Close() can return before the data is read on the other end of the pipe and written to the disk
|
||||
// Hence an immediate Read() on the file can return incorrect data.
|
||||
<-b.canClose
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns streaming bitrot writer implementation.
|
||||
func newStreamingBitrotWriter(disk StorageAPI, volume, filePath string, length int64, algo BitrotAlgorithm, shardSize int64) io.WriteCloser {
|
||||
r, w := io.Pipe()
|
||||
h := algo.New()
|
||||
bw := &streamingBitrotWriter{w, h, shardSize, make(chan struct{}), 0, int(length / shardSize)}
|
||||
go func() {
|
||||
bitrotSumsTotalSize := ceilFrac(length, shardSize) * int64(h.Size()) // Size used for storing bitrot checksums.
|
||||
totalFileSize := bitrotSumsTotalSize + length
|
||||
err := disk.CreateFile(volume, filePath, totalFileSize, r)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
r.CloseWithError(err)
|
||||
}
|
||||
close(bw.canClose)
|
||||
}()
|
||||
return bw
|
||||
}
|
||||
|
||||
// ReadAt() implementation which verifies the bitrot hash available as part of the stream.
|
||||
type streamingBitrotReader struct {
|
||||
disk StorageAPI
|
||||
rc io.ReadCloser
|
||||
volume string
|
||||
filePath string
|
||||
tillOffset int64
|
||||
currOffset int64
|
||||
h hash.Hash
|
||||
shardSize int64
|
||||
hashBytes []byte
|
||||
}
|
||||
|
||||
func (b *streamingBitrotReader) Close() error {
|
||||
if b.rc == nil {
|
||||
return nil
|
||||
}
|
||||
return b.rc.Close()
|
||||
}
|
||||
|
||||
func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
|
||||
var err error
|
||||
if offset%b.shardSize != 0 {
|
||||
// Offset should always be aligned to b.shardSize
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
return 0, errUnexpected
|
||||
}
|
||||
if b.rc == nil {
|
||||
// For the first ReadAt() call we need to open the stream for reading.
|
||||
b.currOffset = offset
|
||||
streamOffset := (offset/b.shardSize)*int64(b.h.Size()) + offset
|
||||
b.rc, err = b.disk.ReadFileStream(b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if offset != b.currOffset {
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
return 0, errUnexpected
|
||||
}
|
||||
b.h.Reset()
|
||||
_, err = io.ReadFull(b.rc, b.hashBytes)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
_, err = io.ReadFull(b.rc, buf)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
b.h.Write(buf)
|
||||
|
||||
if bytes.Compare(b.h.Sum(nil), b.hashBytes) != 0 {
|
||||
err = hashMismatchError{hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil))}
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
b.currOffset += int64(len(buf))
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
// Returns streaming bitrot reader implementation.
|
||||
func newStreamingBitrotReader(disk StorageAPI, volume, filePath string, tillOffset int64, algo BitrotAlgorithm, shardSize int64) *streamingBitrotReader {
|
||||
h := algo.New()
|
||||
return &streamingBitrotReader{
|
||||
disk,
|
||||
nil,
|
||||
volume,
|
||||
filePath,
|
||||
ceilFrac(tillOffset, shardSize)*int64(h.Size()) + tillOffset,
|
||||
0,
|
||||
h,
|
||||
shardSize,
|
||||
make([]byte, h.Size()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2019 Minio, Inc.
|
||||
*
|
||||
* 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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// Implementation to calculate bitrot for the whole file.
|
||||
type wholeBitrotWriter struct {
|
||||
disk StorageAPI
|
||||
volume string
|
||||
filePath string
|
||||
shardSize int64 // This is the shard size of the erasure logic
|
||||
hash.Hash // For bitrot hash
|
||||
|
||||
// Following two fields are used only to make sure that Write(p) is called such that
|
||||
// len(p) is always the block size except the last block and prevent programmer errors.
|
||||
currentBlockIdx int
|
||||
lastBlockIdx int
|
||||
}
|
||||
|
||||
func (b *wholeBitrotWriter) Write(p []byte) (int, error) {
|
||||
if b.currentBlockIdx < b.lastBlockIdx && int64(len(p)) != b.shardSize {
|
||||
// All blocks except last should be of the length b.shardSize
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
return 0, errUnexpected
|
||||
}
|
||||
err := b.disk.AppendFile(b.volume, b.filePath, p)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
_, err = b.Hash.Write(p)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
b.currentBlockIdx++
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *wholeBitrotWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns whole-file bitrot writer.
|
||||
func newWholeBitrotWriter(disk StorageAPI, volume, filePath string, length int64, algo BitrotAlgorithm, shardSize int64) io.WriteCloser {
|
||||
return &wholeBitrotWriter{disk, volume, filePath, shardSize, algo.New(), 0, int(length / shardSize)}
|
||||
}
|
||||
|
||||
// Implementation to verify bitrot for the whole file.
|
||||
type wholeBitrotReader struct {
|
||||
disk StorageAPI
|
||||
volume string
|
||||
filePath string
|
||||
verifier *BitrotVerifier // Holds the bit-rot info
|
||||
tillOffset int64 // Affects the length of data requested in disk.ReadFile depending on Read()'s offset
|
||||
buf []byte // Holds bit-rot verified data
|
||||
}
|
||||
|
||||
func (b *wholeBitrotReader) ReadAt(buf []byte, offset int64) (n int, err error) {
|
||||
if b.buf == nil {
|
||||
b.buf = make([]byte, b.tillOffset-offset)
|
||||
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||
ctx := context.Background()
|
||||
logger.GetReqInfo(ctx).AppendTags("disk", b.disk.String())
|
||||
logger.LogIf(ctx, err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if len(b.buf) < len(buf) {
|
||||
logger.LogIf(context.Background(), errLessData)
|
||||
return 0, errLessData
|
||||
}
|
||||
n = copy(buf, b.buf)
|
||||
b.buf = b.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Returns whole-file bitrot reader.
|
||||
func newWholeBitrotReader(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm, tillOffset int64, sum []byte) *wholeBitrotReader {
|
||||
return &wholeBitrotReader{
|
||||
disk: disk,
|
||||
volume: volume,
|
||||
filePath: filePath,
|
||||
verifier: &BitrotVerifier{algo, sum},
|
||||
tillOffset: tillOffset,
|
||||
buf: nil,
|
||||
}
|
||||
}
|
||||
+65
-72
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -38,19 +39,22 @@ const (
|
||||
SHA256 BitrotAlgorithm = 1 + iota
|
||||
// HighwayHash256 represents the HighwayHash-256 hash function
|
||||
HighwayHash256
|
||||
// HighwayHash256S represents the Streaming HighwayHash-256 hash function
|
||||
HighwayHash256S
|
||||
// BLAKE2b512 represents the BLAKE2b-512 hash function
|
||||
BLAKE2b512
|
||||
)
|
||||
|
||||
// DefaultBitrotAlgorithm is the default algorithm used for bitrot protection.
|
||||
const (
|
||||
DefaultBitrotAlgorithm = HighwayHash256
|
||||
DefaultBitrotAlgorithm = HighwayHash256S
|
||||
)
|
||||
|
||||
var bitrotAlgorithms = map[BitrotAlgorithm]string{
|
||||
SHA256: "sha256",
|
||||
BLAKE2b512: "blake2b",
|
||||
HighwayHash256: "highwayhash256",
|
||||
SHA256: "sha256",
|
||||
BLAKE2b512: "blake2b",
|
||||
HighwayHash256: "highwayhash256",
|
||||
HighwayHash256S: "highwayhash256S",
|
||||
}
|
||||
|
||||
// New returns a new hash.Hash calculating the given bitrot algorithm.
|
||||
@@ -64,6 +68,9 @@ func (a BitrotAlgorithm) New() hash.Hash {
|
||||
case HighwayHash256:
|
||||
hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
|
||||
return hh
|
||||
case HighwayHash256S:
|
||||
hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
|
||||
return hh
|
||||
default:
|
||||
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
|
||||
return nil
|
||||
@@ -109,86 +116,72 @@ func BitrotAlgorithmFromString(s string) (a BitrotAlgorithm) {
|
||||
return
|
||||
}
|
||||
|
||||
// To read bit-rot verified data.
|
||||
type bitrotReader struct {
|
||||
disk StorageAPI
|
||||
volume string
|
||||
filePath string
|
||||
verifier *BitrotVerifier // Holds the bit-rot info
|
||||
endOffset int64 // Affects the length of data requested in disk.ReadFile depending on Read()'s offset
|
||||
buf []byte // Holds bit-rot verified data
|
||||
}
|
||||
|
||||
// newBitrotReader returns bitrotReader.
|
||||
// Note that the buffer is allocated later in Read(). This is because we will know the buffer length only
|
||||
// during the bitrotReader.Read(). Depending on when parallelReader fails-over, the buffer length can be different.
|
||||
func newBitrotReader(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm, endOffset int64, sum []byte) *bitrotReader {
|
||||
return &bitrotReader{
|
||||
disk: disk,
|
||||
volume: volume,
|
||||
filePath: filePath,
|
||||
verifier: &BitrotVerifier{algo, sum},
|
||||
endOffset: endOffset,
|
||||
buf: nil,
|
||||
func newBitrotWriter(disk StorageAPI, volume, filePath string, length int64, algo BitrotAlgorithm, shardSize int64) io.Writer {
|
||||
if algo == HighwayHash256S {
|
||||
return newStreamingBitrotWriter(disk, volume, filePath, length, algo, shardSize)
|
||||
}
|
||||
return newWholeBitrotWriter(disk, volume, filePath, length, algo, shardSize)
|
||||
}
|
||||
|
||||
// ReadChunk returns requested data.
|
||||
func (b *bitrotReader) ReadChunk(offset int64, length int64) ([]byte, error) {
|
||||
if b.buf == nil {
|
||||
b.buf = make([]byte, b.endOffset-offset)
|
||||
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||
ctx := context.Background()
|
||||
logger.GetReqInfo(ctx).AppendTags("disk", b.disk.String())
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
func newBitrotReader(disk StorageAPI, bucket string, filePath string, tillOffset int64, algo BitrotAlgorithm, sum []byte, shardSize int64) io.ReaderAt {
|
||||
if algo == HighwayHash256S {
|
||||
return newStreamingBitrotReader(disk, bucket, filePath, tillOffset, algo, shardSize)
|
||||
}
|
||||
return newWholeBitrotReader(disk, bucket, filePath, algo, tillOffset, sum)
|
||||
}
|
||||
|
||||
// Close all the readers.
|
||||
func closeBitrotReaders(rs []io.ReaderAt) {
|
||||
for _, r := range rs {
|
||||
if br, ok := r.(*streamingBitrotReader); ok {
|
||||
br.Close()
|
||||
}
|
||||
}
|
||||
if int64(len(b.buf)) < length {
|
||||
logger.LogIf(context.Background(), errLessData)
|
||||
return nil, errLessData
|
||||
}
|
||||
retBuf := b.buf[:length]
|
||||
b.buf = b.buf[length:]
|
||||
return retBuf, nil
|
||||
}
|
||||
|
||||
// To calculate the bit-rot of the written data.
|
||||
type bitrotWriter struct {
|
||||
disk StorageAPI
|
||||
volume string
|
||||
filePath string
|
||||
h hash.Hash
|
||||
}
|
||||
|
||||
// newBitrotWriter returns bitrotWriter.
|
||||
func newBitrotWriter(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm) *bitrotWriter {
|
||||
return &bitrotWriter{
|
||||
disk: disk,
|
||||
volume: volume,
|
||||
filePath: filePath,
|
||||
h: algo.New(),
|
||||
// Close all the writers.
|
||||
func closeBitrotWriters(ws []io.Writer) {
|
||||
for _, w := range ws {
|
||||
if bw, ok := w.(*streamingBitrotWriter); ok {
|
||||
bw.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append appends the data and while calculating the hash.
|
||||
func (b *bitrotWriter) Append(buf []byte) error {
|
||||
n, err := b.h.Write(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(buf) {
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
return errUnexpected
|
||||
}
|
||||
if err = b.disk.AppendFile(b.volume, b.filePath, buf); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return err
|
||||
// Returns hash sum for whole-bitrot, nil for streaming-bitrot.
|
||||
func bitrotWriterSum(w io.Writer) []byte {
|
||||
if bw, ok := w.(*wholeBitrotWriter); ok {
|
||||
return bw.Sum(nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sum returns bit-rot sum.
|
||||
func (b *bitrotWriter) Sum() []byte {
|
||||
return b.h.Sum(nil)
|
||||
// Verify if a file has bitrot error.
|
||||
func bitrotCheckFile(disk StorageAPI, volume string, filePath string, tillOffset int64, algo BitrotAlgorithm, sum []byte, shardSize int64) (err error) {
|
||||
if algo != HighwayHash256S {
|
||||
buf := []byte{}
|
||||
// For whole-file bitrot we don't need to read the entire file as the bitrot verify happens on the server side even if we read 0-bytes.
|
||||
_, err = disk.ReadFile(volume, filePath, 0, buf, NewBitrotVerifier(algo, sum))
|
||||
return err
|
||||
}
|
||||
buf := make([]byte, shardSize)
|
||||
r := newStreamingBitrotReader(disk, volume, filePath, tillOffset, algo, shardSize)
|
||||
defer closeBitrotReaders([]io.ReaderAt{r})
|
||||
var offset int64
|
||||
for {
|
||||
if offset == tillOffset {
|
||||
break
|
||||
}
|
||||
var n int
|
||||
tmpBuf := buf
|
||||
if int64(len(tmpBuf)) > (tillOffset - offset) {
|
||||
tmpBuf = tmpBuf[:(tillOffset - offset)]
|
||||
}
|
||||
n, err = r.ReadAt(tmpBuf, offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
offset += int64(n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+26
-13
@@ -17,13 +17,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBitrotReaderWriter(t *testing.T) {
|
||||
func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -40,32 +41,44 @@ func TestBitrotReaderWriter(t *testing.T) {
|
||||
|
||||
disk.MakeVol(volume)
|
||||
|
||||
writer := newBitrotWriter(disk, volume, filePath, HighwayHash256)
|
||||
writer := newBitrotWriter(disk, volume, filePath, 35, bitrotAlgo, 10)
|
||||
|
||||
err = writer.Append([]byte("aaaaaaaaa"))
|
||||
_, err = writer.Write([]byte("aaaaaaaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("a"))
|
||||
_, err = writer.Write([]byte("aaaaaaaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("aaaaaaaaaa"))
|
||||
_, err = writer.Write([]byte("aaaaaaaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("aaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("aaaaaaaaaa"))
|
||||
_, err = writer.Write([]byte("aaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
writer.(io.Closer).Close()
|
||||
|
||||
reader := newBitrotReader(disk, volume, filePath, HighwayHash256, 35, writer.Sum())
|
||||
|
||||
if _, err = reader.ReadChunk(0, 35); err != nil {
|
||||
reader := newBitrotReader(disk, volume, filePath, 35, bitrotAlgo, bitrotWriterSum(writer), 10)
|
||||
b := make([]byte, 10)
|
||||
if _, err = reader.ReadAt(b, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err = reader.ReadAt(b, 10); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err = reader.ReadAt(b, 20); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err = reader.ReadAt(b[:5], 30); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllBitrotAlgorithms(t *testing.T) {
|
||||
for bitrotAlgo := range bitrotAlgorithms {
|
||||
testBitrotReaderWriterAlgo(t, bitrotAlgo)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-29
@@ -68,28 +68,6 @@ func parsePublicCertFile(certFile string) (x509Certs []*x509.Certificate, err er
|
||||
}
|
||||
|
||||
func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||
// Get all CA file names.
|
||||
var caFiles []string
|
||||
fis, err := readDir(certsCAsDir)
|
||||
if err != nil && err != errFileNotFound {
|
||||
return nil, err
|
||||
}
|
||||
// Return success if CA's directory is missing.
|
||||
if err == errFileNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
for _, fi := range fis {
|
||||
// Skip all directories.
|
||||
if hasSuffix(fi, slashSeparator) {
|
||||
continue
|
||||
}
|
||||
// We are only interested in regular files here.
|
||||
caFiles = append(caFiles, pathJoin(certsCAsDir, fi))
|
||||
}
|
||||
if len(caFiles) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rootCAs, _ := x509.SystemCertPool()
|
||||
if rootCAs == nil {
|
||||
// In some systems (like Windows) system cert pool is
|
||||
@@ -98,16 +76,26 @@ func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// Load custom root CAs for client requests
|
||||
for _, caFile := range caFiles {
|
||||
caCert, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
fis, err := readDir(certsCAsDir)
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
err = nil // Return success if CA's directory is missing.
|
||||
}
|
||||
|
||||
rootCAs.AppendCertsFromPEM(caCert)
|
||||
return rootCAs, err
|
||||
}
|
||||
|
||||
// Load all custom CA files.
|
||||
for _, fi := range fis {
|
||||
// Skip all directories.
|
||||
if hasSuffix(fi, slashSeparator) {
|
||||
continue
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(pathJoin(certsCAsDir, fi))
|
||||
if err != nil {
|
||||
return rootCAs, err
|
||||
}
|
||||
rootCAs.AppendCertsFromPEM(caCert)
|
||||
}
|
||||
return rootCAs, nil
|
||||
}
|
||||
|
||||
|
||||
+19
-4
@@ -78,18 +78,23 @@ func loadLoggers() {
|
||||
|
||||
}
|
||||
|
||||
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) *ConfigDir {
|
||||
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
|
||||
var dir string
|
||||
var dirSet bool
|
||||
|
||||
switch {
|
||||
case ctx.IsSet(option):
|
||||
dir = ctx.String(option)
|
||||
dirSet = true
|
||||
case ctx.GlobalIsSet(option):
|
||||
dir = ctx.GlobalString(option)
|
||||
dirSet = true
|
||||
// cli package does not expose parent's option option. Below code is workaround.
|
||||
if dir == "" || dir == getDefaultDir() {
|
||||
dirSet = false // Unset to false since GlobalIsSet() true is a false positive.
|
||||
if ctx.Parent().GlobalIsSet(option) {
|
||||
dir = ctx.Parent().GlobalString(option)
|
||||
dirSet = true
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -111,7 +116,7 @@ func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() s
|
||||
|
||||
logger.FatalIf(mkdirAllIgnorePerm(dirAbs), "Unable to create directory specified %s=%s", option, dir)
|
||||
|
||||
return &ConfigDir{path: dirAbs}
|
||||
return &ConfigDir{path: dirAbs}, dirSet
|
||||
}
|
||||
|
||||
func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
@@ -142,8 +147,17 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
}
|
||||
|
||||
// Set all config, certs and CAs directories.
|
||||
globalConfigDir = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
|
||||
globalCertsDir = newConfigDirFromCtx(ctx, "certs-dir", defaultCertsDir.Get)
|
||||
var configSet, certsSet bool
|
||||
globalConfigDir, configSet = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
|
||||
globalCertsDir, certsSet = newConfigDirFromCtx(ctx, "certs-dir", defaultCertsDir.Get)
|
||||
|
||||
// Remove this code when we deprecate and remove config-dir.
|
||||
// This code is to make sure we inherit from the config-dir
|
||||
// option if certs-dir is not provided.
|
||||
if !certsSet && configSet {
|
||||
globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)}
|
||||
}
|
||||
|
||||
globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
|
||||
|
||||
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
|
||||
@@ -228,6 +242,7 @@ func handleCommonEnvVars() {
|
||||
return &cert, terr
|
||||
}
|
||||
}
|
||||
|
||||
globalEtcdClient, err = etcd.New(etcd.Config{
|
||||
Endpoints: etcdEndpoints,
|
||||
DialTimeout: defaultDialTimeout,
|
||||
|
||||
+32
-14
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -60,8 +61,15 @@ func deleteConfig(ctx context.Context, objAPI ObjectLayer, configFile string) er
|
||||
}
|
||||
|
||||
func saveConfigEtcd(ctx context.Context, client *etcd.Client, configFile string, data []byte) error {
|
||||
_, err := client.Put(ctx, configFile, string(data))
|
||||
return err
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
|
||||
defer cancel()
|
||||
_, err := client.Put(timeoutCtx, configFile, string(data))
|
||||
if err == context.DeadlineExceeded {
|
||||
return fmt.Errorf("etcd setup is unreachable, please check your endpoints %s", client.Endpoints())
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("unexpected error %s returned by etcd setup, please check your endpoints %s", err, client.Endpoints())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
|
||||
@@ -75,9 +83,14 @@ func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data
|
||||
}
|
||||
|
||||
func readConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) ([]byte, error) {
|
||||
resp, err := client.Get(ctx, configFile)
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
|
||||
defer cancel()
|
||||
resp, err := client.Get(timeoutCtx, configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("etcd setup is unreachable, please check your endpoints %s", client.Endpoints())
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected error %s returned by etcd setup, please check your endpoints %s", err, client.Endpoints())
|
||||
}
|
||||
if resp.Count == 0 {
|
||||
return nil, errConfigNotFound
|
||||
@@ -90,23 +103,28 @@ func readConfigEtcd(ctx context.Context, client *etcd.Client, configFile string)
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
|
||||
// watchConfig - watches for changes on `configFile` on etcd and loads them.
|
||||
func watchConfig(objAPI ObjectLayer, configFile string, loadCfgFn func(ObjectLayer) error) {
|
||||
if globalEtcdClient != nil {
|
||||
for watchResp := range globalEtcdClient.Watch(context.Background(), configFile) {
|
||||
for _, event := range watchResp.Events {
|
||||
if event.IsModify() || event.IsCreate() {
|
||||
loadCfgFn(objAPI)
|
||||
}
|
||||
// watchConfigEtcd - watches for changes on `configFile` on etcd and loads them.
|
||||
func watchConfigEtcd(objAPI ObjectLayer, configFile string, loadCfgFn func(ObjectLayer) error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
defer cancel()
|
||||
for watchResp := range globalEtcdClient.Watch(ctx, configFile) {
|
||||
for _, event := range watchResp.Events {
|
||||
if event.IsModify() || event.IsCreate() {
|
||||
loadCfgFn(objAPI)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) error {
|
||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
|
||||
defer cancel()
|
||||
resp, err := client.Get(timeoutCtx, configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
if err == context.DeadlineExceeded {
|
||||
return fmt.Errorf("etcd setup is unreachable, please check your endpoints %s", client.Endpoints())
|
||||
}
|
||||
return fmt.Errorf("unexpected error %s returned by etcd setup, please check your endpoints %s", err, client.Endpoints())
|
||||
}
|
||||
if resp.Count == 0 {
|
||||
return errConfigNotFound
|
||||
|
||||
+10
-12
@@ -545,19 +545,15 @@ func (s *serverConfig) loadToCachedConfigs() {
|
||||
globalIsCompressionEnabled = compressionConf.Enabled
|
||||
}
|
||||
|
||||
if globalIAMValidators == nil {
|
||||
globalIAMValidators = getAuthValidators(s)
|
||||
}
|
||||
globalIAMValidators = getAuthValidators(s)
|
||||
|
||||
if globalPolicyOPA == nil {
|
||||
if s.Policy.OPA.URL != nil && s.Policy.OPA.URL.String() != "" {
|
||||
globalPolicyOPA = iampolicy.NewOpa(iampolicy.OpaArgs{
|
||||
URL: s.Policy.OPA.URL,
|
||||
AuthToken: s.Policy.OPA.AuthToken,
|
||||
Transport: NewCustomHTTPTransport(),
|
||||
CloseRespFn: CloseResponse,
|
||||
})
|
||||
}
|
||||
if s.Policy.OPA.URL != nil && s.Policy.OPA.URL.String() != "" {
|
||||
globalPolicyOPA = iampolicy.NewOpa(iampolicy.OpaArgs{
|
||||
URL: s.Policy.OPA.URL,
|
||||
AuthToken: s.Policy.OPA.AuthToken,
|
||||
Transport: NewCustomHTTPTransport(),
|
||||
CloseRespFn: CloseResponse,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,6 +680,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
|
||||
|
||||
for id, args := range config.Notify.MQTT {
|
||||
if args.Enable {
|
||||
args.RootCAs = globalRootCAs
|
||||
newTarget, err := target.NewMQTTTarget(id, args)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
@@ -768,6 +765,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
|
||||
|
||||
for id, args := range config.Notify.Webhook {
|
||||
if args.Enable {
|
||||
args.RootCAs = globalRootCAs
|
||||
newTarget := target.NewWebhookTarget(id, args)
|
||||
if err := targetList.Add(newTarget); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
|
||||
@@ -233,7 +233,7 @@ func TestValidateConfig(t *testing.T) {
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
||||
|
||||
// Test 27 - Test MQTT
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, false},
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "username": "", "password": "", "queueDir": ""}}}}`, false},
|
||||
|
||||
// Test 28 - Test NSQ
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nsq": { "1": { "enable": true, "nsqdAddress": "", "topic": ""} }}}`, false},
|
||||
|
||||
+25
-7
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
@@ -235,7 +234,7 @@ func purgeV1() error {
|
||||
|
||||
cv1 := &configV1{}
|
||||
_, err := Load(configFile, cv1)
|
||||
if os.IsNotExist(err) || err == dns.ErrNoEntriesFound {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config version ‘1’. %v", err)
|
||||
@@ -2415,6 +2414,14 @@ func migrateV27ToV28() error {
|
||||
|
||||
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
|
||||
func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
// Construct path to config.json for the given bucket.
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Verify if config was already available in .minio.sys in which case, nothing more to be done.
|
||||
if err = checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Rename config.json to config.json.deprecated only upon
|
||||
// success of this function.
|
||||
@@ -2423,8 +2430,6 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
// Construct path to config.json for the given bucket.
|
||||
transactionConfigFile := configFile + ".transaction"
|
||||
|
||||
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||
@@ -2436,7 +2441,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
|
||||
// Verify if backend already has the file.
|
||||
// Verify if backend already has the file (after holding lock)
|
||||
if err = checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
||||
return err
|
||||
} // if errConfigNotFound proceed to migrate..
|
||||
@@ -2448,10 +2453,14 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
}
|
||||
// Read from deprecate file as well if necessary.
|
||||
if _, err = Load(getConfigFile()+".deprecated", config); err != nil {
|
||||
return err
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// If all else fails simply initialize the server config.
|
||||
return newSrvConfig(objAPI)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return saveServerConfig(context.Background(), objAPI, config)
|
||||
}
|
||||
|
||||
@@ -2459,6 +2468,15 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Check if the config version is latest, if not migrate.
|
||||
ok, _, err := checkConfigVersion(objAPI, configFile, serverConfigVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Construct path to config.json for the given bucket.
|
||||
transactionConfigFile := configFile + ".transaction"
|
||||
|
||||
|
||||
@@ -893,7 +893,7 @@ type serverConfigV32 struct {
|
||||
} `json:"policy"`
|
||||
}
|
||||
|
||||
// serverConfigV33 is just like version '32', removes clientID from NATS.
|
||||
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir with MQTT.
|
||||
type serverConfigV33 struct {
|
||||
quick.Config `json:"-"` // ignore interfaces
|
||||
|
||||
|
||||
+21
-34
@@ -20,11 +20,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
@@ -52,10 +50,7 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverCon
|
||||
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
if globalEtcdClient != nil {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
_, err = globalEtcdClient.Put(timeoutCtx, configFile, string(data))
|
||||
defer cancel()
|
||||
return err
|
||||
return saveConfigEtcd(ctx, globalEtcdClient, configFile, data)
|
||||
}
|
||||
|
||||
// Create a backup of the current config
|
||||
@@ -162,20 +157,27 @@ func initConfig(objAPI ObjectLayer) error {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
if globalEtcdClient != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||
cancel()
|
||||
if err == nil && resp.Count > 0 {
|
||||
if err = migrateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Migrates etcd ${HOME}/.minio/config.json to '/config/config.json'
|
||||
if err := migrateConfigToMinioSys(objAPI); err != nil {
|
||||
if globalEtcdClient != nil {
|
||||
if err := checkConfigEtcd(context.Background(), globalEtcdClient, getConfigFile()); err != nil {
|
||||
if err == errConfigNotFound {
|
||||
// Migrates all configs at old location.
|
||||
if err = migrateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Migrates etcd ${HOME}/.minio/config.json to '/config/config.json'
|
||||
if err = migrateConfigToMinioSys(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Watch config for changes and reloads them.
|
||||
go watchConfigEtcd(objAPI, configFile, loadConfig)
|
||||
|
||||
} else {
|
||||
if isFile(getConfigFile()) {
|
||||
if err := migrateConfig(); err != nil {
|
||||
@@ -185,30 +187,15 @@ func initConfig(objAPI ObjectLayer) error {
|
||||
// Migrates ${HOME}/.minio/config.json or config.json.deprecated
|
||||
// to '<export_path>/.minio.sys/config/config.json'
|
||||
// ignore if the file doesn't exist.
|
||||
if err := migrateConfigToMinioSys(objAPI); err != nil && !os.IsNotExist(err) {
|
||||
if err := migrateConfigToMinioSys(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Watch config for changes and reloads them in-memory.
|
||||
go watchConfig(objAPI, configFile, loadConfig)
|
||||
|
||||
if err := checkConfig(context.Background(), objAPI, configFile); err != nil {
|
||||
if err == errConfigNotFound {
|
||||
// Config file does not exist, we create it fresh and return upon success.
|
||||
if err = newSrvConfig(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Migrates backend '<export_path>/.minio.sys/config/config.json' to latest version.
|
||||
if err := migrateMinioSysConfig(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := migrateMinioSysConfig(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return loadConfig(objAPI)
|
||||
}
|
||||
|
||||
+60
-20
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
vault "github.com/hashicorp/vault/api"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -63,6 +64,7 @@ type vaultService struct {
|
||||
config *VaultConfig
|
||||
client *vault.Client
|
||||
leaseDuration time.Duration
|
||||
tokenRenewer *vault.Renewer
|
||||
}
|
||||
|
||||
var _ KMS = (*vaultService)(nil) // compiler check that *vaultService implements KMS
|
||||
@@ -120,42 +122,80 @@ func NewVault(config VaultConfig) (KMS, error) {
|
||||
if config.Namespace != "" {
|
||||
client.SetNamespace(config.Namespace)
|
||||
}
|
||||
v := &vaultService{client: client, config: &config}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"role_id": config.Auth.AppRole.ID,
|
||||
"secret_id": config.Auth.AppRole.Secret,
|
||||
}
|
||||
resp, err := client.Logical().Write("auth/approle/login", payload)
|
||||
if err != nil {
|
||||
if err := v.authenticate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Auth == nil {
|
||||
return nil, ErrKMSAuthLogin
|
||||
}
|
||||
|
||||
client.SetToken(resp.Auth.ClientToken)
|
||||
v := &vaultService{client: client, config: &config, leaseDuration: time.Duration(resp.Auth.LeaseDuration)}
|
||||
v.renewToken()
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// renewToken starts a new go-routine which renews
|
||||
// the vault authentication token periodically.
|
||||
func (v *vaultService) renewToken() {
|
||||
// reauthenticate() tries to login in 1 minute
|
||||
// intervals until successful.
|
||||
func (v *vaultService) reauthenticate() {
|
||||
retryDelay := 1 * time.Minute
|
||||
go func() {
|
||||
for {
|
||||
s, err := v.client.Auth().Token().RenewSelf(int(v.leaseDuration))
|
||||
if err != nil {
|
||||
if err := v.authenticate(); err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
}
|
||||
nextRenew := s.Auth.LeaseDuration / 2
|
||||
time.Sleep(time.Duration(nextRenew) * time.Second)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// renewer calls vault client's renewer that automatically
|
||||
// renews secret periodically
|
||||
func (v *vaultService) renewer(secret *vault.Secret) {
|
||||
renewer, err := v.client.NewRenewer(&vault.RenewerInput{
|
||||
Secret: secret,
|
||||
})
|
||||
if err != nil {
|
||||
logger.FatalIf(err, "crypto: hashicorp vault token renewer could not be started")
|
||||
}
|
||||
v.tokenRenewer = renewer
|
||||
go renewer.Renew()
|
||||
defer renewer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-renewer.DoneCh():
|
||||
if err != nil {
|
||||
v.reauthenticate()
|
||||
renewer.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Renewal is now over
|
||||
case renewal := <-renewer.RenewCh():
|
||||
v.leaseDuration = time.Duration(renewal.Secret.Auth.LeaseDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// authenticate logs the app to vault, and starts the auto renewer
|
||||
// before secret expires
|
||||
func (v *vaultService) authenticate() (err error) {
|
||||
payload := map[string]interface{}{
|
||||
"role_id": v.config.Auth.AppRole.ID,
|
||||
"secret_id": v.config.Auth.AppRole.Secret,
|
||||
}
|
||||
var secret *vault.Secret
|
||||
secret, err = v.client.Logical().Write("auth/approle/login", payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if secret.Auth == nil {
|
||||
err = ErrKMSAuthLogin
|
||||
return
|
||||
}
|
||||
v.client.SetToken(secret.Auth.ClientToken)
|
||||
v.leaseDuration = time.Duration(secret.Auth.LeaseDuration)
|
||||
go v.renewer(secret)
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateKey returns a new plaintext key, generated by the KMS,
|
||||
// and a sealed version of this plaintext key encrypted using the
|
||||
// named key referenced by keyID. It also binds the generated key
|
||||
|
||||
+60
-2
@@ -29,7 +29,9 @@ import (
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/mem"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
)
|
||||
|
||||
@@ -198,9 +200,65 @@ func (endpoints EndpointList) GetString(i int) string {
|
||||
return endpoints[i].String()
|
||||
}
|
||||
|
||||
// localEndpointsPerf - returns ServerDrivesPerfInfo for only the
|
||||
// localEndpointsMemUsage - returns ServerMemUsageInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsPerf(endpoints EndpointList) ServerDrivesPerfInfo {
|
||||
func localEndpointsMemUsage(endpoints EndpointList) ServerMemUsageInfo {
|
||||
var memUsages []mem.Usage
|
||||
var historicUsages []mem.Usage
|
||||
var addr string
|
||||
scratchSpace := map[string]bool{}
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, ok := scratchSpace[endpoint.Host]; ok {
|
||||
continue
|
||||
}
|
||||
addr = GetLocalPeer(endpoints)
|
||||
memUsage := mem.GetUsage()
|
||||
memUsages = append(memUsages, memUsage)
|
||||
historicUsage := mem.GetHistoricUsage()
|
||||
historicUsages = append(historicUsages, historicUsage)
|
||||
scratchSpace[endpoint.Host] = true
|
||||
}
|
||||
}
|
||||
return ServerMemUsageInfo{
|
||||
Addr: addr,
|
||||
Usage: memUsages,
|
||||
HistoricUsage: historicUsages,
|
||||
}
|
||||
}
|
||||
|
||||
// localEndpointsCPULoad - returns ServerCPULoadInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsCPULoad(endpoints EndpointList) ServerCPULoadInfo {
|
||||
var cpuLoads []cpu.Load
|
||||
var historicLoads []cpu.Load
|
||||
var addr string
|
||||
scratchSpace := map[string]bool{}
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, ok := scratchSpace[endpoint.Host]; ok {
|
||||
continue
|
||||
}
|
||||
addr = GetLocalPeer(endpoints)
|
||||
cpuLoad := cpu.GetLoad()
|
||||
cpuLoads = append(cpuLoads, cpuLoad)
|
||||
historicLoad := cpu.GetHistoricLoad()
|
||||
historicLoads = append(historicLoads, historicLoad)
|
||||
scratchSpace[endpoint.Host] = true
|
||||
}
|
||||
}
|
||||
return ServerCPULoadInfo{
|
||||
Addr: addr,
|
||||
Load: cpuLoads,
|
||||
HistoricLoad: historicLoads,
|
||||
}
|
||||
}
|
||||
|
||||
// localEndpointsDrivePerf - returns ServerDrivesPerfInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsDrivePerf(endpoints EndpointList) ServerDrivesPerfInfo {
|
||||
var dps []disk.Performance
|
||||
var addr string
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
+69
-64
@@ -19,29 +19,30 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// Reads in parallel from bitrotReaders.
|
||||
// Reads in parallel from readers.
|
||||
type parallelReader struct {
|
||||
readers []*bitrotReader
|
||||
readers []io.ReaderAt
|
||||
dataBlocks int
|
||||
offset int64
|
||||
shardSize int64
|
||||
shardFileSize int64
|
||||
buf [][]byte
|
||||
}
|
||||
|
||||
// newParallelReader returns parallelReader.
|
||||
func newParallelReader(readers []*bitrotReader, dataBlocks int, offset int64, fileSize int64, blocksize int64) *parallelReader {
|
||||
shardSize := ceilFrac(blocksize, int64(dataBlocks))
|
||||
shardFileSize := getErasureShardFileSize(blocksize, fileSize, dataBlocks)
|
||||
func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int64) *parallelReader {
|
||||
return ¶llelReader{
|
||||
readers,
|
||||
dataBlocks,
|
||||
(offset / blocksize) * shardSize,
|
||||
shardSize,
|
||||
shardFileSize,
|
||||
e.dataBlocks,
|
||||
(offset / e.blockSize) * e.ShardSize(),
|
||||
e.ShardSize(),
|
||||
e.ShardFileSize(totalLength),
|
||||
make([][]byte, len(readers)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,79 +57,83 @@ func (p *parallelReader) canDecode(buf [][]byte) bool {
|
||||
return bufCount >= p.dataBlocks
|
||||
}
|
||||
|
||||
// Read reads from bitrotReaders in parallel. Returns p.dataBlocks number of bufs.
|
||||
// Read reads from readers in parallel. Returns p.dataBlocks number of bufs.
|
||||
func (p *parallelReader) Read() ([][]byte, error) {
|
||||
type errIdx struct {
|
||||
idx int
|
||||
buf []byte
|
||||
err error
|
||||
}
|
||||
|
||||
errCh := make(chan errIdx)
|
||||
currReaderIndex := 0
|
||||
newBuf := make([][]byte, len(p.readers))
|
||||
var newBufLK sync.RWMutex
|
||||
|
||||
if p.offset+p.shardSize > p.shardFileSize {
|
||||
p.shardSize = p.shardFileSize - p.offset
|
||||
}
|
||||
|
||||
read := func(currReaderIndex int) {
|
||||
b, err := p.readers[currReaderIndex].ReadChunk(p.offset, p.shardSize)
|
||||
errCh <- errIdx{currReaderIndex, b, err}
|
||||
readTriggerCh := make(chan bool, len(p.readers))
|
||||
for i := 0; i < p.dataBlocks; i++ {
|
||||
// Setup read triggers for p.dataBlocks number of reads so that it reads in parallel.
|
||||
readTriggerCh <- true
|
||||
}
|
||||
|
||||
readerCount := 0
|
||||
for _, r := range p.readers {
|
||||
if r != nil {
|
||||
readerCount++
|
||||
}
|
||||
}
|
||||
if readerCount < p.dataBlocks {
|
||||
return nil, errXLReadQuorum
|
||||
}
|
||||
|
||||
readerCount = 0
|
||||
for i, r := range p.readers {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
go read(i)
|
||||
readerCount++
|
||||
if readerCount == p.dataBlocks {
|
||||
currReaderIndex = i + 1
|
||||
readerIndex := 0
|
||||
var wg sync.WaitGroup
|
||||
// if readTrigger is true, it implies next disk.ReadAt() should be tried
|
||||
// if readTrigger is false, it implies previous disk.ReadAt() was successful and there is no need
|
||||
// to try reading the next disk.
|
||||
for readTrigger := range readTriggerCh {
|
||||
newBufLK.RLock()
|
||||
canDecode := p.canDecode(newBuf)
|
||||
newBufLK.RUnlock()
|
||||
if canDecode {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for errVal := range errCh {
|
||||
if errVal.err == nil {
|
||||
newBuf[errVal.idx] = errVal.buf
|
||||
if p.canDecode(newBuf) {
|
||||
p.offset += int64(p.shardSize)
|
||||
return newBuf, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
p.readers[errVal.idx] = nil
|
||||
for currReaderIndex < len(p.readers) {
|
||||
if p.readers[currReaderIndex] != nil {
|
||||
break
|
||||
}
|
||||
currReaderIndex++
|
||||
}
|
||||
|
||||
if currReaderIndex == len(p.readers) {
|
||||
if readerIndex == len(p.readers) {
|
||||
break
|
||||
}
|
||||
go read(currReaderIndex)
|
||||
currReaderIndex++
|
||||
if !readTrigger {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
disk := p.readers[i]
|
||||
if disk == nil {
|
||||
// Since disk is nil, trigger another read.
|
||||
readTriggerCh <- true
|
||||
return
|
||||
}
|
||||
if p.buf[i] == nil {
|
||||
// Reading first time on this disk, hence the buffer needs to be allocated.
|
||||
// Subsequent reads will re-use this buffer.
|
||||
p.buf[i] = make([]byte, p.shardSize)
|
||||
}
|
||||
// For the last shard, the shardsize might be less than previous shard sizes.
|
||||
// Hence the following statement ensures that the buffer size is reset to the right size.
|
||||
p.buf[i] = p.buf[i][:p.shardSize]
|
||||
_, err := disk.ReadAt(p.buf[i], p.offset)
|
||||
if err != nil {
|
||||
p.readers[i] = nil
|
||||
// Since ReadAt returned error, trigger another read.
|
||||
readTriggerCh <- true
|
||||
return
|
||||
}
|
||||
newBufLK.Lock()
|
||||
newBuf[i] = p.buf[i]
|
||||
newBufLK.Unlock()
|
||||
// Since ReadAt returned success, there is no need to trigger another read.
|
||||
readTriggerCh <- false
|
||||
}(readerIndex)
|
||||
readerIndex++
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if p.canDecode(newBuf) {
|
||||
p.offset += p.shardSize
|
||||
return newBuf, nil
|
||||
}
|
||||
|
||||
return nil, errXLReadQuorum
|
||||
}
|
||||
|
||||
// Decode reads from readers, reconstructs data if needed and writes the data to the writer.
|
||||
func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []*bitrotReader, offset, length, totalLength int64) error {
|
||||
func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.ReaderAt, offset, length, totalLength int64) error {
|
||||
if offset < 0 || length < 0 {
|
||||
logger.LogIf(ctx, errInvalidArgument)
|
||||
return errInvalidArgument
|
||||
@@ -141,7 +146,7 @@ func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []*bitrot
|
||||
return nil
|
||||
}
|
||||
|
||||
reader := newParallelReader(readers, e.dataBlocks, offset, totalLength, e.blockSize)
|
||||
reader := newParallelReader(readers, e, offset, totalLength)
|
||||
|
||||
startBlock := offset / e.blockSize
|
||||
endBlock := (offset + length) / e.blockSize
|
||||
|
||||
+59
-40
@@ -41,26 +41,28 @@ var erasureDecodeTests = []struct {
|
||||
algorithm BitrotAlgorithm
|
||||
shouldFail, shouldFailQuorum bool
|
||||
}{
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 1, length: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte, length: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 4
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 3, length: 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 5
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 4, length: 8 * 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 6
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, length: 1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 7
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 6, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 8
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 5, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 9
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 10
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 3, blocksize: int64(2 * oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 6, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 16
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 2, blocksize: int64(blockSizeV1), data: 2 * oneMiByte, offset: oneMiByte, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 18
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 3, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 19
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 1, length: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte, length: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false},
|
||||
// 4
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 3, length: 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 5
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 4, length: 8 * 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 6
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, length: 1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 7
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 6, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 8
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 5, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 9
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 10
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 3, blocksize: int64(2 * oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 6, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 16
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 2, blocksize: int64(blockSizeV1), data: 2 * oneMiByte, offset: oneMiByte, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 18
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 3, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false},
|
||||
// 19
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 21
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 9, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 22
|
||||
@@ -104,11 +106,12 @@ func TestErasureDecode(t *testing.T) {
|
||||
writeAlgorithm = DefaultBitrotAlgorithm
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
writers := make([]io.Writer, len(disks))
|
||||
for i, disk := range disks {
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", writeAlgorithm)
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(test.data), writeAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
n, err := erasure.Encode(context.Background(), bytes.NewReader(data[:]), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
||||
@@ -124,17 +127,19 @@ func TestErasureDecode(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get the checksums of the current part.
|
||||
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||
bitrotReaders := make([]io.ReaderAt, len(disks))
|
||||
for index, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||
tillOffset := erasure.ShardFileTillOffset(test.offset, test.length, test.data)
|
||||
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", tillOffset, writeAlgorithm, bitrotWriterSum(writers[index]), erasure.ShardSize())
|
||||
}
|
||||
|
||||
writer := bytes.NewBuffer(nil)
|
||||
err = erasure.Decode(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||
closeBitrotReaders(bitrotReaders)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
@@ -143,31 +148,41 @@ func TestErasureDecode(t *testing.T) {
|
||||
}
|
||||
if err == nil {
|
||||
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
||||
t.Errorf("Test %d: read retruns wrong file content", i)
|
||||
t.Errorf("Test %d: read retruns wrong file content.", i)
|
||||
}
|
||||
}
|
||||
|
||||
for i, r := range bitrotReaders {
|
||||
if r == nil {
|
||||
disks[i] = OfflineDisk
|
||||
}
|
||||
}
|
||||
if err == nil && !test.shouldFail {
|
||||
bitrotReaders = make([]*bitrotReader, len(disks))
|
||||
bitrotReaders = make([]io.ReaderAt, len(disks))
|
||||
for index, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||
tillOffset := erasure.ShardFileTillOffset(test.offset, test.length, test.data)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", tillOffset, writeAlgorithm, bitrotWriterSum(writers[index]), erasure.ShardSize())
|
||||
}
|
||||
for j := range disks[:test.offDisks] {
|
||||
bitrotReaders[j].disk = badDisk{nil}
|
||||
if bitrotReaders[j] == nil {
|
||||
continue
|
||||
}
|
||||
switch r := bitrotReaders[j].(type) {
|
||||
case *wholeBitrotReader:
|
||||
r.disk = badDisk{nil}
|
||||
case *streamingBitrotReader:
|
||||
r.disk = badDisk{nil}
|
||||
}
|
||||
}
|
||||
if test.offDisks > 0 {
|
||||
bitrotReaders[0] = nil
|
||||
}
|
||||
writer.Reset()
|
||||
err = erasure.Decode(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||
closeBitrotReaders(bitrotReaders)
|
||||
if err != nil && !test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
@@ -213,12 +228,12 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
writers := make([]io.Writer, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(length), DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
|
||||
// 10000 iterations with random offsets and lengths.
|
||||
@@ -227,6 +242,7 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) {
|
||||
// Create a test file to read from.
|
||||
buffer := make([]byte, blockSize, 2*blockSize)
|
||||
n, err := erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -247,15 +263,16 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) {
|
||||
expected := data[offset : offset+readLen]
|
||||
|
||||
// Get the checksums of the current part.
|
||||
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||
bitrotReaders := make([]io.ReaderAt, len(disks))
|
||||
for index, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(offset, readLen, length, blockSize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||
tillOffset := erasure.ShardFileTillOffset(offset, readLen, length)
|
||||
bitrotReaders[index] = newStreamingBitrotReader(disk, "testbucket", "object", tillOffset, DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
err = erasure.Decode(context.Background(), buf, bitrotReaders, offset, readLen, length)
|
||||
closeBitrotReaders(bitrotReaders)
|
||||
if err != nil {
|
||||
t.Fatal(err, offset, readLen)
|
||||
}
|
||||
@@ -281,17 +298,18 @@ func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64,
|
||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
}
|
||||
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
writers := make([]io.Writer, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
|
||||
content := make([]byte, size)
|
||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||
_, err = erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create erasure test file: %v", err)
|
||||
}
|
||||
@@ -307,17 +325,18 @@ func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64,
|
||||
b.SetBytes(size)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||
bitrotReaders := make([]io.ReaderAt, len(disks))
|
||||
for index, disk := range disks {
|
||||
if writers[index] == nil {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(0, size, size, erasure.blockSize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||
tillOffset := erasure.ShardFileTillOffset(0, size, size)
|
||||
bitrotReaders[index] = newStreamingBitrotReader(disk, "testbucket", "object", tillOffset, DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
if err = erasure.Decode(context.Background(), bytes.NewBuffer(content[:0]), bitrotReaders, 0, size, size); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
closeBitrotReaders(bitrotReaders)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// Writes in parallel to bitrotWriters
|
||||
// Writes in parallel to writers
|
||||
type parallelWriter struct {
|
||||
writers []*bitrotWriter
|
||||
writers []io.Writer
|
||||
writeQuorum int
|
||||
errs []error
|
||||
}
|
||||
|
||||
// Append appends data to bitrotWriters in parallel.
|
||||
func (p *parallelWriter) Append(ctx context.Context, blocks [][]byte) error {
|
||||
// Write writes data to writers in parallel.
|
||||
func (p *parallelWriter) Write(ctx context.Context, blocks [][]byte) error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range p.writers {
|
||||
@@ -45,7 +45,7 @@ func (p *parallelWriter) Append(ctx context.Context, blocks [][]byte) error {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
p.errs[i] = p.writers[i].Append(blocks[i])
|
||||
_, p.errs[i] = p.writers[i].Write(blocks[i])
|
||||
if p.errs[i] != nil {
|
||||
p.writers[i] = nil
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func (p *parallelWriter) Append(ctx context.Context, blocks [][]byte) error {
|
||||
}
|
||||
|
||||
// Encode reads from the reader, erasure-encodes the data and writes to the writers.
|
||||
func (e *Erasure) Encode(ctx context.Context, src io.Reader, writers []*bitrotWriter, buf []byte, quorum int) (total int64, err error) {
|
||||
func (e *Erasure) Encode(ctx context.Context, src io.Reader, writers []io.Writer, buf []byte, quorum int) (total int64, err error) {
|
||||
writer := ¶llelWriter{
|
||||
writers: writers,
|
||||
writeQuorum: quorum,
|
||||
@@ -96,7 +96,7 @@ func (e *Erasure) Encode(ctx context.Context, src io.Reader, writers []*bitrotWr
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err = writer.Append(ctx, blocks); err != nil {
|
||||
if err = writer.Write(ctx, blocks); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -36,6 +36,14 @@ func (a badDisk) AppendFile(volume string, path string, buf []byte) error {
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
func (a badDisk) ReadFileStream(volume, path string, offset, length int64) (io.ReadCloser, error) {
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
|
||||
func (a badDisk) CreateFile(volume, path string, size int64, reader io.Reader) error {
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
const oneMiByte = 1 * humanize.MiByte
|
||||
|
||||
var erasureEncodeTests = []struct {
|
||||
@@ -87,14 +95,15 @@ func TestErasureEncode(t *testing.T) {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||
}
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
writers := make([]io.Writer, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", test.algorithm)
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(int64(len(data[test.offset:]))), test.algorithm, erasure.ShardSize())
|
||||
}
|
||||
n, err := erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
@@ -110,20 +119,26 @@ func TestErasureEncode(t *testing.T) {
|
||||
if length := int64(len(data[test.offset:])); n != length {
|
||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
|
||||
}
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
writers := make([]io.Writer, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object2", test.algorithm)
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object2", erasure.ShardFileSize(int64(len(data[test.offset:]))), test.algorithm, erasure.ShardSize())
|
||||
}
|
||||
for j := range disks[:test.offDisks] {
|
||||
writers[j].disk = badDisk{nil}
|
||||
switch w := writers[j].(type) {
|
||||
case *wholeBitrotWriter:
|
||||
w.disk = badDisk{nil}
|
||||
case *streamingBitrotWriter:
|
||||
w.iow.CloseWithError(errFaultyDisk)
|
||||
}
|
||||
}
|
||||
if test.offDisks > 0 {
|
||||
writers[0] = nil
|
||||
}
|
||||
n, err = erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil && !test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
@@ -167,14 +182,16 @@ func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64,
|
||||
b.SetBytes(size)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
writers := make([]io.Writer, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||
disk.DeleteFile("testbucket", "object")
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
_, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
// Heal heals the shard files on non-nil writers. Note that the quorum passed is 1
|
||||
// as healing should continue even if it has been successful healing only one shard file.
|
||||
func (e Erasure) Heal(ctx context.Context, readers []*bitrotReader, writers []*bitrotWriter, size int64) error {
|
||||
func (e Erasure) Heal(ctx context.Context, readers []io.ReaderAt, writers []io.Writer, size int64) error {
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
if err := e.Decode(ctx, w, readers, 0, size, size); err != nil {
|
||||
|
||||
+20
-10
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -84,20 +85,21 @@ func TestErasureHeal(t *testing.T) {
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
writers := make([]io.Writer, len(disks))
|
||||
for i, disk := range disks {
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "testobject", erasure.ShardFileSize(test.size), test.algorithm, erasure.ShardSize())
|
||||
}
|
||||
_, err = erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
|
||||
readers := make([]*bitrotReader, len(disks))
|
||||
readers := make([]io.ReaderAt, len(disks))
|
||||
for i, disk := range disks {
|
||||
shardFilesize := getErasureShardFileSize(test.blocksize, test.size, erasure.dataBlocks)
|
||||
readers[i] = newBitrotReader(disk, "testbucket", "testobject", test.algorithm, shardFilesize, writers[i].Sum())
|
||||
shardFilesize := erasure.ShardFileSize(test.size)
|
||||
readers[i] = newBitrotReader(disk, "testbucket", "testobject", shardFilesize, test.algorithm, bitrotWriterSum(writers[i]), erasure.ShardSize())
|
||||
}
|
||||
|
||||
// setup stale disks for the test case
|
||||
@@ -111,22 +113,30 @@ func TestErasureHeal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for j := 0; j < test.badDisks; j++ {
|
||||
readers[test.offDisks+j].disk = badDisk{nil}
|
||||
switch r := readers[test.offDisks+j].(type) {
|
||||
case *streamingBitrotReader:
|
||||
r.disk = badDisk{nil}
|
||||
case *wholeBitrotReader:
|
||||
r.disk = badDisk{nil}
|
||||
}
|
||||
}
|
||||
for j := 0; j < test.badStaleDisks; j++ {
|
||||
staleDisks[j] = badDisk{nil}
|
||||
}
|
||||
|
||||
staleWriters := make([]*bitrotWriter, len(staleDisks))
|
||||
staleWriters := make([]io.Writer, len(staleDisks))
|
||||
for i, disk := range staleDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
staleWriters[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
||||
os.Remove(pathJoin(disk.String(), "testbucket", "testobject"))
|
||||
staleWriters[i] = newBitrotWriter(disk, "testbucket", "testobject", erasure.ShardFileSize(test.size), test.algorithm, erasure.ShardSize())
|
||||
}
|
||||
|
||||
// test case setup is complete - now call Healfile()
|
||||
// test case setup is complete - now call Heal()
|
||||
err = erasure.Heal(context.Background(), readers, staleWriters, test.size)
|
||||
closeBitrotReaders(readers)
|
||||
closeBitrotWriters(staleWriters)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but it failed with: %v", i, err)
|
||||
}
|
||||
@@ -140,7 +150,7 @@ func TestErasureHeal(t *testing.T) {
|
||||
if staleWriters[i] == nil {
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(staleWriters[i].Sum(), writers[i].Sum()) {
|
||||
if !bytes.Equal(bitrotWriterSum(staleWriters[i]), bitrotWriterSum(writers[i])) {
|
||||
t.Errorf("Test %d: heal returned different bitrot checksums", i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,25 +109,3 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
|
||||
// Success.
|
||||
return totalWritten, nil
|
||||
}
|
||||
|
||||
// Returns shard-file size.
|
||||
func getErasureShardFileSize(blockSize int64, totalLength int64, dataBlocks int) int64 {
|
||||
shardSize := ceilFrac(int64(blockSize), int64(dataBlocks))
|
||||
numShards := totalLength / int64(blockSize)
|
||||
lastBlockSize := totalLength % int64(blockSize)
|
||||
lastShardSize := ceilFrac(lastBlockSize, int64(dataBlocks))
|
||||
return shardSize*numShards + lastShardSize
|
||||
}
|
||||
|
||||
// Returns the endOffset till which bitrotReader should read data using disk.ReadFile()
|
||||
// partOffset, partLength and partSize are values of the object's part file.
|
||||
func getErasureShardFileEndOffset(partOffset int64, partLength int64, partSize int64, erasureBlockSize int64, dataBlocks int) int64 {
|
||||
shardSize := ceilFrac(erasureBlockSize, int64(dataBlocks))
|
||||
shardFileSize := getErasureShardFileSize(erasureBlockSize, partSize, dataBlocks)
|
||||
endShard := (partOffset + int64(partLength)) / erasureBlockSize
|
||||
endOffset := endShard*shardSize + shardSize
|
||||
if endOffset > shardFileSize {
|
||||
endOffset = shardFileSize
|
||||
}
|
||||
return endOffset
|
||||
}
|
||||
|
||||
+33
-7
@@ -32,18 +32,16 @@ type Erasure struct {
|
||||
|
||||
// NewErasure creates a new ErasureStorage.
|
||||
func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
|
||||
shardsize := int(ceilFrac(blockSize, int64(dataBlocks)))
|
||||
erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return e, err
|
||||
}
|
||||
e = Erasure{
|
||||
encoder: erasure,
|
||||
dataBlocks: dataBlocks,
|
||||
parityBlocks: parityBlocks,
|
||||
blockSize: blockSize,
|
||||
}
|
||||
e.encoder, err = reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(int(e.ShardSize())))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return e, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,3 +92,31 @@ func (e *Erasure) DecodeDataAndParityBlocks(ctx context.Context, data [][]byte)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShardSize - returns actual shared size from erasure blockSize.
|
||||
func (e *Erasure) ShardSize() int64 {
|
||||
return ceilFrac(e.blockSize, int64(e.dataBlocks))
|
||||
}
|
||||
|
||||
// ShardFileSize - returns final erasure size from original size.
|
||||
func (e *Erasure) ShardFileSize(totalLength int64) int64 {
|
||||
if totalLength == 0 {
|
||||
return 0
|
||||
}
|
||||
numShards := totalLength / e.blockSize
|
||||
lastBlockSize := totalLength % int64(e.blockSize)
|
||||
lastShardSize := ceilFrac(lastBlockSize, int64(e.dataBlocks))
|
||||
return numShards*e.ShardSize() + lastShardSize
|
||||
}
|
||||
|
||||
// ShardFileTillOffset - returns the effectiv eoffset where erasure reading begins.
|
||||
func (e *Erasure) ShardFileTillOffset(startOffset, length, totalLength int64) int64 {
|
||||
shardSize := e.ShardSize()
|
||||
shardFileSize := e.ShardFileSize(totalLength)
|
||||
endShard := (startOffset + int64(length)) / e.blockSize
|
||||
tillOffset := endShard*shardSize + shardSize
|
||||
if tillOffset > shardFileSize {
|
||||
tillOffset = shardFileSize
|
||||
}
|
||||
return tillOffset
|
||||
}
|
||||
|
||||
@@ -896,6 +896,7 @@ func newHealFormatSets(refFormat *formatXLV3, setCount, disksPerSet int, formats
|
||||
if errs[i*disksPerSet+j] == errUnformattedDisk || errs[i*disksPerSet+j] == nil {
|
||||
newFormats[i][j] = &formatXLV3{}
|
||||
newFormats[i][j].Version = refFormat.Version
|
||||
newFormats[i][j].ID = refFormat.ID
|
||||
newFormats[i][j].Format = refFormat.Format
|
||||
newFormats[i][j].XL.Version = refFormat.XL.Version
|
||||
newFormats[i][j].XL.DistributionAlgo = refFormat.XL.DistributionAlgo
|
||||
|
||||
@@ -556,4 +556,13 @@ func TestNewFormatSets(t *testing.T) {
|
||||
if newFormats == nil {
|
||||
t.Fatal("Unexpected failure")
|
||||
}
|
||||
|
||||
// Check if deployment IDs are preserved.
|
||||
for i := range newFormats {
|
||||
for j := range newFormats[i] {
|
||||
if newFormats[i][j].ID != quorumFormat.ID {
|
||||
t.Fatal("Deployment id in the new format is lost")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,17 +34,6 @@ var (
|
||||
// MustGetUUID function alias.
|
||||
MustGetUUID = mustGetUUID
|
||||
|
||||
// IsMinAllowedPartSize function alias.
|
||||
IsMinAllowedPartSize = isMinAllowedPartSize
|
||||
|
||||
// GetCompleteMultipartMD5 functon alias.
|
||||
GetCompleteMultipartMD5 = getCompleteMultipartMD5
|
||||
|
||||
// Contains function alias.
|
||||
Contains = contains
|
||||
|
||||
// ExtractETag provides extractETag function alias.
|
||||
ExtractETag = extractETag
|
||||
// CleanMetadataKeys provides cleanMetadataKeys function alias.
|
||||
CleanMetadataKeys = cleanMetadataKeys
|
||||
)
|
||||
|
||||
+9
-15
@@ -155,9 +155,12 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
registerSTSRouter(router)
|
||||
}
|
||||
|
||||
enableConfigOps := globalEtcdClient != nil && gatewayName == "nas"
|
||||
enableIAMOps := globalEtcdClient != nil
|
||||
|
||||
// Enable IAM admin APIs if etcd is enabled, if not just enable basic
|
||||
// operations such as profiling, server info etc.
|
||||
registerAdminRouter(router, globalEtcdClient != nil)
|
||||
registerAdminRouter(router, enableConfigOps, enableIAMOps)
|
||||
|
||||
// Add healthcheck router
|
||||
registerHealthCheckRouter(router)
|
||||
@@ -176,15 +179,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// Add API router.
|
||||
registerAPIRouter(router, encryptionEnabled)
|
||||
|
||||
// Dummy endpoint representing gateway instance.
|
||||
globalEndpoints = []Endpoint{{
|
||||
URL: &url.URL{Path: "/minio/gateway"},
|
||||
IsLocal: true,
|
||||
}}
|
||||
|
||||
// Initialize Admin Peers.
|
||||
initGlobalAdminPeers(globalEndpoints)
|
||||
|
||||
var getCert certs.GetCertificateFunc
|
||||
if globalTLSCerts != nil {
|
||||
getCert = globalTLSCerts.GetCertificate
|
||||
@@ -224,12 +218,12 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||
}
|
||||
|
||||
if globalEtcdClient != nil && gatewayName == "nas" {
|
||||
if enableConfigOps {
|
||||
// Create a new config system.
|
||||
globalConfigSys = NewConfigSys()
|
||||
|
||||
// Load globalServerConfig from etcd
|
||||
_ = globalConfigSys.Init(newObject)
|
||||
logger.LogIf(context.Background(), globalConfigSys.Init(newObject))
|
||||
}
|
||||
|
||||
// Load logger subsystem
|
||||
@@ -251,9 +245,9 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
|
||||
// Create new IAM system.
|
||||
globalIAMSys = NewIAMSys()
|
||||
if globalEtcdClient != nil {
|
||||
if enableIAMOps {
|
||||
// Initialize IAM sys.
|
||||
_ = globalIAMSys.Init(newObject)
|
||||
logger.LogIf(context.Background(), globalIAMSys.Init(newObject))
|
||||
}
|
||||
|
||||
// Create new policy system.
|
||||
@@ -265,7 +259,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// Create new notification system.
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||
if globalEtcdClient != nil && newObject.IsNotificationSupported() {
|
||||
_ = globalNotificationSys.Init(newObject)
|
||||
logger.LogIf(context.Background(), globalNotificationSys.Init(newObject))
|
||||
}
|
||||
|
||||
// Encryption support checks in gateway mode.
|
||||
|
||||
@@ -1,850 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017, 2018 Minio, Inc.
|
||||
*
|
||||
* 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 b2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
b2 "github.com/minio/blazer/base"
|
||||
"github.com/minio/cli"
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
h2 "github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/policy/condition"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
// Supported bucket types by B2 backend.
|
||||
const (
|
||||
bucketTypePrivate = "allPrivate"
|
||||
bucketTypeReadOnly = "allPublic"
|
||||
b2Backend = "b2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const b2GatewayTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}} {{if .VisibleFlags}}[FLAGS]{{end}}
|
||||
{{if .VisibleFlags}}
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
ACCESS:
|
||||
MINIO_ACCESS_KEY: B2 account id.
|
||||
MINIO_SECRET_KEY: B2 application key.
|
||||
|
||||
BROWSER:
|
||||
MINIO_BROWSER: To disable web browser access, set this value to "off".
|
||||
|
||||
DOMAIN:
|
||||
MINIO_DOMAIN: To enable virtual-host-style requests, set this value to Minio host domain name.
|
||||
|
||||
CACHE:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for B2 backend.
|
||||
$ export MINIO_ACCESS_KEY=accountID
|
||||
$ export MINIO_SECRET_KEY=applicationKey
|
||||
$ {{.HelpName}}
|
||||
|
||||
2. Start minio gateway server for B2 backend with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=accountID
|
||||
$ export MINIO_SECRET_KEY=applicationKey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
Name: b2Backend,
|
||||
Usage: "Backblaze B2",
|
||||
Action: b2GatewayMain,
|
||||
CustomHelpTemplate: b2GatewayTemplate,
|
||||
HideHelpCommand: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Handler for 'minio gateway b2' command line.
|
||||
func b2GatewayMain(ctx *cli.Context) {
|
||||
minio.StartGateway(ctx, &B2{})
|
||||
}
|
||||
|
||||
// B2 implements Minio Gateway
|
||||
type B2 struct{}
|
||||
|
||||
// Name implements Gateway interface.
|
||||
func (g *B2) Name() string {
|
||||
return b2Backend
|
||||
}
|
||||
|
||||
// NewGatewayLayer returns b2 gateway layer, implements ObjectLayer interface to
|
||||
// talk to B2 remote backend.
|
||||
func (g *B2) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
||||
ctx := context.Background()
|
||||
client, err := b2.AuthorizeAccount(ctx, creds.AccessKey, creds.SecretKey, b2.Transport(minio.NewCustomHTTPTransport()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &b2Objects{
|
||||
creds: creds,
|
||||
b2Client: client,
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Production - Ready for production use.
|
||||
func (g *B2) Production() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// b2Object implements gateway for Minio and BackBlaze B2 compatible object storage servers.
|
||||
type b2Objects struct {
|
||||
minio.GatewayUnsupported
|
||||
mu sync.Mutex
|
||||
creds auth.Credentials
|
||||
b2Client *b2.B2
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// Convert B2 errors to minio object layer errors.
|
||||
func b2ToObjectError(err error, params ...string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
bucket := ""
|
||||
object := ""
|
||||
uploadID := ""
|
||||
if len(params) >= 1 {
|
||||
bucket = params[0]
|
||||
}
|
||||
if len(params) == 2 {
|
||||
object = params[1]
|
||||
}
|
||||
if len(params) == 3 {
|
||||
uploadID = params[2]
|
||||
}
|
||||
|
||||
// Following code is a non-exhaustive check to convert
|
||||
// B2 errors into S3 compatible errors.
|
||||
//
|
||||
// For a more complete information - https://www.backblaze.com/b2/docs/
|
||||
statusCode, code, msg := b2.Code(err)
|
||||
if statusCode == 0 {
|
||||
// We don't interpret non B2 errors. B2 errors have statusCode
|
||||
// to help us convert them to S3 object errors.
|
||||
return err
|
||||
}
|
||||
|
||||
switch code {
|
||||
case "duplicate_bucket_name":
|
||||
err = minio.BucketAlreadyOwnedByYou{Bucket: bucket}
|
||||
case "bad_request":
|
||||
if object != "" {
|
||||
err = minio.ObjectNameInvalid{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
} else if bucket != "" {
|
||||
err = minio.BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
case "bad_json":
|
||||
if object != "" {
|
||||
err = minio.ObjectNameInvalid{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
} else if bucket != "" {
|
||||
err = minio.BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
case "bad_bucket_id":
|
||||
err = minio.BucketNotFound{Bucket: bucket}
|
||||
case "file_not_present", "not_found":
|
||||
err = minio.ObjectNotFound{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
case "cannot_delete_non_empty_bucket":
|
||||
err = minio.BucketNotEmpty{Bucket: bucket}
|
||||
}
|
||||
|
||||
// Special interpretation like this is required for Multipart sessions.
|
||||
if strings.Contains(msg, "No active upload for") && uploadID != "" {
|
||||
err = minio.InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutdown saves any gateway metadata to disk
|
||||
// if necessary and reload upon next restart.
|
||||
func (l *b2Objects) Shutdown(ctx context.Context) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageInfo is not relevant to B2 backend.
|
||||
func (l *b2Objects) StorageInfo(ctx context.Context) (si minio.StorageInfo) {
|
||||
return si
|
||||
}
|
||||
|
||||
// MakeBucket creates a new container on B2 backend.
|
||||
func (l *b2Objects) MakeBucketWithLocation(ctx context.Context, bucket, location string) error {
|
||||
// location is ignored for B2 backend.
|
||||
|
||||
// All buckets are set to private by default.
|
||||
_, err := l.b2Client.CreateBucket(l.ctx, bucket, bucketTypePrivate, nil, nil)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
func (l *b2Objects) reAuthorizeAccount(ctx context.Context) error {
|
||||
client, err := b2.AuthorizeAccount(l.ctx, l.creds.AccessKey, l.creds.SecretKey, b2.Transport(minio.NewCustomHTTPTransport()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.mu.Lock()
|
||||
l.b2Client.Update(client)
|
||||
l.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// listBuckets is a wrapper similar to ListBuckets, which re-authorizes
|
||||
// the account and updates the B2 client safely. Once successfully
|
||||
// authorized performs the call again and returns list of buckets.
|
||||
// For any errors which are not actionable we return an error.
|
||||
func (l *b2Objects) listBuckets(ctx context.Context, err error) ([]*b2.Bucket, error) {
|
||||
if err != nil {
|
||||
if b2.Action(err) != b2.ReAuthenticate {
|
||||
return nil, err
|
||||
}
|
||||
if rerr := l.reAuthorizeAccount(ctx); rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
}
|
||||
bktList, lerr := l.b2Client.ListBuckets(l.ctx)
|
||||
if lerr != nil {
|
||||
return l.listBuckets(ctx, lerr)
|
||||
}
|
||||
return bktList, nil
|
||||
}
|
||||
|
||||
// Bucket - is a helper which provides a *Bucket instance
|
||||
// for performing an API operation. B2 API doesn't
|
||||
// provide a direct way to access the bucket so we need
|
||||
// to employ following technique.
|
||||
func (l *b2Objects) Bucket(ctx context.Context, bucket string) (*b2.Bucket, error) {
|
||||
bktList, err := l.listBuckets(ctx, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, b2ToObjectError(err, bucket)
|
||||
}
|
||||
for _, bkt := range bktList {
|
||||
if bkt.Name == bucket {
|
||||
return bkt, nil
|
||||
}
|
||||
}
|
||||
return nil, minio.BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
|
||||
// GetBucketInfo gets bucket metadata..
|
||||
func (l *b2Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, err error) {
|
||||
if _, err = l.Bucket(ctx, bucket); err != nil {
|
||||
return bi, err
|
||||
}
|
||||
return minio.BucketInfo{
|
||||
Name: bucket,
|
||||
Created: time.Unix(0, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListBuckets lists all B2 buckets
|
||||
func (l *b2Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) {
|
||||
bktList, err := l.listBuckets(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var bktInfo []minio.BucketInfo
|
||||
for _, bkt := range bktList {
|
||||
bktInfo = append(bktInfo, minio.BucketInfo{
|
||||
Name: bkt.Name,
|
||||
Created: time.Unix(0, 0),
|
||||
})
|
||||
}
|
||||
return bktInfo, nil
|
||||
}
|
||||
|
||||
// DeleteBucket deletes a bucket on B2
|
||||
func (l *b2Objects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bkt.DeleteBucket(l.ctx)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
// ListObjects lists all objects in B2 bucket filtered by prefix, returns upto at max 1000 entries at a time.
|
||||
func (l *b2Objects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return loi, err
|
||||
}
|
||||
files, next, lerr := bkt.ListFileNames(l.ctx, maxKeys, marker, prefix, delimiter)
|
||||
if lerr != nil {
|
||||
logger.LogIf(ctx, lerr)
|
||||
return loi, b2ToObjectError(lerr, bucket)
|
||||
}
|
||||
loi.IsTruncated = next != ""
|
||||
loi.NextMarker = next
|
||||
for _, file := range files {
|
||||
switch file.Status {
|
||||
case "folder":
|
||||
loi.Prefixes = append(loi.Prefixes, file.Name)
|
||||
case "upload":
|
||||
loi.Objects = append(loi.Objects, minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: file.Name,
|
||||
ModTime: file.Timestamp,
|
||||
Size: file.Size,
|
||||
ETag: minio.ToS3ETag(file.Info.ID),
|
||||
ContentType: file.Info.ContentType,
|
||||
UserDefined: file.Info.Info,
|
||||
})
|
||||
}
|
||||
}
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
// ListObjectsV2 lists all objects in B2 bucket filtered by prefix, returns upto max 1000 entries at a time.
|
||||
func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int,
|
||||
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
|
||||
// fetchOwner is not supported and unused.
|
||||
marker := continuationToken
|
||||
if marker == "" {
|
||||
// B2's continuation token is an object name to "start at" rather than "start after"
|
||||
// startAfter plus the lowest character B2 supports is used so that the startAfter
|
||||
// object isn't included in the results
|
||||
marker = startAfter + " "
|
||||
}
|
||||
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return loi, err
|
||||
}
|
||||
files, next, lerr := bkt.ListFileNames(l.ctx, maxKeys, marker, prefix, delimiter)
|
||||
if lerr != nil {
|
||||
logger.LogIf(ctx, lerr)
|
||||
return loi, b2ToObjectError(lerr, bucket)
|
||||
}
|
||||
loi.IsTruncated = next != ""
|
||||
loi.ContinuationToken = continuationToken
|
||||
loi.NextContinuationToken = next
|
||||
for _, file := range files {
|
||||
switch file.Status {
|
||||
case "folder":
|
||||
loi.Prefixes = append(loi.Prefixes, file.Name)
|
||||
case "upload":
|
||||
loi.Objects = append(loi.Objects, minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: file.Name,
|
||||
ModTime: file.Timestamp,
|
||||
Size: file.Size,
|
||||
ETag: minio.ToS3ETag(file.Info.ID),
|
||||
ContentType: file.Info.ContentType,
|
||||
UserDefined: file.Info.Info,
|
||||
})
|
||||
}
|
||||
}
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||
func (l *b2Objects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||
var objInfo minio.ObjectInfo
|
||||
objInfo, err = l.GetObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var startOffset, length int64
|
||||
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err := l.GetObject(ctx, bucket, object, startOffset, length, pw, objInfo.ETag, opts)
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
// Setup cleanup function to cause the above go-routine to
|
||||
// exit in case of partial read
|
||||
pipeCloser := func() { pr.Close() }
|
||||
return minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||
}
|
||||
|
||||
// GetObject reads an object from B2. Supports additional
|
||||
// parameters like offset and length which are synonymous with
|
||||
// HTTP Range requests.
|
||||
//
|
||||
// startOffset indicates the starting read location of the object.
|
||||
// length indicates the total length of the object.
|
||||
func (l *b2Objects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := bkt.DownloadFileByName(l.ctx, object, startOffset, length)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
defer reader.Close()
|
||||
_, err = io.Copy(writer, reader)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
func (l *b2Objects) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
f, err := bkt.DownloadFileByName(l.ctx, object, 0, 1)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
f.Close()
|
||||
fi, err := bkt.File(f.ID, object).GetFileInfo(l.ctx)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
return minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
ETag: minio.ToS3ETag(fi.ID),
|
||||
Size: fi.Size,
|
||||
ModTime: fi.Timestamp,
|
||||
ContentType: fi.ContentType,
|
||||
UserDefined: fi.Info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// In B2 - You must always include the X-Bz-Content-Sha1 header with
|
||||
// your upload request. The value you provide can be:
|
||||
// (1) the 40-character hex checksum of the file,
|
||||
// (2) the string hex_digits_at_end, or
|
||||
// (3) the string do_not_verify.
|
||||
// For more reference - https://www.backblaze.com/b2/docs/uploading.html
|
||||
//
|
||||
// In our case we are going to use (2) option
|
||||
const sha1AtEOF = "hex_digits_at_end"
|
||||
|
||||
// With the second option mentioned above, you append the 40-character hex sha1
|
||||
// to the end of the request body, immediately after the contents of the file
|
||||
// being uploaded. Note that the content length is the size of the file plus 40
|
||||
// of the original size of the reader.
|
||||
//
|
||||
// newB2Reader implements a B2 compatible reader by wrapping the hash.Reader into
|
||||
// a new io.Reader which will emit out the sha1 hex digits at io.EOF.
|
||||
// It also means that your overall content size is now original size + 40 bytes.
|
||||
// Additionally this reader also verifies Hash encapsulated inside hash.Reader
|
||||
// at io.EOF if the verification failed we return an error and do not send
|
||||
// the content to server.
|
||||
func newB2Reader(r *h2.Reader, size int64) *Reader {
|
||||
return &Reader{
|
||||
r: r,
|
||||
size: size,
|
||||
sha1Hash: sha1.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Reader - is a Reader wraps the hash.Reader which will emit out the sha1
|
||||
// hex digits at io.EOF. It also means that your overall content size is
|
||||
// now original size + 40 bytes. Additionally this reader also verifies
|
||||
// Hash encapsulated inside hash.Reader at io.EOF if the verification
|
||||
// failed we return an error and do not send the content to server.
|
||||
type Reader struct {
|
||||
r *h2.Reader
|
||||
size int64
|
||||
sha1Hash hash.Hash
|
||||
|
||||
isEOF bool
|
||||
buf *strings.Reader
|
||||
}
|
||||
|
||||
// Size - Returns the total size of Reader.
|
||||
func (nb *Reader) Size() int64 { return nb.size + 40 }
|
||||
func (nb *Reader) Read(p []byte) (int, error) {
|
||||
if nb.isEOF {
|
||||
return nb.buf.Read(p)
|
||||
}
|
||||
// Read into hash to update the on going checksum.
|
||||
n, err := io.TeeReader(nb.r, nb.sha1Hash).Read(p)
|
||||
if err == io.EOF {
|
||||
// Stream is not corrupted on this end
|
||||
// now fill in the last 40 bytes of sha1 hex
|
||||
// so that the server can verify the stream on
|
||||
// their end.
|
||||
err = nil
|
||||
nb.isEOF = true
|
||||
nb.buf = strings.NewReader(fmt.Sprintf("%x", nb.sha1Hash.Sum(nil)))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// PutObject uploads the single upload to B2 backend by using *b2_upload_file* API, uploads upto 5GiB.
|
||||
func (l *b2Objects) PutObject(ctx context.Context, bucket string, object string, r *minio.PutObjReader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
data := r.Reader
|
||||
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
contentType := metadata["content-type"]
|
||||
delete(metadata, "content-type")
|
||||
|
||||
var u *b2.URL
|
||||
u, err = bkt.GetUploadURL(l.ctx)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
hr := newB2Reader(data, data.Size())
|
||||
var f *b2.File
|
||||
f, err = u.UploadFile(l.ctx, hr, int(hr.Size()), object, contentType, sha1AtEOF, metadata)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
var fi *b2.FileInfo
|
||||
fi, err = f.GetFileInfo(l.ctx)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
return minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
ETag: minio.ToS3ETag(fi.ID),
|
||||
Size: fi.Size,
|
||||
ModTime: fi.Timestamp,
|
||||
ContentType: fi.ContentType,
|
||||
UserDefined: fi.Info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteObject deletes a blob in bucket
|
||||
func (l *b2Objects) DeleteObject(ctx context.Context, bucket string, object string) error {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := bkt.DownloadFileByName(l.ctx, object, 0, 1)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
io.Copy(ioutil.Discard, reader)
|
||||
reader.Close()
|
||||
err = bkt.File(reader.ID, object).DeleteFileVersion(l.ctx)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
// ListMultipartUploads lists all multipart uploads.
|
||||
func (l *b2Objects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string,
|
||||
delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, err error) {
|
||||
// keyMarker, prefix, delimiter are all ignored, Backblaze B2 doesn't support any
|
||||
// of these parameters only equivalent parameter is uploadIDMarker.
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return lmi, err
|
||||
}
|
||||
// The maximum number of files to return from this call.
|
||||
// The default value is 100, and the maximum allowed is 100.
|
||||
if maxUploads > 100 {
|
||||
maxUploads = 100
|
||||
}
|
||||
largeFiles, nextMarker, err := bkt.ListUnfinishedLargeFiles(l.ctx, uploadIDMarker, maxUploads)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return lmi, b2ToObjectError(err, bucket)
|
||||
}
|
||||
lmi = minio.ListMultipartsInfo{
|
||||
MaxUploads: maxUploads,
|
||||
}
|
||||
if nextMarker != "" {
|
||||
lmi.IsTruncated = true
|
||||
lmi.NextUploadIDMarker = nextMarker
|
||||
}
|
||||
for _, largeFile := range largeFiles {
|
||||
lmi.Uploads = append(lmi.Uploads, minio.MultipartInfo{
|
||||
Object: largeFile.Name,
|
||||
UploadID: largeFile.ID,
|
||||
Initiated: largeFile.Timestamp,
|
||||
})
|
||||
}
|
||||
return lmi, nil
|
||||
}
|
||||
|
||||
// NewMultipartUpload upload object in multiple parts, uses B2's LargeFile upload API.
|
||||
// Large files can range in size from 5MB to 10TB.
|
||||
// Each large file must consist of at least 2 parts, and all of the parts except the
|
||||
// last one must be at least 5MB in size. The last part must contain at least one byte.
|
||||
// For more information - https://www.backblaze.com/b2/docs/large_files.html
|
||||
func (l *b2Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string, o minio.ObjectOptions) (string, error) {
|
||||
var uploadID string
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return uploadID, err
|
||||
}
|
||||
|
||||
contentType := metadata["content-type"]
|
||||
delete(metadata, "content-type")
|
||||
lf, err := bkt.StartLargeFile(l.ctx, object, contentType, metadata)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return uploadID, b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
return lf.ID, nil
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket, uses B2's LargeFile upload API.
|
||||
func (l *b2Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, r *minio.PutObjReader, opts minio.ObjectOptions) (pi minio.PartInfo, err error) {
|
||||
data := r.Reader
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return pi, err
|
||||
}
|
||||
|
||||
fc, err := bkt.File(uploadID, object).CompileParts(0, nil).GetUploadPartURL(l.ctx)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return pi, b2ToObjectError(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
hr := newB2Reader(data, data.Size())
|
||||
sha1, err := fc.UploadPart(l.ctx, hr, sha1AtEOF, int(hr.Size()), partID)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return pi, b2ToObjectError(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
return minio.PartInfo{
|
||||
PartNumber: partID,
|
||||
LastModified: minio.UTCNow(),
|
||||
ETag: minio.ToS3ETag(sha1),
|
||||
Size: data.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket, uses B2's LargeFile upload API.
|
||||
func (l *b2Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return lpi, err
|
||||
}
|
||||
lpi = minio.ListPartsInfo{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
UploadID: uploadID,
|
||||
MaxParts: maxParts,
|
||||
PartNumberMarker: partNumberMarker,
|
||||
}
|
||||
// startPartNumber must be in the range 1 - 10000 for B2.
|
||||
partNumberMarker++
|
||||
partsList, next, err := bkt.File(uploadID, object).ListParts(l.ctx, partNumberMarker, maxParts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return lpi, b2ToObjectError(err, bucket, object, uploadID)
|
||||
}
|
||||
if next != 0 {
|
||||
lpi.IsTruncated = true
|
||||
lpi.NextPartNumberMarker = next
|
||||
}
|
||||
for _, part := range partsList {
|
||||
lpi.Parts = append(lpi.Parts, minio.PartInfo{
|
||||
PartNumber: part.Number,
|
||||
ETag: minio.ToS3ETag(part.SHA1),
|
||||
Size: part.Size,
|
||||
})
|
||||
}
|
||||
return lpi, nil
|
||||
}
|
||||
|
||||
// AbortMultipartUpload aborts a on going multipart upload, uses B2's LargeFile upload API.
|
||||
func (l *b2Objects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bkt.File(uploadID, object).CompileParts(0, nil).CancelLargeFile(l.ctx)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object, uses B2's LargeFile upload API.
|
||||
func (l *b2Objects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, uploadedParts []minio.CompletePart, opts minio.ObjectOptions) (oi minio.ObjectInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
hashes := make(map[int]string)
|
||||
for i, uploadedPart := range uploadedParts {
|
||||
// B2 requires contigous part numbers starting with 1, they do not support
|
||||
// hand picking part numbers, we return an S3 compatible error instead.
|
||||
if i+1 != uploadedPart.PartNumber {
|
||||
logger.LogIf(ctx, minio.InvalidPart{})
|
||||
return oi, b2ToObjectError(minio.InvalidPart{}, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
// Trim "-1" suffix in ETag as PutObjectPart() treats B2 returned SHA1 as ETag.
|
||||
hashes[uploadedPart.PartNumber] = strings.TrimSuffix(uploadedPart.ETag, "-1")
|
||||
}
|
||||
|
||||
if _, err = bkt.File(uploadID, object).CompileParts(0, hashes).FinishLargeFile(l.ctx); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return oi, b2ToObjectError(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
return l.GetObjectInfo(ctx, bucket, object, minio.ObjectOptions{})
|
||||
}
|
||||
|
||||
// SetBucketPolicy - B2 supports 2 types of bucket policies:
|
||||
// bucketType.AllPublic - bucketTypeReadOnly means that anybody can download the files is the bucket;
|
||||
// bucketType.AllPrivate - bucketTypePrivate means that you need an authorization token to download them.
|
||||
// Default is AllPrivate for all buckets.
|
||||
func (l *b2Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
|
||||
policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
|
||||
if err != nil {
|
||||
// This should not happen.
|
||||
return b2ToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
var policies []minio.BucketAccessPolicy
|
||||
for prefix, policy := range miniogopolicy.GetPolicies(policyInfo.Statements, bucket, "") {
|
||||
policies = append(policies, minio.BucketAccessPolicy{
|
||||
Prefix: prefix,
|
||||
Policy: policy,
|
||||
})
|
||||
}
|
||||
prefix := bucket + "/*" // For all objects inside the bucket.
|
||||
if len(policies) != 1 {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
return minio.NotImplemented{}
|
||||
}
|
||||
if policies[0].Prefix != prefix {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
return minio.NotImplemented{}
|
||||
}
|
||||
if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
return minio.NotImplemented{}
|
||||
}
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bkt.Type = bucketTypeReadOnly
|
||||
_, err = bkt.Update(l.ctx)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err)
|
||||
}
|
||||
|
||||
// GetBucketPolicy, returns the current bucketType from B2 backend and convert
|
||||
// it into S3 compatible bucket policy info.
|
||||
func (l *b2Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// bkt.Type can also be snapshot, but it is only allowed through B2 browser console,
|
||||
// just return back as policy not found for all cases.
|
||||
// CreateBucket always sets the value to allPrivate by default.
|
||||
if bkt.Type != bucketTypeReadOnly {
|
||||
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
|
||||
return &policy.Policy{
|
||||
Version: policy.DefaultVersion,
|
||||
Statements: []policy.Statement{
|
||||
policy.NewStatement(
|
||||
policy.Allow,
|
||||
policy.NewPrincipal("*"),
|
||||
policy.NewActionSet(
|
||||
policy.GetBucketLocationAction,
|
||||
policy.ListBucketAction,
|
||||
policy.GetObjectAction,
|
||||
),
|
||||
policy.NewResourceSet(
|
||||
policy.NewResource(bucket, ""),
|
||||
policy.NewResource(bucket, "*"),
|
||||
),
|
||||
condition.NewFunctions(),
|
||||
),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteBucketPolicy - resets the bucketType of bucket on B2 to 'allPrivate'.
|
||||
func (l *b2Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bkt.Type = bucketTypePrivate
|
||||
_, err = bkt.Update(l.ctx)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err)
|
||||
}
|
||||
|
||||
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||
func (l *b2Objects) IsCompressionSupported() bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* 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 b2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
b2 "github.com/minio/blazer/base"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
// Test b2 object error.
|
||||
func TestB2ObjectError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
params []string
|
||||
b2Err error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
[]string{}, nil, nil,
|
||||
},
|
||||
{
|
||||
[]string{}, fmt.Errorf("Not *Error"), fmt.Errorf("Not *Error"),
|
||||
},
|
||||
{
|
||||
[]string{}, fmt.Errorf("Non B2 Error"), fmt.Errorf("Non B2 Error"),
|
||||
},
|
||||
{
|
||||
[]string{"bucket"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Code: "duplicate_bucket_name",
|
||||
}, minio.BucketAlreadyOwnedByYou{
|
||||
Bucket: "bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Code: "bad_request",
|
||||
}, minio.BucketNotFound{
|
||||
Bucket: "bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Code: "bad_request",
|
||||
}, minio.ObjectNameInvalid{
|
||||
Bucket: "bucket",
|
||||
Object: "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Code: "bad_bucket_id",
|
||||
}, minio.BucketNotFound{Bucket: "bucket"},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Code: "file_not_present",
|
||||
}, minio.ObjectNotFound{
|
||||
Bucket: "bucket",
|
||||
Object: "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Code: "not_found",
|
||||
}, minio.ObjectNotFound{
|
||||
Bucket: "bucket",
|
||||
Object: "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Code: "cannot_delete_non_empty_bucket",
|
||||
}, minio.BucketNotEmpty{
|
||||
Bucket: "bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object", "uploadID"}, b2.Error{
|
||||
StatusCode: 1,
|
||||
Message: "No active upload for",
|
||||
}, minio.InvalidUploadID{
|
||||
UploadID: "uploadID",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actualErr := b2ToObjectError(testCase.b2Err, testCase.params...)
|
||||
if actualErr != nil {
|
||||
if actualErr.Error() != testCase.expectedErr.Error() {
|
||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.expectedErr, actualErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,9 @@ package gateway
|
||||
import (
|
||||
// Import all gateways.
|
||||
_ "github.com/minio/minio/cmd/gateway/azure"
|
||||
_ "github.com/minio/minio/cmd/gateway/b2"
|
||||
_ "github.com/minio/minio/cmd/gateway/gcs"
|
||||
_ "github.com/minio/minio/cmd/gateway/manta"
|
||||
_ "github.com/minio/minio/cmd/gateway/nas"
|
||||
_ "github.com/minio/minio/cmd/gateway/oss"
|
||||
_ "github.com/minio/minio/cmd/gateway/s3"
|
||||
_ "github.com/minio/minio/cmd/gateway/sia"
|
||||
// Add your gateway here.
|
||||
)
|
||||
|
||||
@@ -1,666 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017, 2018 Minio, Inc.
|
||||
*
|
||||
* 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 manta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
triton "github.com/joyent/triton-go"
|
||||
"github.com/joyent/triton-go/authentication"
|
||||
terrors "github.com/joyent/triton-go/errors"
|
||||
"github.com/joyent/triton-go/storage"
|
||||
"github.com/minio/cli"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// stor is a namespace within manta where you store any documents that are deemed as private
|
||||
// and require access credentials to read them. Within the stor namespace, you can create any
|
||||
// number of directories and objects.
|
||||
const (
|
||||
mantaBackend = "manta"
|
||||
defaultMantaRoot = "/stor"
|
||||
defaultMantaURL = "https://us-east.manta.joyent.com"
|
||||
)
|
||||
|
||||
var mantaRoot = defaultMantaRoot
|
||||
|
||||
func init() {
|
||||
const mantaGatewayTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
USAGE:
|
||||
{{.HelpName}} {{if .VisibleFlags}}[FLAGS]{{end}} [ENDPOINT]
|
||||
{{if .VisibleFlags}}
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
ENDPOINT:
|
||||
Manta server endpoint. Default ENDPOINT is https://us-east.manta.joyent.com
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
ACCESS:
|
||||
MINIO_ACCESS_KEY: The Manta account name.
|
||||
MINIO_SECRET_KEY: A KeyID associated with the Manta account.
|
||||
MANTA_KEY_MATERIAL: The path to the SSH Key associated with the Manta account if the MINIO_SECRET_KEY is not in SSH Agent.
|
||||
MANTA_SUBUSER: The username of a user who has limited access to your account.
|
||||
|
||||
BROWSER:
|
||||
MINIO_BROWSER: To disable web browser access, set this value to "off".
|
||||
|
||||
DOMAIN:
|
||||
MINIO_DOMAIN: To enable virtual-host-style requests, set this value to Minio host domain name.
|
||||
|
||||
CACHE:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for Manta Object Storage backend.
|
||||
$ export MINIO_ACCESS_KEY=manta_account_name
|
||||
$ export MINIO_SECRET_KEY=manta_key_id
|
||||
$ {{.HelpName}}
|
||||
|
||||
2. Start minio gateway server for Manta Object Storage backend on custom endpoint.
|
||||
$ export MINIO_ACCESS_KEY=manta_account_name
|
||||
$ export MINIO_SECRET_KEY=manta_key_id
|
||||
$ {{.HelpName}} https://us-west.manta.joyent.com
|
||||
|
||||
3. Start minio gateway server for Manta Object Storage backend without using SSH Agent.
|
||||
$ export MINIO_ACCESS_KEY=manta_account_name
|
||||
$ export MINIO_SECRET_KEY=manta_key_id
|
||||
$ export MANTA_KEY_MATERIAL=~/.ssh/custom_rsa
|
||||
$ {{.HelpName}}
|
||||
|
||||
4. Start minio gateway server for Manta Object Storage backend with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=manta_account_name
|
||||
$ export MINIO_SECRET_KEY=manta_key_id
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
Name: mantaBackend,
|
||||
Usage: "Manta Object Storage",
|
||||
Action: mantaGatewayMain,
|
||||
CustomHelpTemplate: mantaGatewayTemplate,
|
||||
HideHelpCommand: true,
|
||||
})
|
||||
}
|
||||
|
||||
func mantaGatewayMain(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if !ctx.Args().Present() {
|
||||
args = cli.Args{"https://us-east.manta.joyent.com"}
|
||||
}
|
||||
|
||||
// Validate gateway arguments.
|
||||
logger.FatalIf(minio.ValidateGatewayArguments(ctx.GlobalString("address"), args.First()), "Invalid argument")
|
||||
|
||||
// Start the gateway..
|
||||
minio.StartGateway(ctx, &Manta{args.First()})
|
||||
}
|
||||
|
||||
// Manta implements Gateway.
|
||||
type Manta struct {
|
||||
host string
|
||||
}
|
||||
|
||||
// Name implements Gateway interface.
|
||||
func (g *Manta) Name() string {
|
||||
return mantaBackend
|
||||
}
|
||||
|
||||
// NewGatewayLayer returns manta gateway layer, implements ObjectLayer interface to
|
||||
// talk to manta remote backend.
|
||||
func (g *Manta) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
||||
var err error
|
||||
var secure bool
|
||||
var signer authentication.Signer
|
||||
var endpoint = defaultMantaURL
|
||||
ctx := context.Background()
|
||||
|
||||
if g.host != "" {
|
||||
endpoint, secure, err = minio.ParseGatewayEndpoint(g.host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if secure {
|
||||
endpoint = "https://" + endpoint
|
||||
} else {
|
||||
endpoint = "http://" + endpoint
|
||||
}
|
||||
}
|
||||
if overrideRoot, ok := os.LookupEnv("MANTA_ROOT"); ok {
|
||||
mantaRoot = overrideRoot
|
||||
}
|
||||
|
||||
keyMaterial := os.Getenv("MANTA_KEY_MATERIAL")
|
||||
|
||||
if keyMaterial == "" {
|
||||
input := authentication.SSHAgentSignerInput{
|
||||
KeyID: creds.SecretKey,
|
||||
AccountName: creds.AccessKey,
|
||||
}
|
||||
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
|
||||
input.Username = userName
|
||||
}
|
||||
signer, err = authentication.NewSSHAgentSigner(input)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
var keyBytes []byte
|
||||
if _, err = os.Stat(keyMaterial); err == nil {
|
||||
keyBytes, err = ioutil.ReadFile(keyMaterial)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading key material from %s: %s",
|
||||
keyMaterial, err)
|
||||
}
|
||||
block, _ := pem.Decode(keyBytes)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"Failed to read key material '%s': no key found", keyMaterial)
|
||||
}
|
||||
|
||||
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
|
||||
return nil, fmt.Errorf(
|
||||
"Failed to read key '%s': password protected keys are\n"+
|
||||
"not currently supported. Please decrypt the key prior to use.", keyMaterial)
|
||||
}
|
||||
|
||||
} else {
|
||||
keyBytes = []byte(keyMaterial)
|
||||
}
|
||||
|
||||
input := authentication.PrivateKeySignerInput{
|
||||
KeyID: creds.SecretKey,
|
||||
PrivateKeyMaterial: keyBytes,
|
||||
AccountName: creds.AccessKey,
|
||||
}
|
||||
if userName, ok := os.LookupEnv("MANTA_SUBUSER"); ok {
|
||||
input.Username = userName
|
||||
}
|
||||
|
||||
signer, err = authentication.NewPrivateKeySigner(input)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tc, err := storage.NewClient(&triton.ClientConfig{
|
||||
MantaURL: endpoint,
|
||||
AccountName: creds.AccessKey,
|
||||
Signers: []authentication.Signer{signer},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tc.Client.HTTPClient = &http.Client{
|
||||
Transport: minio.NewCustomHTTPTransport(),
|
||||
}
|
||||
|
||||
return &tritonObjects{
|
||||
client: tc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Production - Manta is production ready.
|
||||
func (g *Manta) Production() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// tritonObjects - Implements Object layer for Triton Manta storage
|
||||
type tritonObjects struct {
|
||||
minio.GatewayUnsupported
|
||||
client *storage.StorageClient
|
||||
}
|
||||
|
||||
// Shutdown - save any gateway metadata to disk
|
||||
// if necessary and reload upon next restart.
|
||||
func (t *tritonObjects) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageInfo - Not relevant to Triton backend.
|
||||
func (t *tritonObjects) StorageInfo(ctx context.Context) (si minio.StorageInfo) {
|
||||
return si
|
||||
}
|
||||
|
||||
//
|
||||
// ~~~ Buckets ~~~
|
||||
//
|
||||
|
||||
// MakeBucketWithLocation - Create a new directory within manta.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#PutDirectory
|
||||
func (t *tritonObjects) MakeBucketWithLocation(ctx context.Context, bucket, location string) error {
|
||||
err := t.client.Dir().Put(ctx, &storage.PutDirectoryInput{
|
||||
DirectoryName: path.Join(mantaRoot, bucket),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBucketInfo - Get directory metadata..
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#GetObject
|
||||
func (t *tritonObjects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, e error) {
|
||||
var info minio.BucketInfo
|
||||
resp, err := t.client.Objects().Get(ctx, &storage.GetObjectInput{
|
||||
ObjectPath: path.Join(mantaRoot, bucket),
|
||||
})
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
return minio.BucketInfo{
|
||||
Name: bucket,
|
||||
Created: resp.LastModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListBuckets - Lists all Manta directories, uses Manta equivalent
|
||||
// ListDirectories.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#ListDirectory
|
||||
func (t *tritonObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) {
|
||||
dirs, err := t.client.Dir().List(ctx, &storage.ListDirectoryInput{
|
||||
DirectoryName: path.Join(mantaRoot),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, dir := range dirs.Entries {
|
||||
if dir.Type == "directory" {
|
||||
buckets = append(buckets, minio.BucketInfo{
|
||||
Name: dir.Name,
|
||||
Created: dir.ModifiedTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
// DeleteBucket - Delete a directory in Manta, uses Manta equivalent
|
||||
// DeleteDirectory.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#DeleteDirectory
|
||||
func (t *tritonObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
return t.client.Dir().Delete(ctx, &storage.DeleteDirectoryInput{
|
||||
DirectoryName: path.Join(mantaRoot, bucket),
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// ~~~ Objects ~~~
|
||||
//
|
||||
|
||||
// ListObjects - Lists all objects in Manta with a container filtered by prefix
|
||||
// and marker, uses Manta equivalent ListDirectory.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#ListDirectory
|
||||
func (t *tritonObjects) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (result minio.ListObjectsInfo, err error) {
|
||||
var (
|
||||
dirName string
|
||||
objs *storage.ListDirectoryOutput
|
||||
input *storage.ListDirectoryInput
|
||||
|
||||
pathBase = path.Base(prefix)
|
||||
)
|
||||
|
||||
// Make sure to only request a Dir.List for the parent "directory" for a
|
||||
// given prefix first. We don't know if our prefix is referencing a
|
||||
// directory or file name and can't send file names into Dir.List because
|
||||
// that'll cause Manta to return file content in the response body. Dir.List
|
||||
// expects to parse out directory entries in JSON. So, try the first
|
||||
// directory name of the prefix path provided.
|
||||
if pathDir := path.Dir(prefix); pathDir == "." {
|
||||
dirName = path.Join(mantaRoot, bucket)
|
||||
} else {
|
||||
dirName = path.Join(mantaRoot, bucket, pathDir)
|
||||
}
|
||||
|
||||
if marker != "" {
|
||||
// Manta uses the marker as the key to start at rather than start after
|
||||
// A space is appended to the marker so that the corresponding object is not
|
||||
// included in the results
|
||||
marker += " "
|
||||
}
|
||||
|
||||
input = &storage.ListDirectoryInput{
|
||||
DirectoryName: dirName,
|
||||
Limit: uint64(maxKeys),
|
||||
Marker: marker,
|
||||
}
|
||||
objs, err = t.client.Dir().List(ctx, input)
|
||||
if err != nil {
|
||||
if terrors.IsResourceNotFoundError(err) {
|
||||
return result, nil
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
for _, obj := range objs.Entries {
|
||||
// If the base name of our prefix was found to be of type "directory"
|
||||
// than we need to pull the directory entries for that instead.
|
||||
if obj.Name == pathBase && obj.Type == "directory" {
|
||||
input.DirectoryName = path.Join(mantaRoot, bucket, prefix)
|
||||
objs, err = t.client.Dir().List(ctx, input)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return result, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
isTruncated := true // Always send a second request.
|
||||
if marker == "" && len(objs.Entries) < maxKeys {
|
||||
isTruncated = false
|
||||
} else if marker != "" && len(objs.Entries) < maxKeys {
|
||||
isTruncated = false
|
||||
}
|
||||
|
||||
for _, obj := range objs.Entries {
|
||||
if obj.Type == "directory" {
|
||||
result.Prefixes = append(result.Prefixes, obj.Name+delimiter)
|
||||
} else {
|
||||
result.Objects = append(result.Objects, minio.ObjectInfo{
|
||||
Name: obj.Name,
|
||||
Size: int64(obj.Size),
|
||||
ModTime: obj.ModifiedTime,
|
||||
ETag: obj.ETag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result.IsTruncated = isTruncated
|
||||
if isTruncated {
|
||||
result.NextMarker = result.Objects[len(result.Objects)-1].Name
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
//
|
||||
// ~~~ Objects ~~~
|
||||
//
|
||||
|
||||
// ListObjectsV2 - Lists all objects in Manta with a container filtered by prefix
|
||||
// and continuationToken, uses Manta equivalent ListDirectory.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#ListDirectory
|
||||
func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result minio.ListObjectsV2Info, err error) {
|
||||
var (
|
||||
dirName string
|
||||
objs *storage.ListDirectoryOutput
|
||||
input *storage.ListDirectoryInput
|
||||
|
||||
pathBase = path.Base(prefix)
|
||||
)
|
||||
|
||||
marker := continuationToken
|
||||
if marker == "" {
|
||||
marker = startAfter
|
||||
}
|
||||
|
||||
if marker != "" {
|
||||
// Manta uses the marker as the key to start at rather than start after.
|
||||
// A space is appended to the marker so that the corresponding object is not
|
||||
// included in the results
|
||||
marker += " "
|
||||
}
|
||||
|
||||
if pathDir := path.Dir(prefix); pathDir == "." {
|
||||
dirName = path.Join(mantaRoot, bucket)
|
||||
} else {
|
||||
dirName = path.Join(mantaRoot, bucket, pathDir)
|
||||
}
|
||||
|
||||
input = &storage.ListDirectoryInput{
|
||||
DirectoryName: dirName,
|
||||
Limit: uint64(maxKeys),
|
||||
Marker: marker,
|
||||
}
|
||||
objs, err = t.client.Dir().List(ctx, input)
|
||||
if err != nil {
|
||||
if terrors.IsResourceNotFoundError(err) {
|
||||
return result, nil
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
return result, err
|
||||
}
|
||||
|
||||
for _, obj := range objs.Entries {
|
||||
if obj.Name == pathBase && obj.Type == "directory" {
|
||||
input.DirectoryName = path.Join(mantaRoot, bucket, prefix)
|
||||
objs, err = t.client.Dir().List(ctx, input)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return result, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
isTruncated := true // Always send a second request.
|
||||
if continuationToken == "" && len(objs.Entries) < maxKeys {
|
||||
isTruncated = false
|
||||
} else if continuationToken != "" && len(objs.Entries) < maxKeys {
|
||||
isTruncated = false
|
||||
}
|
||||
|
||||
for _, obj := range objs.Entries {
|
||||
if obj.Type == "directory" {
|
||||
result.Prefixes = append(result.Prefixes, obj.Name+delimiter)
|
||||
} else {
|
||||
result.Objects = append(result.Objects, minio.ObjectInfo{
|
||||
Name: obj.Name,
|
||||
Size: int64(obj.Size),
|
||||
ModTime: obj.ModifiedTime,
|
||||
ETag: obj.ETag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result.IsTruncated = isTruncated
|
||||
if isTruncated {
|
||||
result.NextContinuationToken = result.Objects[len(result.Objects)-1].Name
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||
func (t *tritonObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||
var objInfo minio.ObjectInfo
|
||||
objInfo, err = t.GetObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var startOffset, length int64
|
||||
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err := t.GetObject(ctx, bucket, object, startOffset, length, pw, objInfo.ETag, opts)
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
// Setup cleanup function to cause the above go-routine to
|
||||
// exit in case of partial read
|
||||
pipeCloser := func() { pr.Close() }
|
||||
return minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||
}
|
||||
|
||||
// GetObject - Reads an object from Manta. Supports additional parameters like
|
||||
// offset and length which are synonymous with HTTP Range requests.
|
||||
//
|
||||
// startOffset indicates the starting read location of the object. length
|
||||
// indicates the total length of the object.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#GetObject
|
||||
func (t *tritonObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
// Start offset cannot be negative.
|
||||
if startOffset < 0 {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
|
||||
return fmt.Errorf("Unexpected error")
|
||||
}
|
||||
|
||||
output, err := t.client.Objects().Get(ctx, &storage.GetObjectInput{
|
||||
ObjectPath: path.Join(mantaRoot, bucket, object),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer output.ObjectReader.Close()
|
||||
|
||||
// Read until startOffset and discard, Manta object storage doesn't support range GET requests yet.
|
||||
if _, err = io.CopyN(ioutil.Discard, output.ObjectReader, startOffset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if length > 0 {
|
||||
_, err = io.Copy(writer, io.LimitReader(output.ObjectReader, length))
|
||||
} else {
|
||||
_, err = io.Copy(writer, output.ObjectReader)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetObjectInfo - reads blob metadata properties and replies back minio.ObjectInfo,
|
||||
// uses Triton equivalent GetBlobProperties.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#GetObject
|
||||
func (t *tritonObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
info, err := t.client.Objects().GetInfo(ctx, &storage.GetInfoInput{
|
||||
ObjectPath: path.Join(mantaRoot, bucket, object),
|
||||
})
|
||||
if err != nil {
|
||||
if terrors.IsStatusNotFoundCode(err) {
|
||||
return objInfo, minio.ObjectNotFound{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
}
|
||||
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
return minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
ContentType: info.ContentType,
|
||||
Size: int64(info.ContentLength),
|
||||
ETag: info.ETag,
|
||||
ModTime: info.LastModified,
|
||||
UserDefined: info.Metadata,
|
||||
IsDir: strings.HasSuffix(info.ContentType, "type=directory"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dummySeeker struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (d dummySeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// PutObject - Create a new blob with the incoming data, uses Triton equivalent
|
||||
// CreateBlockBlobFromReader.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#PutObject
|
||||
func (t *tritonObjects) PutObject(ctx context.Context, bucket, object string, r *minio.PutObjReader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
data := r.Reader
|
||||
if err = t.client.Objects().Put(ctx, &storage.PutObjectInput{
|
||||
ContentLength: uint64(data.Size()),
|
||||
ObjectPath: path.Join(mantaRoot, bucket, object),
|
||||
ContentType: metadata["content-type"],
|
||||
// TODO: Change to `string(data.md5sum)` if/when that becomes an exported field
|
||||
ContentMD5: metadata["content-md5"],
|
||||
ObjectReader: dummySeeker{data},
|
||||
ForceInsert: true,
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, err
|
||||
}
|
||||
if err = data.Verify(); err != nil {
|
||||
t.DeleteObject(ctx, bucket, object)
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
return t.GetObjectInfo(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
// CopyObject - Copies a blob from source container to destination container.
|
||||
// Uses Manta Snaplinks API.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#PutSnapLink
|
||||
func (t *tritonObjects) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
if err = t.client.SnapLinks().Put(ctx, &storage.PutSnapLinkInput{
|
||||
SourcePath: path.Join(mantaRoot, srcBucket, srcObject),
|
||||
LinkPath: path.Join(mantaRoot, destBucket, destObject),
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
return t.GetObjectInfo(ctx, destBucket, destObject, dstOpts)
|
||||
}
|
||||
|
||||
// DeleteObject - Delete a blob in Manta, uses Triton equivalent DeleteBlob API.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#DeleteObject
|
||||
func (t *tritonObjects) DeleteObject(ctx context.Context, bucket, object string) error {
|
||||
if err := t.client.Objects().Delete(ctx, &storage.DeleteObjectInput{
|
||||
ObjectPath: path.Join(mantaRoot, bucket, object),
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||
func (t *tritonObjects) IsCompressionSupported() bool {
|
||||
return false
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
package nas
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/minio/cli"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
@@ -121,6 +123,12 @@ func (n *nasObjects) IsListenBucketSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *nasObjects) StorageInfo(ctx context.Context) minio.StorageInfo {
|
||||
sinfo := n.ObjectLayer.StorageInfo(ctx)
|
||||
sinfo.Backend.Type = minio.Unknown
|
||||
return sinfo
|
||||
}
|
||||
|
||||
// nasObjects implements gateway for Minio and S3 compatible object storage servers.
|
||||
type nasObjects struct {
|
||||
minio.ObjectLayer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017, 2018 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -168,35 +169,61 @@ func randString(n int, src rand.Source, prefix string) string {
|
||||
return prefix + string(b[0:30-len(prefix)])
|
||||
}
|
||||
|
||||
func isAmazonS3Endpoint(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s3utils.IsAmazonEndpoint(*u)
|
||||
}
|
||||
|
||||
// Chains all credential types, in the following order:
|
||||
// - AWS env vars (i.e. AWS_ACCESS_KEY_ID)
|
||||
// - AWS creds file (i.e. AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials)
|
||||
// - Static credentials provided by user (i.e. MINIO_ACCESS_KEY)
|
||||
var defaultProviders = []credentials.Provider{
|
||||
&credentials.EnvAWS{},
|
||||
&credentials.FileAWSCredentials{},
|
||||
&credentials.EnvMinio{},
|
||||
}
|
||||
|
||||
// Chains all credential types, in the following order:
|
||||
// - AWS env vars (i.e. AWS_ACCESS_KEY_ID)
|
||||
// - AWS creds file (i.e. AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials)
|
||||
// - IAM profile based credentials. (performs an HTTP
|
||||
// call to a pre-defined endpoint, only valid inside
|
||||
// configured ec2 instances)
|
||||
var defaultAWSCredProviders = []credentials.Provider{
|
||||
&credentials.EnvAWS{},
|
||||
&credentials.FileAWSCredentials{},
|
||||
&credentials.IAM{
|
||||
Client: &http.Client{
|
||||
Transport: minio.NewCustomHTTPTransport(),
|
||||
},
|
||||
},
|
||||
&credentials.EnvMinio{},
|
||||
}
|
||||
|
||||
// newS3 - Initializes a new client by auto probing S3 server signature.
|
||||
func newS3(url string) (*miniogo.Core, error) {
|
||||
if url == "" {
|
||||
url = "https://s3.amazonaws.com"
|
||||
func newS3(urlStr string) (*miniogo.Core, error) {
|
||||
if urlStr == "" {
|
||||
urlStr = "https://s3.amazonaws.com"
|
||||
}
|
||||
|
||||
// Override default params if the host is provided
|
||||
endpoint, secure, err := minio.ParseGatewayEndpoint(url)
|
||||
endpoint, secure, err := minio.ParseGatewayEndpoint(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Chains all credential types, in the following order:
|
||||
// - AWS env vars (i.e. AWS_ACCESS_KEY_ID)
|
||||
// - IAM profile based credentials. (performs an HTTP
|
||||
// call to a pre-defined endpoint, only valid inside
|
||||
// configured ec2 instances)
|
||||
// - AWS creds file (i.e. AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials)
|
||||
// - Static credentials provided by user (i.e. MINIO_ACCESS_KEY)
|
||||
creds := credentials.NewChainCredentials([]credentials.Provider{
|
||||
&credentials.EnvAWS{},
|
||||
&credentials.IAM{
|
||||
Client: &http.Client{
|
||||
Transport: minio.NewCustomHTTPTransport(),
|
||||
},
|
||||
},
|
||||
&credentials.FileAWSCredentials{},
|
||||
&credentials.EnvMinio{},
|
||||
})
|
||||
var creds *credentials.Credentials
|
||||
if isAmazonS3Endpoint(urlStr) {
|
||||
// If we see an Amazon S3 endpoint, then we use more ways to fetch backend credentials.
|
||||
// Specifically IAM style rotating credentials are only supported with AWS S3 endpoint.
|
||||
creds = credentials.NewChainCredentials(defaultAWSCredProviders)
|
||||
} else {
|
||||
creds = credentials.NewChainCredentials(defaultProviders)
|
||||
}
|
||||
|
||||
clnt, err := miniogo.NewWithCredentials(endpoint, creds, secure, "")
|
||||
if err != nil {
|
||||
|
||||
@@ -1,658 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017, 2018 Minio, Inc.
|
||||
*
|
||||
* 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 sia
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
siaBackend = "sia"
|
||||
)
|
||||
|
||||
type siaObjects struct {
|
||||
minio.GatewayUnsupported
|
||||
Address string // Address and port of Sia Daemon.
|
||||
TempDir string // Temporary storage location for file transfers.
|
||||
RootDir string // Root directory to store files on Sia.
|
||||
password string // Sia password for uploading content in authenticated manner.
|
||||
}
|
||||
|
||||
func init() {
|
||||
const siaGatewayTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}} {{if .VisibleFlags}}[FLAGS]{{end}} [SIA_DAEMON_ADDR]
|
||||
{{if .VisibleFlags}}
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
ENVIRONMENT VARIABLES: (Default values in parenthesis)
|
||||
ACCESS:
|
||||
MINIO_ACCESS_KEY: Custom access key (Do not reuse same access keys on all instances)
|
||||
MINIO_SECRET_KEY: Custom secret key (Do not reuse same secret keys on all instances)
|
||||
|
||||
BROWSER:
|
||||
MINIO_BROWSER: To disable web browser access, set this value to "off".
|
||||
|
||||
DOMAIN:
|
||||
MINIO_DOMAIN: To enable virtual-host-style requests, set this value to Minio host domain name.
|
||||
|
||||
CACHE:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
SIA_TEMP_DIR: The name of the local Sia temporary storage directory. (.sia_temp)
|
||||
SIA_API_PASSWORD: API password for Sia daemon. (default is empty)
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for Sia backend.
|
||||
$ {{.HelpName}}
|
||||
|
||||
2. Start minio gateway server for Sia backend with edge caching enabled.
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
Name: siaBackend,
|
||||
Usage: "Sia Decentralized Cloud",
|
||||
Action: siaGatewayMain,
|
||||
CustomHelpTemplate: siaGatewayTemplate,
|
||||
HideHelpCommand: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Handler for 'minio gateway sia' command line.
|
||||
func siaGatewayMain(ctx *cli.Context) {
|
||||
// Validate gateway arguments.
|
||||
host := ctx.Args().First()
|
||||
// Validate gateway arguments.
|
||||
logger.FatalIf(minio.ValidateGatewayArguments(ctx.GlobalString("address"), host), "Invalid argument")
|
||||
|
||||
minio.StartGateway(ctx, &Sia{host})
|
||||
}
|
||||
|
||||
// Sia implements Gateway.
|
||||
type Sia struct {
|
||||
host string // Sia daemon host address
|
||||
}
|
||||
|
||||
// Name implements Gateway interface.
|
||||
func (g *Sia) Name() string {
|
||||
return siaBackend
|
||||
}
|
||||
|
||||
// NewGatewayLayer returns Sia gateway layer, implements ObjectLayer interface to
|
||||
// talk to Sia backend.
|
||||
func (g *Sia) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
||||
sia := &siaObjects{
|
||||
Address: g.host,
|
||||
// RootDir uses access key directly, provides partitioning for
|
||||
// concurrent users talking to same sia daemon.
|
||||
RootDir: creds.AccessKey,
|
||||
TempDir: os.Getenv("SIA_TEMP_DIR"),
|
||||
password: os.Getenv("SIA_API_PASSWORD"),
|
||||
}
|
||||
|
||||
// If Address not provided on command line or ENV, default to:
|
||||
if sia.Address == "" {
|
||||
sia.Address = "127.0.0.1:9980"
|
||||
}
|
||||
|
||||
// If local Sia temp directory not specified, default to:
|
||||
if sia.TempDir == "" {
|
||||
sia.TempDir = ".sia_temp"
|
||||
}
|
||||
|
||||
var err error
|
||||
sia.TempDir, err = filepath.Abs(sia.TempDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the temp directory with proper permissions.
|
||||
// Ignore error when dir already exists.
|
||||
if err = os.MkdirAll(sia.TempDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
colorBlue := color.New(color.FgBlue).SprintfFunc()
|
||||
colorBold := color.New(color.Bold).SprintFunc()
|
||||
|
||||
formatStr := "%" + fmt.Sprintf("%ds", len(sia.Address)+7)
|
||||
logger.StartupMessage(colorBlue("\nSia Configuration:"))
|
||||
logger.StartupMessage(colorBlue(" API Address:") + colorBold(fmt.Sprintf(formatStr, sia.Address)))
|
||||
logger.StartupMessage(colorBlue(" Staging Directory:") + colorBold(fmt.Sprintf(" %s", sia.TempDir)))
|
||||
|
||||
return sia, nil
|
||||
}
|
||||
|
||||
// Production - sia gateway is not ready for production use.
|
||||
func (g *Sia) Production() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// non2xx returns true for non-success HTTP status codes.
|
||||
func non2xx(code int) bool {
|
||||
return code < 200 || code > 299
|
||||
}
|
||||
|
||||
// decodeError returns the api.Error from a API response. This method should
|
||||
// only be called if the response's status code is non-2xx. The error returned
|
||||
// may not be of type api.Error in the event of an error unmarshalling the
|
||||
// JSON.
|
||||
type siaError struct {
|
||||
// Message describes the error in English. Typically it is set to
|
||||
// `err.Error()`. This field is required.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (s siaError) Error() string {
|
||||
return s.Message
|
||||
}
|
||||
|
||||
func decodeError(resp *http.Response) error {
|
||||
// Error is a type that is encoded as JSON and returned in an API response in
|
||||
// the event of an error. Only the Message field is required. More fields may
|
||||
// be added to this struct in the future for better error reporting.
|
||||
var apiErr siaError
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
|
||||
return err
|
||||
}
|
||||
return apiErr
|
||||
}
|
||||
|
||||
// MethodNotSupported - returned if call returned error.
|
||||
type MethodNotSupported struct {
|
||||
method string
|
||||
}
|
||||
|
||||
func (s MethodNotSupported) Error() string {
|
||||
return fmt.Sprintf("API call not recognized: %s", s.method)
|
||||
}
|
||||
|
||||
// apiGet wraps a GET request with a status code check, such that if the GET does
|
||||
// not return 2xx, the error will be read and returned. The response body is
|
||||
// not closed.
|
||||
func apiGet(ctx context.Context, addr, call, apiPassword string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", "http://"+addr+call, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Sia-Agent")
|
||||
if apiPassword != "" {
|
||||
req.SetBasicAuth("", apiPassword)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
minio.CloseResponse(resp.Body)
|
||||
logger.LogIf(ctx, MethodNotSupported{call})
|
||||
return nil, MethodNotSupported{call}
|
||||
}
|
||||
if non2xx(resp.StatusCode) {
|
||||
err := decodeError(resp)
|
||||
minio.CloseResponse(resp.Body)
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// apiPost wraps a POST request with a status code check, such that if the POST
|
||||
// does not return 2xx, the error will be read and returned. The response body
|
||||
// is not closed.
|
||||
func apiPost(ctx context.Context, addr, call, vals, apiPassword string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("POST", "http://"+addr+call, strings.NewReader(vals))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Sia-Agent")
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if apiPassword != "" {
|
||||
req.SetBasicAuth("", apiPassword)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil, MethodNotSupported{call}
|
||||
}
|
||||
|
||||
if non2xx(resp.StatusCode) {
|
||||
err := decodeError(resp)
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// post makes an API call and discards the response. An error is returned if
|
||||
// the response status is not 2xx.
|
||||
func post(ctx context.Context, addr, call, vals, apiPassword string) error {
|
||||
resp, err := apiPost(ctx, addr, call, vals, apiPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
// list makes a lists all the uploaded files, decodes the json response.
|
||||
func list(ctx context.Context, addr string, apiPassword string, obj *renterFiles) error {
|
||||
resp, err := apiGet(ctx, addr, "/renter/files", apiPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer minio.CloseResponse(resp.Body)
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
logger.LogIf(ctx, fmt.Errorf("Expecting a response, but API returned %s", resp.Status))
|
||||
return fmt.Errorf("Expecting a response, but API returned %s", resp.Status)
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(obj)
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// get makes an API call and discards the response. An error is returned if the
|
||||
// responsee status is not 2xx.
|
||||
func get(ctx context.Context, addr, call, apiPassword string) error {
|
||||
resp, err := apiGet(ctx, addr, call, apiPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown saves any gateway metadata to disk
|
||||
// if necessary and reload upon next restart.
|
||||
func (s *siaObjects) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageInfo is not relevant to Sia backend.
|
||||
func (s *siaObjects) StorageInfo(ctx context.Context) (si minio.StorageInfo) {
|
||||
return si
|
||||
}
|
||||
|
||||
// MakeBucket creates a new container on Sia backend.
|
||||
func (s *siaObjects) MakeBucketWithLocation(ctx context.Context, bucket, location string) error {
|
||||
srcFile := path.Join(s.TempDir, minio.MustGetUUID())
|
||||
defer os.Remove(srcFile)
|
||||
|
||||
writer, err := os.Create(srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = io.Copy(writer, bytes.NewReader([]byte(""))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sha256sum := sha256.Sum256([]byte(bucket))
|
||||
var siaObj = path.Join(s.RootDir, bucket, hex.EncodeToString(sha256sum[:]))
|
||||
return post(ctx, s.Address, "/renter/upload/"+siaObj, "source="+srcFile, s.password)
|
||||
}
|
||||
|
||||
// GetBucketInfo gets bucket metadata.
|
||||
func (s *siaObjects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, err error) {
|
||||
sha256sum := sha256.Sum256([]byte(bucket))
|
||||
var siaObj = path.Join(s.RootDir, bucket, hex.EncodeToString(sha256sum[:]))
|
||||
|
||||
dstFile := path.Join(s.TempDir, minio.MustGetUUID())
|
||||
defer os.Remove(dstFile)
|
||||
|
||||
if err := get(ctx, s.Address, "/renter/download/"+siaObj+"?destination="+url.QueryEscape(dstFile), s.password); err != nil {
|
||||
return bi, err
|
||||
}
|
||||
return minio.BucketInfo{Name: bucket}, nil
|
||||
}
|
||||
|
||||
// ListBuckets will detect and return existing buckets on Sia.
|
||||
func (s *siaObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) {
|
||||
sObjs, serr := s.listRenterFiles(ctx, "")
|
||||
if serr != nil {
|
||||
return buckets, serr
|
||||
}
|
||||
|
||||
m := make(set.StringSet)
|
||||
|
||||
prefix := s.RootDir + "/"
|
||||
for _, sObj := range sObjs {
|
||||
if strings.HasPrefix(sObj.SiaPath, prefix) {
|
||||
siaObj := strings.TrimPrefix(sObj.SiaPath, prefix)
|
||||
idx := strings.Index(siaObj, "/")
|
||||
if idx > 0 {
|
||||
m.Add(siaObj[0:idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, bktName := range m.ToSlice() {
|
||||
buckets = append(buckets, minio.BucketInfo{
|
||||
Name: bktName,
|
||||
Created: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
})
|
||||
}
|
||||
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
// DeleteBucket deletes a bucket on Sia.
|
||||
func (s *siaObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
sha256sum := sha256.Sum256([]byte(bucket))
|
||||
var siaObj = path.Join(s.RootDir, bucket, hex.EncodeToString(sha256sum[:]))
|
||||
|
||||
return post(ctx, s.Address, "/renter/delete/"+siaObj, "", s.password)
|
||||
}
|
||||
|
||||
func (s *siaObjects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, err error) {
|
||||
siaObjs, siaErr := s.listRenterFiles(ctx, bucket)
|
||||
if siaErr != nil {
|
||||
return loi, siaErr
|
||||
}
|
||||
|
||||
loi.IsTruncated = false
|
||||
loi.NextMarker = ""
|
||||
|
||||
root := s.RootDir + "/"
|
||||
|
||||
sha256sum := sha256.Sum256([]byte(bucket))
|
||||
// FIXME(harsha) - No paginated output supported for Sia backend right now, only prefix
|
||||
// based filtering. Once list renter files API supports paginated output we can support
|
||||
// paginated results here as well - until then Listing is an expensive operation.
|
||||
for _, sObj := range siaObjs {
|
||||
name := strings.TrimPrefix(sObj.SiaPath, path.Join(root, bucket)+"/")
|
||||
// Skip the file created specially when bucket was created.
|
||||
if name == hex.EncodeToString(sha256sum[:]) {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
loi.Objects = append(loi.Objects, minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: name,
|
||||
Size: int64(sObj.Filesize),
|
||||
IsDir: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||
func (s *siaObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||
var objInfo minio.ObjectInfo
|
||||
objInfo, err = s.GetObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var startOffset, length int64
|
||||
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err := s.GetObject(ctx, bucket, object, startOffset, length, pw, objInfo.ETag, opts)
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
// Setup cleanup function to cause the above go-routine to
|
||||
// exit in case of partial read
|
||||
pipeCloser := func() { pr.Close() }
|
||||
return minio.NewGetObjectReaderFromReader(pr, objInfo, pipeCloser), nil
|
||||
}
|
||||
|
||||
func (s *siaObjects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
dstFile := path.Join(s.TempDir, minio.MustGetUUID())
|
||||
defer os.Remove(dstFile)
|
||||
|
||||
var siaObj = path.Join(s.RootDir, bucket, object)
|
||||
if err := get(ctx, s.Address, "/renter/download/"+siaObj+"?destination="+url.QueryEscape(dstFile), s.password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader, err := os.Open(dstFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
st, err := reader.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size := st.Size()
|
||||
if _, err = reader.Seek(startOffset, os.SEEK_SET); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// For negative length we read everything.
|
||||
if length < 0 {
|
||||
length = size - startOffset
|
||||
}
|
||||
|
||||
bufSize := int64(1 * humanize.MiByte)
|
||||
if bufSize > length {
|
||||
bufSize = length
|
||||
}
|
||||
|
||||
// Reply back invalid range if the input offset and length fall out of range.
|
||||
if startOffset > size || startOffset+length > size {
|
||||
logger.LogIf(ctx, minio.InvalidRange{
|
||||
OffsetBegin: startOffset,
|
||||
OffsetEnd: length,
|
||||
ResourceSize: size,
|
||||
})
|
||||
return minio.InvalidRange{
|
||||
OffsetBegin: startOffset,
|
||||
OffsetEnd: length,
|
||||
ResourceSize: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate a staging buffer.
|
||||
buf := make([]byte, int(bufSize))
|
||||
|
||||
_, err = io.CopyBuffer(writer, io.LimitReader(reader, length), buf)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// findSiaObject retrieves the siaObjectInfo for the Sia object with the given
|
||||
// Sia path name.
|
||||
func (s *siaObjects) findSiaObject(ctx context.Context, bucket, object string) (siaObjectInfo, error) {
|
||||
siaPath := path.Join(s.RootDir, bucket, object)
|
||||
|
||||
sObjs, err := s.listRenterFiles(ctx, "")
|
||||
if err != nil {
|
||||
return siaObjectInfo{}, err
|
||||
}
|
||||
|
||||
for _, sObj := range sObjs {
|
||||
if sObj.SiaPath == siaPath {
|
||||
return sObj, nil
|
||||
}
|
||||
}
|
||||
logger.LogIf(ctx, minio.ObjectNotFound{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
})
|
||||
return siaObjectInfo{}, minio.ObjectNotFound{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
func (s *siaObjects) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (minio.ObjectInfo, error) {
|
||||
so, err := s.findSiaObject(ctx, bucket, object)
|
||||
if err != nil {
|
||||
return minio.ObjectInfo{}, err
|
||||
}
|
||||
|
||||
// Metadata about sia objects is just quite minimal. Sia only provides file size.
|
||||
return minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
ModTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Size: int64(so.Filesize),
|
||||
IsDir: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PutObject creates a new object with the incoming data,
|
||||
func (s *siaObjects) PutObject(ctx context.Context, bucket string, object string, r *minio.PutObjReader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
data := r.Reader
|
||||
srcFile := path.Join(s.TempDir, minio.MustGetUUID())
|
||||
writer, err := os.Create(srcFile)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
wsize, err := io.CopyN(writer, data, data.Size())
|
||||
if err != nil {
|
||||
os.Remove(srcFile)
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
if err = post(ctx, s.Address, "/renter/upload/"+path.Join(s.RootDir, bucket, object), "source="+srcFile, s.password); err != nil {
|
||||
os.Remove(srcFile)
|
||||
return objInfo, err
|
||||
}
|
||||
defer s.deleteTempFileWhenUploadCompletes(ctx, srcFile, bucket, object)
|
||||
|
||||
return minio.ObjectInfo{
|
||||
Name: object,
|
||||
Bucket: bucket,
|
||||
ModTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Size: wsize,
|
||||
ETag: minio.GenETag(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteObject deletes a blob in bucket
|
||||
func (s *siaObjects) DeleteObject(ctx context.Context, bucket string, object string) error {
|
||||
// Tell Sia daemon to delete the object
|
||||
var siaObj = path.Join(s.RootDir, bucket, object)
|
||||
return post(ctx, s.Address, "/renter/delete/"+siaObj, "", s.password)
|
||||
}
|
||||
|
||||
// siaObjectInfo represents object info stored on Sia
|
||||
type siaObjectInfo struct {
|
||||
SiaPath string `json:"siapath"`
|
||||
LocalPath string `json:"localpath"`
|
||||
Filesize uint64 `json:"filesize"`
|
||||
Available bool `json:"available"`
|
||||
Renewing bool `json:"renewing"`
|
||||
Redundancy float64 `json:"redundancy"`
|
||||
UploadProgress float64 `json:"uploadprogress"`
|
||||
}
|
||||
|
||||
type renterFiles struct {
|
||||
Files []siaObjectInfo `json:"files"`
|
||||
}
|
||||
|
||||
// listRenterFiles will return a list of existing objects in the bucket provided
|
||||
func (s *siaObjects) listRenterFiles(ctx context.Context, bucket string) (siaObjs []siaObjectInfo, err error) {
|
||||
// Get list of all renter files
|
||||
var rf renterFiles
|
||||
if err = list(ctx, s.Address, s.password, &rf); err != nil {
|
||||
return siaObjs, err
|
||||
}
|
||||
|
||||
var prefix string
|
||||
root := s.RootDir + "/"
|
||||
if bucket == "" {
|
||||
prefix = root
|
||||
} else {
|
||||
prefix = root + bucket + "/"
|
||||
}
|
||||
|
||||
for _, f := range rf.Files {
|
||||
if strings.HasPrefix(f.SiaPath, prefix) {
|
||||
siaObjs = append(siaObjs, f)
|
||||
}
|
||||
}
|
||||
|
||||
return siaObjs, nil
|
||||
}
|
||||
|
||||
// deleteTempFileWhenUploadCompletes checks the status of a Sia file upload
|
||||
// until it reaches 100% upload progress, then deletes the local temp copy from
|
||||
// the filesystem.
|
||||
func (s *siaObjects) deleteTempFileWhenUploadCompletes(ctx context.Context, tempFile string, bucket, object string) {
|
||||
var soi siaObjectInfo
|
||||
// Wait until 100% upload instead of 1x redundancy because if we delete
|
||||
// after 1x redundancy, the user has to pay the cost of other hosts
|
||||
// redistributing the file.
|
||||
for soi.UploadProgress < 100.0 {
|
||||
var err error
|
||||
soi, err = s.findSiaObject(ctx, bucket, object)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Sleep between each check so that we're not hammering
|
||||
// the Sia daemon with requests.
|
||||
time.Sleep(15 * time.Second)
|
||||
}
|
||||
|
||||
os.Remove(tempFile)
|
||||
}
|
||||
|
||||
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||
func (s *siaObjects) IsCompressionSupported() bool {
|
||||
return false
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
@@ -162,12 +161,6 @@ var (
|
||||
// File to log HTTP request/response headers and body.
|
||||
globalHTTPTraceFile *os.File
|
||||
|
||||
// List of admin peers.
|
||||
globalAdminPeers = adminPeers{}
|
||||
|
||||
// Minio server user agent string.
|
||||
globalServerUserAgent = "Minio/" + ReleaseTag + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
|
||||
|
||||
globalEndpoints EndpointList
|
||||
|
||||
// Global server's network statistics
|
||||
@@ -361,7 +354,6 @@ func getGlobalInfo() (globalInfo map[string]interface{}) {
|
||||
"isEnvRegion": globalIsEnvRegion,
|
||||
"isSSL": globalIsSSL,
|
||||
"serverRegion": globalServerRegion,
|
||||
"serverUserAgent": globalServerUserAgent,
|
||||
// Add more relevant global settings here.
|
||||
}
|
||||
|
||||
|
||||
@@ -56,18 +56,21 @@ func LivenessCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s := objLayer.StorageInfo(ctx)
|
||||
// Gateways don't provide disk info, also handle special case for NAS gateway.
|
||||
if s.Backend.Type == Unknown || s.Backend.Type == BackendFS {
|
||||
// ListBuckets to confirm gateway backend is up
|
||||
if _, err := objLayer.ListBuckets(ctx); err != nil {
|
||||
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||
if !globalIsXL && !globalIsDistXL {
|
||||
s := objLayer.StorageInfo(ctx)
|
||||
// Gateways don't provide disk info.
|
||||
if s.Backend.Type == Unknown {
|
||||
// ListBuckets to confirm gateway backend is up
|
||||
if _, err := objLayer.ListBuckets(ctx); err != nil {
|
||||
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||
return
|
||||
}
|
||||
writeResponse(w, http.StatusOK, nil, mimeNone)
|
||||
return
|
||||
}
|
||||
writeResponse(w, http.StatusOK, nil, mimeNone)
|
||||
return
|
||||
}
|
||||
|
||||
// For FS and Erasure backend, check if local disks are up.
|
||||
var totalLocalDisks int
|
||||
var erroredDisks int
|
||||
for _, endpoint := range globalEndpoints {
|
||||
|
||||
+3
-3
@@ -464,13 +464,13 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||
return args.IsOwner
|
||||
}
|
||||
|
||||
var defaultContextTimeout = 5 * time.Minute
|
||||
var defaultContextTimeout = 30 * time.Second
|
||||
|
||||
// Similar to reloadUsers but updates users, policies maps from etcd server,
|
||||
func reloadEtcdUsers(prefix string, usersMap map[string]auth.Credentials, policyMap map[string]string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||
defer cancel()
|
||||
r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -536,8 +536,8 @@ func reloadEtcdUsers(prefix string, usersMap map[string]auth.Credentials, policy
|
||||
|
||||
func reloadEtcdPolicies(prefix string, cannedPolicyMap map[string]iampolicy.Policy) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||
defer cancel()
|
||||
r, err := globalEtcdClient.Get(ctx, prefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* 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 cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// localAdminClient - represents admin operation to be executed locally.
|
||||
type localAdminClient struct{}
|
||||
|
||||
// SignalService - sends a restart or stop signal to the local server
|
||||
func (lc localAdminClient) SignalService(s serviceSignal) error {
|
||||
switch s {
|
||||
case serviceRestart, serviceStop:
|
||||
globalServiceSignalCh <- s
|
||||
default:
|
||||
return errUnsupportedSignal
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReInitFormat - re-initialize disk format.
|
||||
func (lc localAdminClient) ReInitFormat(dryRun bool) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
return objectAPI.ReloadFormat(context.Background(), dryRun)
|
||||
}
|
||||
|
||||
// ServerInfo - Returns the server info of this server.
|
||||
func (lc localAdminClient) ServerInfo() (sid ServerInfoData, e error) {
|
||||
if globalBootTime.IsZero() {
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
storage := objLayer.StorageInfo(context.Background())
|
||||
|
||||
return ServerInfoData{
|
||||
StorageInfo: storage,
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: globalNotificationSys.GetARNList(),
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling on the local server.
|
||||
func (lc localAdminClient) StartProfiling(profiler string) error {
|
||||
if globalProfiler != nil {
|
||||
globalProfiler.Stop()
|
||||
}
|
||||
prof, err := startProfiler(profiler, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalProfiler = prof
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadProfilingData - stops and returns profiling data of the local server.
|
||||
func (lc localAdminClient) DownloadProfilingData() ([]byte, error) {
|
||||
if globalProfiler == nil {
|
||||
return nil, errors.New("profiler not enabled")
|
||||
}
|
||||
|
||||
profilerPath := globalProfiler.Path()
|
||||
|
||||
// Stop the profiler
|
||||
globalProfiler.Stop()
|
||||
|
||||
profilerFile, err := os.Open(profilerPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(profilerFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
+38
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,17 +26,18 @@ import (
|
||||
|
||||
// lockRequesterInfo stores various info from the client for each lock that is requested.
|
||||
type lockRequesterInfo struct {
|
||||
writer bool // Bool whether write or read lock.
|
||||
node string // Network address of client claiming lock.
|
||||
serviceEndpoint string // RPC path of client claiming lock.
|
||||
uid string // UID to uniquely identify request of client.
|
||||
timestamp time.Time // Timestamp set at the time of initialization.
|
||||
timeLastCheck time.Time // Timestamp for last check of validity of lock.
|
||||
Writer bool // Bool whether write or read lock.
|
||||
Node string // Network address of client claiming lock.
|
||||
ServiceEndpoint string // RPC path of client claiming lock.
|
||||
UID string // UID to uniquely identify request of client.
|
||||
Timestamp time.Time // Timestamp set at the time of initialization.
|
||||
TimeLastCheck time.Time // Timestamp for last check of validity of lock.
|
||||
Source string // Contains line, function and filename reqesting the lock.
|
||||
}
|
||||
|
||||
// isWriteLock returns whether the lock is a write or read lock.
|
||||
func isWriteLock(lri []lockRequesterInfo) bool {
|
||||
return len(lri) == 1 && lri[0].writer
|
||||
return len(lri) == 1 && lri[0].Writer
|
||||
}
|
||||
|
||||
// localLocker implements Dsync.NetLocker
|
||||
@@ -62,12 +63,13 @@ func (l *localLocker) Lock(args dsync.LockArgs) (reply bool, err error) {
|
||||
if !isLockTaken { // No locks held on the given name, so claim write lock
|
||||
l.lockMap[args.Resource] = []lockRequesterInfo{
|
||||
{
|
||||
writer: true,
|
||||
node: args.ServerAddr,
|
||||
serviceEndpoint: args.ServiceEndpoint,
|
||||
uid: args.UID,
|
||||
timestamp: UTCNow(),
|
||||
timeLastCheck: UTCNow(),
|
||||
Writer: true,
|
||||
Node: args.ServerAddr,
|
||||
ServiceEndpoint: args.ServiceEndpoint,
|
||||
Source: args.Source,
|
||||
UID: args.UID,
|
||||
Timestamp: UTCNow(),
|
||||
TimeLastCheck: UTCNow(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -98,12 +100,13 @@ func (l *localLocker) RLock(args dsync.LockArgs) (reply bool, err error) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
lrInfo := lockRequesterInfo{
|
||||
writer: false,
|
||||
node: args.ServerAddr,
|
||||
serviceEndpoint: args.ServiceEndpoint,
|
||||
uid: args.UID,
|
||||
timestamp: UTCNow(),
|
||||
timeLastCheck: UTCNow(),
|
||||
Writer: false,
|
||||
Node: args.ServerAddr,
|
||||
ServiceEndpoint: args.ServiceEndpoint,
|
||||
Source: args.Source,
|
||||
UID: args.UID,
|
||||
Timestamp: UTCNow(),
|
||||
TimeLastCheck: UTCNow(),
|
||||
}
|
||||
if lri, ok := l.lockMap[args.Resource]; ok {
|
||||
if reply = !isWriteLock(lri); reply {
|
||||
@@ -149,3 +152,18 @@ func (l *localLocker) ForceUnlock(args dsync.LockArgs) (reply bool, err error) {
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (l *localLocker) DupLockMap() map[string][]lockRequesterInfo {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
lockCopy := make(map[string][]lockRequesterInfo)
|
||||
for k, v := range l.lockMap {
|
||||
var lockSlice []lockRequesterInfo
|
||||
for _, lockInfo := range v {
|
||||
lockSlice = append(lockSlice, lockInfo)
|
||||
}
|
||||
lockCopy[k] = lockSlice
|
||||
}
|
||||
return lockCopy
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,12 +34,12 @@ type nameLockRequesterInfoPair struct {
|
||||
func (l *localLocker) removeEntryIfExists(nlrip nameLockRequesterInfoPair) {
|
||||
// Check if entry is still in map (could have been removed altogether by 'concurrent' (R)Unlock of last entry)
|
||||
if lri, ok := l.lockMap[nlrip.name]; ok {
|
||||
if !l.removeEntry(nlrip.name, nlrip.lri.uid, &lri) {
|
||||
if !l.removeEntry(nlrip.name, nlrip.lri.UID, &lri) {
|
||||
// Remove failed, in case it is a:
|
||||
if nlrip.lri.writer {
|
||||
if nlrip.lri.Writer {
|
||||
// Writer: this should never happen as the whole (mapped) entry should have been deleted
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("name", nlrip.name)
|
||||
reqInfo.AppendTags("uid", nlrip.lri.uid)
|
||||
reqInfo.AppendTags("uid", nlrip.lri.UID)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, errors.New("Lock maintenance failed to remove entry for write lock (should never happen)"))
|
||||
} // Reader: this can happen if multiple read locks were active and
|
||||
@@ -53,7 +53,7 @@ func (l *localLocker) removeEntryIfExists(nlrip nameLockRequesterInfoPair) {
|
||||
func (l *localLocker) removeEntry(name, uid string, lri *[]lockRequesterInfo) bool {
|
||||
// Find correct entry to remove based on uid.
|
||||
for index, entry := range *lri {
|
||||
if entry.uid == uid {
|
||||
if entry.UID == uid {
|
||||
if len(*lri) == 1 {
|
||||
// Remove the (last) lock.
|
||||
delete(l.lockMap, name)
|
||||
@@ -76,9 +76,9 @@ func getLongLivedLocks(m map[string][]lockRequesterInfo, interval time.Duration)
|
||||
for name, lriArray := range m {
|
||||
for idx := range lriArray {
|
||||
// Check whether enough time has gone by since last check
|
||||
if time.Since(lriArray[idx].timeLastCheck) >= interval {
|
||||
if time.Since(lriArray[idx].TimeLastCheck) >= interval {
|
||||
rslt = append(rslt, nameLockRequesterInfoPair{name: name, lri: lriArray[idx]})
|
||||
lriArray[idx].timeLastCheck = UTCNow()
|
||||
lriArray[idx].TimeLastCheck = UTCNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,12 +29,12 @@ func TestLockRpcServerRemoveEntryIfExists(t *testing.T) {
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
lri := lockRequesterInfo{
|
||||
writer: false,
|
||||
node: "host",
|
||||
serviceEndpoint: "rpc-path",
|
||||
uid: "0123-4567",
|
||||
timestamp: UTCNow(),
|
||||
timeLastCheck: UTCNow(),
|
||||
Writer: false,
|
||||
Node: "host",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "0123-4567",
|
||||
Timestamp: UTCNow(),
|
||||
TimeLastCheck: UTCNow(),
|
||||
}
|
||||
nlrip := nameLockRequesterInfoPair{name: "name", lri: lri}
|
||||
|
||||
@@ -66,20 +66,20 @@ func TestLockRpcServerRemoveEntry(t *testing.T) {
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
lockRequesterInfo1 := lockRequesterInfo{
|
||||
writer: true,
|
||||
node: "host",
|
||||
serviceEndpoint: "rpc-path",
|
||||
uid: "0123-4567",
|
||||
timestamp: UTCNow(),
|
||||
timeLastCheck: UTCNow(),
|
||||
Writer: true,
|
||||
Node: "host",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "0123-4567",
|
||||
Timestamp: UTCNow(),
|
||||
TimeLastCheck: UTCNow(),
|
||||
}
|
||||
lockRequesterInfo2 := lockRequesterInfo{
|
||||
writer: true,
|
||||
node: "host",
|
||||
serviceEndpoint: "rpc-path",
|
||||
uid: "89ab-cdef",
|
||||
timestamp: UTCNow(),
|
||||
timeLastCheck: UTCNow(),
|
||||
Writer: true,
|
||||
Node: "host",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "89ab-cdef",
|
||||
Timestamp: UTCNow(),
|
||||
TimeLastCheck: UTCNow(),
|
||||
}
|
||||
|
||||
locker.ll.lockMap["name"] = []lockRequesterInfo{
|
||||
@@ -128,12 +128,12 @@ func TestLockRpcServerGetLongLivedLocks(t *testing.T) {
|
||||
{
|
||||
lockMap: map[string][]lockRequesterInfo{
|
||||
"test": {{
|
||||
writer: true,
|
||||
node: "10.1.10.21",
|
||||
serviceEndpoint: "/lock/mnt/disk1",
|
||||
uid: "10000112",
|
||||
timestamp: ut,
|
||||
timeLastCheck: ut,
|
||||
Writer: true,
|
||||
Node: "10.1.10.21",
|
||||
ServiceEndpoint: "/lock/mnt/disk1",
|
||||
UID: "10000112",
|
||||
Timestamp: ut,
|
||||
TimeLastCheck: ut,
|
||||
}},
|
||||
},
|
||||
lockInterval: 1 * time.Minute,
|
||||
@@ -143,12 +143,12 @@ func TestLockRpcServerGetLongLivedLocks(t *testing.T) {
|
||||
{
|
||||
lockMap: map[string][]lockRequesterInfo{
|
||||
"test": {{
|
||||
writer: true,
|
||||
node: "10.1.10.21",
|
||||
serviceEndpoint: "/lock/mnt/disk1",
|
||||
uid: "10000112",
|
||||
timestamp: ut,
|
||||
timeLastCheck: ut.Add(-2 * time.Minute),
|
||||
Writer: true,
|
||||
Node: "10.1.10.21",
|
||||
ServiceEndpoint: "/lock/mnt/disk1",
|
||||
UID: "10000112",
|
||||
Timestamp: ut,
|
||||
TimeLastCheck: ut.Add(-2 * time.Minute),
|
||||
}},
|
||||
},
|
||||
lockInterval: 1 * time.Minute,
|
||||
@@ -156,12 +156,12 @@ func TestLockRpcServerGetLongLivedLocks(t *testing.T) {
|
||||
{
|
||||
name: "test",
|
||||
lri: lockRequesterInfo{
|
||||
writer: true,
|
||||
node: "10.1.10.21",
|
||||
serviceEndpoint: "/lock/mnt/disk1",
|
||||
uid: "10000112",
|
||||
timestamp: ut,
|
||||
timeLastCheck: ut.Add(-2 * time.Minute),
|
||||
Writer: true,
|
||||
Node: "10.1.10.21",
|
||||
ServiceEndpoint: "/lock/mnt/disk1",
|
||||
UID: "10000112",
|
||||
Timestamp: ut,
|
||||
TimeLastCheck: ut.Add(-2 * time.Minute),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -94,7 +94,7 @@ func (l *lockRPCReceiver) Expired(args *LockArgs, reply *bool) error {
|
||||
if lri, ok := l.ll.lockMap[args.LockArgs.Resource]; ok {
|
||||
// Check whether uid is still active
|
||||
for _, entry := range lri {
|
||||
if entry.uid == args.LockArgs.UID {
|
||||
if entry.UID == args.LockArgs.UID {
|
||||
*reply = false // When uid found, lock is still active so return not expired.
|
||||
return nil // When uid found *reply is set to true.
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func (l *lockRPCReceiver) lockMaintenance(interval time.Duration) {
|
||||
// Validate if long lived locks are indeed clean.
|
||||
for _, nlrip := range nlripLongLived {
|
||||
// Initialize client based on the long live locks.
|
||||
host, err := xnet.ParseHost(nlrip.lri.node)
|
||||
host, err := xnet.ParseHost(nlrip.lri.Node)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
@@ -136,7 +136,7 @@ func (l *lockRPCReceiver) lockMaintenance(interval time.Duration) {
|
||||
|
||||
// Call back to original server verify whether the lock is still active (based on name & uid)
|
||||
expired, _ := c.Expired(dsync.LockArgs{
|
||||
UID: nlrip.lri.uid,
|
||||
UID: nlrip.lri.UID,
|
||||
Resource: nlrip.name,
|
||||
})
|
||||
|
||||
|
||||
+17
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,10 +32,10 @@ func testLockEquality(lriLeft, lriRight []lockRequesterInfo) bool {
|
||||
}
|
||||
|
||||
for i := 0; i < len(lriLeft); i++ {
|
||||
if lriLeft[i].writer != lriRight[i].writer ||
|
||||
lriLeft[i].node != lriRight[i].node ||
|
||||
lriLeft[i].serviceEndpoint != lriRight[i].serviceEndpoint ||
|
||||
lriLeft[i].uid != lriRight[i].uid {
|
||||
if lriLeft[i].Writer != lriRight[i].Writer ||
|
||||
lriLeft[i].Node != lriRight[i].Node ||
|
||||
lriLeft[i].ServiceEndpoint != lriRight[i].ServiceEndpoint ||
|
||||
lriLeft[i].UID != lriRight[i].UID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -97,10 +97,10 @@ func TestLockRpcServerLock(t *testing.T) {
|
||||
gotLri, _ := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo{
|
||||
{
|
||||
writer: true,
|
||||
node: "node",
|
||||
serviceEndpoint: "rpc-path",
|
||||
uid: "0123-4567",
|
||||
Writer: true,
|
||||
Node: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "0123-4567",
|
||||
},
|
||||
}
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
@@ -213,10 +213,10 @@ func TestLockRpcServerRLock(t *testing.T) {
|
||||
gotLri, _ := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo{
|
||||
{
|
||||
writer: false,
|
||||
node: "node",
|
||||
serviceEndpoint: "rpc-path",
|
||||
uid: "0123-4567",
|
||||
Writer: false,
|
||||
Node: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "0123-4567",
|
||||
},
|
||||
}
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
@@ -315,10 +315,10 @@ func TestLockRpcServerRUnlock(t *testing.T) {
|
||||
gotLri, _ := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo{
|
||||
{
|
||||
writer: false,
|
||||
node: "node",
|
||||
serviceEndpoint: "rpc-path",
|
||||
uid: "89ab-cdef",
|
||||
Writer: false,
|
||||
Node: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "89ab-cdef",
|
||||
},
|
||||
}
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
|
||||
+3
-3
@@ -39,11 +39,11 @@ var (
|
||||
}
|
||||
return fmt.Sprint
|
||||
}()
|
||||
ColorFgRed = func() func(format string, a ...interface{}) string {
|
||||
ColorFgRed = func() func(a ...interface{}) string {
|
||||
if isTerminal() {
|
||||
return color.New(color.FgRed).SprintfFunc()
|
||||
return color.New(color.FgRed).SprintFunc()
|
||||
}
|
||||
return fmt.Sprintf
|
||||
return fmt.Sprint
|
||||
}()
|
||||
ColorBgRed = func() func(format string, a ...interface{}) string {
|
||||
if isTerminal() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -53,9 +53,9 @@ type RWLocker interface {
|
||||
|
||||
// RWLockerSync - internal locker interface.
|
||||
type RWLockerSync interface {
|
||||
GetLock(timeout time.Duration) bool
|
||||
GetLock(id, source string, timeout time.Duration) bool
|
||||
Unlock()
|
||||
GetRLock(timeout time.Duration) bool
|
||||
GetRLock(id, source string, timeout time.Duration) bool
|
||||
RUnlock()
|
||||
}
|
||||
|
||||
@@ -162,9 +162,9 @@ func (n *nsLockMap) lock(volume, path string, lockSource, opsID string, readLock
|
||||
|
||||
// Locking here will block (until timeout).
|
||||
if readLock {
|
||||
locked = nsLk.GetRLock(timeout)
|
||||
locked = nsLk.GetRLock(opsID, lockSource, timeout)
|
||||
} else {
|
||||
locked = nsLk.GetLock(timeout)
|
||||
locked = nsLk.GetLock(opsID, lockSource, timeout)
|
||||
}
|
||||
|
||||
if !locked { // We failed to get the lock
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -124,11 +125,18 @@ func (d *naughtyDisk) ReadFile(volume string, path string, offset int64, buf []b
|
||||
return d.disk.ReadFile(volume, path, offset, buf, verifier)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) PrepareFile(volume, path string, length int64) error {
|
||||
func (d *naughtyDisk) ReadFileStream(volume, path string, offset, length int64) (io.ReadCloser, error) {
|
||||
if err := d.calcError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.disk.ReadFileStream(volume, path, offset, length)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) CreateFile(volume, path string, size int64, reader io.Reader) error {
|
||||
if err := d.calcError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.disk.PrepareFile(volume, path, length)
|
||||
return d.disk.CreateFile(volume, path, size, reader)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) AppendFile(volume, path string, buf []byte) error {
|
||||
|
||||
+307
-50
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,11 +17,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -90,85 +92,290 @@ func (sys *NotificationSys) DeleteBucket(ctx context.Context, bucketName string)
|
||||
}()
|
||||
}
|
||||
|
||||
// ReloadFormat - calls ReloadFormat RPC call on all peers.
|
||||
func (sys *NotificationSys) ReloadFormat(dryRun bool) map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load format in three attempts, before giving up.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.ReloadFormat(dryRun)
|
||||
if err == nil {
|
||||
break
|
||||
// A NotificationGroup is a collection of goroutines working on subtasks that are part of
|
||||
// the same overall task.
|
||||
//
|
||||
// A zero NotificationGroup is valid and does not cancel on error.
|
||||
type NotificationGroup struct {
|
||||
wg sync.WaitGroup
|
||||
errs []NotificationPeerErr
|
||||
}
|
||||
|
||||
// WithNPeers returns a new NotificationGroup with length of errs slice upto nerrs,
|
||||
// upon Wait() errors are returned collected from all tasks.
|
||||
func WithNPeers(nerrs int) *NotificationGroup {
|
||||
return &NotificationGroup{errs: make([]NotificationPeerErr, nerrs)}
|
||||
}
|
||||
|
||||
// Wait blocks until all function calls from the Go method have returned, then
|
||||
// returns the slice of errors from all function calls.
|
||||
func (g *NotificationGroup) Wait() []NotificationPeerErr {
|
||||
g.wg.Wait()
|
||||
return g.errs
|
||||
}
|
||||
|
||||
// Go calls the given function in a new goroutine.
|
||||
//
|
||||
// The first call to return a non-nil error will be
|
||||
// collected in errs slice and returned by Wait().
|
||||
func (g *NotificationGroup) Go(ctx context.Context, f func() error, index int, addr xnet.Host) {
|
||||
g.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer g.wg.Done()
|
||||
g.errs[index] = NotificationPeerErr{
|
||||
Host: addr,
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := f(); err != nil {
|
||||
g.errs[index].Err = err
|
||||
// Last iteration log the error.
|
||||
if i == 2 {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
break
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return errors
|
||||
// ReloadFormat - calls ReloadFormat RPC call on all peers.
|
||||
func (sys *NotificationSys) ReloadFormat(dryRun bool) []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
client := client
|
||||
ng.Go(context.Background(), func() error {
|
||||
return client.ReloadFormat(dryRun)
|
||||
}, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// LoadUsers - calls LoadUsers RPC call on all peers.
|
||||
func (sys *NotificationSys) LoadUsers() map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
var wg sync.WaitGroup
|
||||
func (sys *NotificationSys) LoadUsers() []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load users in three attempts.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.LoadUsers()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}(addr, client)
|
||||
ng.Go(context.Background(), client.LoadUsers, idx, addr)
|
||||
idx++
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return errors
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// LoadCredentials - calls LoadCredentials RPC call on all peers.
|
||||
func (sys *NotificationSys) LoadCredentials() map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
func (sys *NotificationSys) LoadCredentials() []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
ng.Go(context.Background(), client.LoadCredentials, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// StartProfiling - start profiling on remote peers, by initiating a remote RPC.
|
||||
func (sys *NotificationSys) StartProfiling(profiler string) []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
client := client
|
||||
ng.Go(context.Background(), func() error {
|
||||
return client.StartProfiling(profiler)
|
||||
}, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// DownloadProfilingData - download profiling data from all remote peers.
|
||||
func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io.Writer) bool {
|
||||
profilingDataFound := false
|
||||
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of profiling data of all nodes
|
||||
zipWriter := zip.NewWriter(writer)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
data, err := client.DownloadProfilingData()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
profilingDataFound = true
|
||||
|
||||
// Send profiling data to zip as file
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profiling-%s.pprof", addr),
|
||||
size: int64(len(data)),
|
||||
mode: 0600,
|
||||
modTime: UTCNow(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if zerr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, zerr)
|
||||
continue
|
||||
}
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, zerr)
|
||||
continue
|
||||
}
|
||||
if _, err = io.Copy(zwriter, bytes.NewBuffer(data)); err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
data, err := getProfileData()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", thisAddr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
profilingDataFound = true
|
||||
|
||||
// Send profiling data to zip as file
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profiling-%s.pprof", thisAddr),
|
||||
size: int64(len(data)),
|
||||
mode: 0600,
|
||||
modTime: UTCNow(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
if _, err = io.Copy(zwriter, bytes.NewBuffer(data)); err != nil {
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
// SignalService - calls signal service RPC call on all peers.
|
||||
func (sys *NotificationSys) SignalService(sig serviceSignal) []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
client := client
|
||||
ng.Go(context.Background(), func() error {
|
||||
return client.SignalService(sig)
|
||||
}, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// ServerInfo - calls ServerInfo RPC call on all peers.
|
||||
func (sys *NotificationSys) ServerInfo(ctx context.Context) []ServerInfo {
|
||||
var idx = 0
|
||||
serverInfo := make([]ServerInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
go func(idx int, addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load credentials in three attempts.
|
||||
// Try to fetch serverInfo remotely in three attempts.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.LoadCredentials()
|
||||
info, err := client.ServerInfo()
|
||||
if err == nil {
|
||||
break
|
||||
serverInfo[idx] = ServerInfo{
|
||||
Addr: addr.String(),
|
||||
Data: &info,
|
||||
}
|
||||
return
|
||||
}
|
||||
serverInfo[idx] = ServerInfo{
|
||||
Addr: addr.String(),
|
||||
Data: &info,
|
||||
Error: err.Error(),
|
||||
}
|
||||
// Last iteration log the error.
|
||||
if i == 2 {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}(addr, client)
|
||||
}(idx, addr, client)
|
||||
idx++
|
||||
}
|
||||
wg.Wait()
|
||||
return serverInfo
|
||||
}
|
||||
|
||||
return errors
|
||||
// GetLocks - makes GetLocks RPC call on all peers.
|
||||
func (sys *NotificationSys) GetLocks(ctx context.Context) []*PeerLocks {
|
||||
var idx = 0
|
||||
locksResp := make([]*PeerLocks, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(idx int, addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to fetch serverInfo remotely in three attempts.
|
||||
for i := 0; i < 3; i++ {
|
||||
serverLocksResp, err := client.GetLocks()
|
||||
if err == nil {
|
||||
locksResp[idx] = &PeerLocks{
|
||||
Addr: addr.String(),
|
||||
Locks: serverLocksResp,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Last iteration log the error.
|
||||
if i == 2 {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogOnceIf(ctx, err, addr.String())
|
||||
}
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}(idx, addr, client)
|
||||
idx++
|
||||
}
|
||||
wg.Wait()
|
||||
return locksResp
|
||||
}
|
||||
|
||||
// SetBucketPolicy - calls SetBucketPolicy RPC call on all peers.
|
||||
@@ -537,6 +744,56 @@ func (sys *NotificationSys) DrivePerfInfo() []ServerDrivesPerfInfo {
|
||||
return reply
|
||||
}
|
||||
|
||||
// MemUsageInfo - Mem utilization information
|
||||
func (sys *NotificationSys) MemUsageInfo() []ServerMemUsageInfo {
|
||||
reply := make([]ServerMemUsageInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
var i int
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient, idx int) {
|
||||
defer wg.Done()
|
||||
memi, err := client.MemUsageInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
memi.Addr = addr.String()
|
||||
memi.Error = err.Error()
|
||||
}
|
||||
reply[idx] = memi
|
||||
}(addr, client, i)
|
||||
i++
|
||||
}
|
||||
wg.Wait()
|
||||
return reply
|
||||
}
|
||||
|
||||
// CPULoadInfo - CPU utilization information
|
||||
func (sys *NotificationSys) CPULoadInfo() []ServerCPULoadInfo {
|
||||
reply := make([]ServerCPULoadInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
var i int
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient, idx int) {
|
||||
defer wg.Done()
|
||||
cpui, err := client.CPULoadInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
cpui.Addr = addr.String()
|
||||
cpui.Error = err.Error()
|
||||
}
|
||||
reply[idx] = cpui
|
||||
}(addr, client, i)
|
||||
i++
|
||||
}
|
||||
wg.Wait()
|
||||
return reply
|
||||
}
|
||||
|
||||
// NewNotificationSys - creates new notification system object.
|
||||
func NewNotificationSys(config *serverConfig, endpoints EndpointList) *NotificationSys {
|
||||
targetList := getNotificationTargets(config)
|
||||
|
||||
+40
-28
@@ -131,7 +131,7 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.ListBucketAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
}) {
|
||||
_, err = getObjectInfo(ctx, bucket, object, opts)
|
||||
@@ -179,12 +179,18 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
rs := &HTTPRangeSpec{
|
||||
IsSuffixLength: isSuffixLength,
|
||||
Start: offset,
|
||||
End: length,
|
||||
End: offset + length,
|
||||
}
|
||||
|
||||
return getObjectNInfo(ctx, bucket, object, rs, r.Header, readLock, ObjectOptions{})
|
||||
}
|
||||
|
||||
objInfo, err := getObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s3Select.Open(getObject); err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
w.WriteHeader(serr.HTTPStatusCode())
|
||||
@@ -198,12 +204,6 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
s3Select.Evaluate(w)
|
||||
s3Select.Close()
|
||||
|
||||
objInfo, err := getObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get host and port from Request.RemoteAddr.
|
||||
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
|
||||
if err != nil {
|
||||
@@ -281,7 +281,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.ListBucketAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
}) {
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
@@ -463,7 +463,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.ListBucketAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
}) {
|
||||
_, err = getObjectInfo(ctx, bucket, object, opts)
|
||||
@@ -653,12 +653,8 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// TODO: Reject requests where body/payload is present, for now we don't even read it.
|
||||
|
||||
// Copy source path.
|
||||
cpSrcPath, err := url.QueryUnescape(r.Header.Get("X-Amz-Copy-Source"))
|
||||
if err != nil {
|
||||
// Save unescaped string as is.
|
||||
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
|
||||
}
|
||||
// Read escaped copy source path to check for parameters.
|
||||
cpSrcPath := r.Header.Get("X-Amz-Copy-Source")
|
||||
|
||||
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
|
||||
// Regardless of whether you have enabled versioning, each object in your bucket
|
||||
@@ -673,8 +669,18 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// Note that url.Parse does the unescaping
|
||||
cpSrcPath = u.Path
|
||||
}
|
||||
if vid := r.Header.Get("X-Amz-Copy-Source-Version-Id"); vid != "" {
|
||||
// Check if versionId header was added, if yes then check if
|
||||
// its non "null" value, we should error out since we do not support
|
||||
// any versions other than "null".
|
||||
if vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
|
||||
// If source object is empty or bucket is empty, reply back invalid copy source.
|
||||
@@ -699,7 +705,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
var srcOpts, dstOpts ObjectOptions
|
||||
srcOpts, err = copySrcEncryptionOpts(ctx, r, srcBucket, srcObject)
|
||||
srcOpts, err := copySrcEncryptionOpts(ctx, r, srcBucket, srcObject)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
@@ -1349,6 +1355,11 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, bucket, object); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
@@ -1364,11 +1375,6 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, bucket, object); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
if _, err = objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
@@ -1466,12 +1472,8 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Copy source path.
|
||||
cpSrcPath, err := url.QueryUnescape(r.Header.Get("X-Amz-Copy-Source"))
|
||||
if err != nil {
|
||||
// Save unescaped string as is.
|
||||
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
|
||||
}
|
||||
// Read escaped copy source path to check for parameters.
|
||||
cpSrcPath := r.Header.Get("X-Amz-Copy-Source")
|
||||
|
||||
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
|
||||
// Regardless of whether you have enabled versioning, each object in your bucket
|
||||
@@ -1486,8 +1488,18 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// Note that url.Parse does the unescaping
|
||||
cpSrcPath = u.Path
|
||||
}
|
||||
if vid := r.Header.Get("X-Amz-Copy-Source-Version-Id"); vid != "" {
|
||||
// Check if X-Amz-Copy-Source-Version-Id header was added, if yes then check if
|
||||
// its non "null" value, we should error out since we do not support
|
||||
// any versions other than "null".
|
||||
if vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
|
||||
// If source object is empty or bucket is empty, reply back invalid copy source.
|
||||
|
||||
+103
-9
@@ -25,6 +25,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"io/ioutil"
|
||||
@@ -1523,14 +1524,15 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
|
||||
// test cases with inputs and expected result for Copy Object.
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
|
||||
copySourceRange string // data for "X-Amz-Copy-Source-Range" header, contains the byte range offsets of data to be copied.
|
||||
uploadID string // uploadID of the transaction.
|
||||
invalidPartNumber bool // Sets an invalid multipart.
|
||||
maximumPartNumber bool // Sets a maximum parts.
|
||||
accessKey string
|
||||
secretKey string
|
||||
bucketName string
|
||||
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
|
||||
copySourceVersionID string // data for "X-Amz-Copy-Source-Version-Id" header.
|
||||
copySourceRange string // data for "X-Amz-Copy-Source-Range" header, contains the byte range offsets of data to be copied.
|
||||
uploadID string // uploadID of the transaction.
|
||||
invalidPartNumber bool // Sets an invalid multipart.
|
||||
maximumPartNumber bool // Sets a maximum parts.
|
||||
accessKey string
|
||||
secretKey string
|
||||
// expected output.
|
||||
expectedRespStatus int
|
||||
}{
|
||||
@@ -1694,6 +1696,44 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 14, copy part 1 from from newObject1 with null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 15, copy part 1 from from newObject1 with non null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
// Test case - 16, copy part 1 from from newObject1 with null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 16, copy part 1 from from newObject1 with non null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
@@ -1717,6 +1757,9 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
if testCase.copySourceHeader != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copySourceRange != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-Range", testCase.copySourceRange)
|
||||
}
|
||||
@@ -1752,6 +1795,9 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
if testCase.copySourceHeader != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copySourceRange != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-Range", testCase.copySourceRange)
|
||||
}
|
||||
@@ -1802,7 +1848,10 @@ func TestAPICopyObjectHandler(t *testing.T) {
|
||||
func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
|
||||
objectName := "test-object"
|
||||
objectName := "test?object" // use file with ? to test URL parsing...
|
||||
if runtime.GOOS == "windows" {
|
||||
objectName = "test-object" // ...except on Windows
|
||||
}
|
||||
// object used for anonymous HTTP request test.
|
||||
anonObject := "anon-object"
|
||||
var err error
|
||||
@@ -1861,6 +1910,7 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
bucketName string
|
||||
newObjectName string // name of the newly copied object.
|
||||
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
|
||||
copySourceVersionID string // data for "X-Amz-Copy-Source-Version-Id" header.
|
||||
copyModifiedHeader string // data for "X-Amz-Copy-Source-If-Modified-Since" header
|
||||
copyUnmodifiedHeader string // data for "X-Amz-Copy-Source-If-Unmodified-Since" header
|
||||
metadataGarbage bool
|
||||
@@ -2071,6 +2121,44 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 17, copy metadata from newObject1 with null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 18, copy metadata from newObject1 with non null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
// Test case - 19, copy metadata from newObject1 with null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 20, copy metadata from newObject1 with non null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
@@ -2089,6 +2177,9 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
if testCase.copySourceHeader != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copyModifiedHeader != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-If-Modified-Since", testCase.copyModifiedHeader)
|
||||
}
|
||||
@@ -2150,6 +2241,9 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
if testCase.copySourceHeader != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copyModifiedHeader != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-If-Modified-Since", testCase.copyModifiedHeader)
|
||||
}
|
||||
|
||||
+60
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -150,6 +150,65 @@ func (rpcClient *PeerRPCClient) DrivePerfInfo() (ServerDrivesPerfInfo, error) {
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// MemUsageInfo - returns mem utilization info for remote server
|
||||
func (rpcClient *PeerRPCClient) MemUsageInfo() (ServerMemUsageInfo, error) {
|
||||
args := AuthArgs{}
|
||||
var reply ServerMemUsageInfo
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".MemUsageInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// CPULoadInfo - returns cpu performance info for remote server
|
||||
func (rpcClient *PeerRPCClient) CPULoadInfo() (ServerCPULoadInfo, error) {
|
||||
args := AuthArgs{}
|
||||
var reply ServerCPULoadInfo
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".CPULoadInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling on the remote server.
|
||||
func (rpcClient *PeerRPCClient) StartProfiling(profiler string) error {
|
||||
args := StartProfilingArgs{Profiler: profiler}
|
||||
reply := VoidReply{}
|
||||
return rpcClient.Call(peerServiceName+".StartProfiling", &args, &reply)
|
||||
}
|
||||
|
||||
// DownloadProfilingData - download already running profiling on the remote server.
|
||||
func (rpcClient *PeerRPCClient) DownloadProfilingData() ([]byte, error) {
|
||||
args := AuthArgs{}
|
||||
var reply []byte
|
||||
err := rpcClient.Call(peerServiceName+".DownloadProfilingData", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// SignalService - calls load server info RPC.
|
||||
func (rpcClient *PeerRPCClient) SignalService(sig serviceSignal) error {
|
||||
args := SignalServiceArgs{Sig: sig}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(peerServiceName+".SignalService", &args, &reply)
|
||||
}
|
||||
|
||||
// ServerInfo - calls load server info RPC.
|
||||
func (rpcClient *PeerRPCClient) ServerInfo() (ServerInfoData, error) {
|
||||
args := AuthArgs{}
|
||||
reply := ServerInfoData{}
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".ServerInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// GetLocksResp stores various info from the client for each lock that is requested.
|
||||
type GetLocksResp map[string][]lockRequesterInfo
|
||||
|
||||
// GetLocks - returns the lock information on the server to which the RPC call is made.
|
||||
func (rpcClient *PeerRPCClient) GetLocks() (resp GetLocksResp, err error) {
|
||||
err = rpcClient.Call(peerServiceName+".GetLocks", &AuthArgs{}, &resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// NewPeerRPCClient - returns new peer RPC client.
|
||||
func NewPeerRPCClient(host *xnet.Host) (*PeerRPCClient, error) {
|
||||
scheme := "http"
|
||||
|
||||
+156
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2018, 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -30,7 +32,7 @@ import (
|
||||
)
|
||||
|
||||
const peerServiceName = "Peer"
|
||||
const peerServiceSubPath = "/s3/remote"
|
||||
const peerServiceSubPath = "/peer/remote"
|
||||
|
||||
var peerServicePath = path.Join(minioReservedBucketPath, peerServiceSubPath)
|
||||
|
||||
@@ -118,7 +120,8 @@ type ListenBucketNotificationArgs struct {
|
||||
Addr xnet.Host `json:"addr"`
|
||||
}
|
||||
|
||||
// ListenBucketNotification - handles listen bucket notification RPC call. It creates PeerRPCClient target which pushes requested events to target in remote peer.
|
||||
// ListenBucketNotification - handles listen bucket notification RPC call.
|
||||
// It creates PeerRPCClient target which pushes requested events to target in remote peer.
|
||||
func (receiver *peerRPCReceiver) ListenBucketNotification(args *ListenBucketNotificationArgs, reply *VoidReply) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
@@ -245,7 +248,156 @@ func (receiver *peerRPCReceiver) DrivePerfInfo(args *AuthArgs, reply *ServerDriv
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
*reply = localEndpointsPerf(globalEndpoints)
|
||||
*reply = localEndpointsDrivePerf(globalEndpoints)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CPULoadInfo - handles cpu performance RPC call
|
||||
func (receiver *peerRPCReceiver) CPULoadInfo(args *AuthArgs, reply *ServerCPULoadInfo) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
*reply = localEndpointsCPULoad(globalEndpoints)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MemUsageInfo - handles mem utilization RPC call
|
||||
func (receiver *peerRPCReceiver) MemUsageInfo(args *AuthArgs, reply *ServerMemUsageInfo) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
*reply = localEndpointsMemUsage(globalEndpoints)
|
||||
return nil
|
||||
}
|
||||
|
||||
// uptimes - used to sort uptimes in chronological order.
|
||||
type uptimes []time.Duration
|
||||
|
||||
func (ts uptimes) Len() int {
|
||||
return len(ts)
|
||||
}
|
||||
|
||||
func (ts uptimes) Less(i, j int) bool {
|
||||
return ts[i] < ts[j]
|
||||
}
|
||||
|
||||
func (ts uptimes) Swap(i, j int) {
|
||||
ts[i], ts[j] = ts[j], ts[i]
|
||||
}
|
||||
|
||||
// getPeerUptimes - returns the uptime.
|
||||
func getPeerUptimes(serverInfo []ServerInfo) time.Duration {
|
||||
// In a single node Erasure or FS backend setup the uptime of
|
||||
// the setup is the uptime of the single minio server
|
||||
// instance.
|
||||
if !globalIsDistXL {
|
||||
return UTCNow().Sub(globalBootTime)
|
||||
}
|
||||
|
||||
var times []time.Duration
|
||||
|
||||
for _, info := range serverInfo {
|
||||
if info.Error != "" {
|
||||
continue
|
||||
}
|
||||
times = append(times, info.Data.Properties.Uptime)
|
||||
}
|
||||
|
||||
// Sort uptimes in chronological order.
|
||||
sort.Sort(uptimes(times))
|
||||
|
||||
// Return the latest time as the uptime.
|
||||
return times[0]
|
||||
}
|
||||
|
||||
// StartProfilingArgs - holds the RPC argument for StartingProfiling RPC call
|
||||
type StartProfilingArgs struct {
|
||||
AuthArgs
|
||||
Profiler string
|
||||
}
|
||||
|
||||
// StartProfiling - profiling server receiver.
|
||||
func (receiver *peerRPCReceiver) StartProfiling(args *StartProfilingArgs, reply *VoidReply) error {
|
||||
if globalProfiler != nil {
|
||||
globalProfiler.Stop()
|
||||
}
|
||||
var err error
|
||||
globalProfiler, err = startProfiler(args.Profiler, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// DownloadProfilingData - download profiling data.
|
||||
func (receiver *peerRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[]byte) error {
|
||||
var err error
|
||||
*reply, err = getProfileData()
|
||||
return err
|
||||
}
|
||||
|
||||
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
|
||||
|
||||
// SignalServiceArgs - send event RPC arguments.
|
||||
type SignalServiceArgs struct {
|
||||
AuthArgs
|
||||
Sig serviceSignal
|
||||
}
|
||||
|
||||
// SignalService - signal service receiver.
|
||||
func (receiver *peerRPCReceiver) SignalService(args *SignalServiceArgs, reply *VoidReply) error {
|
||||
switch args.Sig {
|
||||
case serviceRestart, serviceStop:
|
||||
globalServiceSignalCh <- args.Sig
|
||||
default:
|
||||
return errUnsupportedSignal
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo - server info receiver.
|
||||
func (receiver *peerRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) error {
|
||||
if globalBootTime.IsZero() {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// Server info data.
|
||||
*reply = ServerInfoData{
|
||||
StorageInfo: objLayer.StorageInfo(context.Background()),
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: globalNotificationSys.GetARNList(),
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLocks - Get Locks receiver.
|
||||
func (receiver *peerRPCReceiver) GetLocks(args *AuthArgs, reply *GetLocksResp) error {
|
||||
if globalBootTime.IsZero() {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// Locks data.
|
||||
*reply = globalLockServer.ll.DupLockMap()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+15
-2
@@ -30,6 +30,7 @@ import (
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
@@ -183,12 +184,24 @@ func NewPolicySys() *PolicySys {
|
||||
}
|
||||
}
|
||||
|
||||
func getConditionValues(request *http.Request, locationConstraint string) map[string][]string {
|
||||
func getConditionValues(request *http.Request, locationConstraint string, username string) map[string][]string {
|
||||
currTime := UTCNow()
|
||||
principalType := func() string {
|
||||
if username != "" {
|
||||
return "User"
|
||||
}
|
||||
return "Anonymous"
|
||||
}()
|
||||
args := map[string][]string{
|
||||
"SourceIp": {handlers.GetSourceIP(request)},
|
||||
"CurrenTime": {currTime.Format(event.AMZTimeFormat)},
|
||||
"EpochTime": {fmt.Sprintf("%d", currTime.Unix())},
|
||||
"principaltype": {principalType},
|
||||
"SecureTransport": {fmt.Sprintf("%t", request.TLS != nil)},
|
||||
"SourceIp": {handlers.GetSourceIP(request)},
|
||||
"UserAgent": {request.UserAgent()},
|
||||
"Referer": {request.Referer()},
|
||||
"userid": {username},
|
||||
"username": {username},
|
||||
}
|
||||
|
||||
for key, values := range request.Header {
|
||||
|
||||
+124
-16
@@ -303,9 +303,10 @@ func (s *posix) IsOnline() bool {
|
||||
// DiskInfo is an extended type which returns current
|
||||
// disk usage per path.
|
||||
type DiskInfo struct {
|
||||
Total uint64
|
||||
Free uint64
|
||||
Used uint64
|
||||
Total uint64
|
||||
Free uint64
|
||||
Used uint64
|
||||
RootDisk bool
|
||||
}
|
||||
|
||||
// DiskInfo provides current information about disk space usage,
|
||||
@@ -319,12 +320,17 @@ func (s *posix) DiskInfo() (info DiskInfo, err error) {
|
||||
if !s.diskMount {
|
||||
used = atomic.LoadUint64(&s.totalUsed)
|
||||
}
|
||||
return DiskInfo{
|
||||
Total: di.Total,
|
||||
Free: di.Free,
|
||||
Used: used,
|
||||
}, nil
|
||||
|
||||
rootDisk, err := disk.IsRootDisk(s.diskPath)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
return DiskInfo{
|
||||
Total: di.Total,
|
||||
Free: di.Free,
|
||||
Used: used,
|
||||
RootDisk: rootDisk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getVolDir - will convert incoming volume names to
|
||||
@@ -928,11 +934,99 @@ func (s *posix) openFile(volume, path string, mode int) (f *os.File, err error)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// PrepareFile - run prior actions before creating a new file for optimization purposes
|
||||
// Currently we use fallocate when available to avoid disk fragmentation as much as possible
|
||||
func (s *posix) PrepareFile(volume, path string, fileSize int64) (err error) {
|
||||
// It doesn't make sense to create a negative-sized file
|
||||
if fileSize < -1 {
|
||||
// Just like io.LimitedReader but supports Close() to be compatible with io.ReadCloser that is
|
||||
// returned by posix.ReadFileStream()
|
||||
type posixLimitedReader struct {
|
||||
io.LimitedReader
|
||||
}
|
||||
|
||||
func (l *posixLimitedReader) Close() error {
|
||||
c, ok := l.R.(io.Closer)
|
||||
if !ok {
|
||||
return errUnexpected
|
||||
}
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
// ReadFileStream - Returns the read stream of the file.
|
||||
func (s *posix) ReadFileStream(volume, path string, offset, length int64) (io.ReadCloser, error) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err == errFaultyDisk {
|
||||
atomic.AddInt32(&s.ioErrCount, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
if offset < 0 {
|
||||
return nil, errInvalidArgument
|
||||
}
|
||||
|
||||
if atomic.LoadInt32(&s.ioErrCount) > maxAllowedIOError {
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
|
||||
if err = s.checkDiskFound(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
volumeDir, err := s.getVolDir(volume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Stat a volume entry.
|
||||
_, err = os.Stat((volumeDir))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errVolumeNotFound
|
||||
} else if isSysErrIO(err) {
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate effective path length before reading.
|
||||
filePath := pathJoin(volumeDir, path)
|
||||
if err = checkPathLength((filePath)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open the file for reading.
|
||||
file, err := os.Open((filePath))
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return nil, errFileNotFound
|
||||
case os.IsPermission(err):
|
||||
return nil, errFileAccessDenied
|
||||
case isSysErrNotDir(err):
|
||||
return nil, errFileAccessDenied
|
||||
case isSysErrIO(err):
|
||||
return nil, errFaultyDisk
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
st, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify it is a regular file, otherwise subsequent Seek is
|
||||
// undefined.
|
||||
if !st.Mode().IsRegular() {
|
||||
return nil, errIsNotRegular
|
||||
}
|
||||
|
||||
if _, err = file.Seek(offset, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &posixLimitedReader{io.LimitedReader{R: file, N: length}}, nil
|
||||
}
|
||||
|
||||
// CreateFile - creates the file.
|
||||
func (s *posix) CreateFile(volume, path string, fileSize int64, r io.Reader) (err error) {
|
||||
if fileSize < 0 {
|
||||
return errInvalidArgument
|
||||
}
|
||||
|
||||
@@ -954,13 +1048,13 @@ func (s *posix) PrepareFile(volume, path string, fileSize int64) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create file if not found
|
||||
w, err := s.openFile(volume, path, os.O_CREATE|os.O_APPEND|os.O_WRONLY)
|
||||
// Create file if not found. Note that it is created with os.O_EXCL flag as the file
|
||||
// always is supposed to be created in the tmp directory with a unique file name.
|
||||
w, err := s.openFile(volume, path, os.O_CREATE|os.O_APPEND|os.O_WRONLY|os.O_EXCL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close upon return.
|
||||
defer w.Close()
|
||||
|
||||
var e error
|
||||
@@ -983,6 +1077,20 @@ func (s *posix) PrepareFile(volume, path string, fileSize int64) (err error) {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
bufp := s.pool.Get().(*[]byte)
|
||||
defer s.pool.Put(bufp)
|
||||
|
||||
n, err := io.CopyBuffer(w, r, *bufp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n < fileSize {
|
||||
return errLessData
|
||||
}
|
||||
if n > fileSize {
|
||||
return errMoreData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1438,113 +1438,6 @@ func TestPosixAppendFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPosix posix.PrepareFile()
|
||||
func TestPosixPrepareFile(t *testing.T) {
|
||||
// create posix test setup
|
||||
posixStorage, path, err := newPosixTestSetup()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
if err = posixStorage.MakeVol("success-vol"); err != nil {
|
||||
t.Fatalf("Unable to create volume, %s", err)
|
||||
}
|
||||
|
||||
if err = os.Mkdir(slashpath.Join(path, "success-vol", "object-as-dir"), 0777); err != nil {
|
||||
t.Fatalf("Unable to create directory, %s", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
fileName string
|
||||
expectedErr error
|
||||
}{
|
||||
{"myobject", nil},
|
||||
{"path/to/my/object", nil},
|
||||
// TestPosix to append to previously created file.
|
||||
{"myobject", nil},
|
||||
// TestPosix to use same path of previously created file.
|
||||
{"path/to/my/testobject", nil},
|
||||
{"object-as-dir", errIsNotRegular},
|
||||
// path segment uses previously uploaded object.
|
||||
{"myobject/testobject", errFileAccessDenied},
|
||||
// One path segment length is > 255 chars long.
|
||||
{"path/to/my/object0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", errFileNameTooLong},
|
||||
}
|
||||
|
||||
// Add path length > 1024 test specially as OS X system does not support 1024 long path.
|
||||
err = errFileNameTooLong
|
||||
if runtime.GOOS != "darwin" {
|
||||
err = nil
|
||||
}
|
||||
// path length is 1024 chars long.
|
||||
testCases = append(testCases, struct {
|
||||
fileName string
|
||||
expectedErr error
|
||||
}{"level0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001/level0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002/level0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003/object000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", err})
|
||||
|
||||
for i, testCase := range testCases {
|
||||
if err = posixStorage.PrepareFile("success-vol", testCase.fileName, 16); err != testCase.expectedErr {
|
||||
t.Errorf("Case: %d, expected: %s, got: %s", i, testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPosix for permission denied.
|
||||
if runtime.GOOS != globalWindowsOSName {
|
||||
permDeniedDir := createPermDeniedFile(t)
|
||||
defer removePermDeniedFile(permDeniedDir)
|
||||
|
||||
var posixPermStorage StorageAPI
|
||||
// Initialize posix storage layer for permission denied error.
|
||||
_, err = newPosix(permDeniedDir)
|
||||
if err != nil && !os.IsPermission(err) {
|
||||
t.Fatalf("Unable to initialize posix, %s", err)
|
||||
}
|
||||
|
||||
if err = os.Chmod(permDeniedDir, 0755); err != nil {
|
||||
t.Fatalf("Unable to change permission to temporary directory %v. %v", permDeniedDir, err)
|
||||
}
|
||||
|
||||
posixPermStorage, err = newPosix(permDeniedDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize posix, %s", err)
|
||||
}
|
||||
|
||||
if err = posixPermStorage.PrepareFile("mybucket", "myobject", 16); err != errFileAccessDenied {
|
||||
t.Fatalf("expected: Permission error, got: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPosix case with invalid volume name.
|
||||
// A valid volume name should be atleast of size 3.
|
||||
err = posixStorage.PrepareFile("bn", "yes", 16)
|
||||
if err != errVolumeNotFound {
|
||||
t.Fatalf("expected: \"Invalid argument error\", got: \"%s\"", err)
|
||||
}
|
||||
|
||||
// TestPosix case with invalid file size which should be strictly positive
|
||||
err = posixStorage.PrepareFile("success-vol", "yes", -3)
|
||||
if err != errInvalidArgument {
|
||||
t.Fatalf("should fail: %v", err)
|
||||
}
|
||||
|
||||
// TestPosix case with IO error count > max limit.
|
||||
|
||||
// setting ioErrCnt to 6.
|
||||
// should fail with errFaultyDisk.
|
||||
if posixType, ok := posixStorage.(*posix); ok {
|
||||
// setting the io error count from as specified in the test case.
|
||||
posixType.ioErrCount = int32(6)
|
||||
err = posixType.PrepareFile("abc", "yes", 16)
|
||||
if err != errFaultyDisk {
|
||||
t.Fatalf("Expected \"Faulty Disk\", got: \"%s\"", err)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("Expected the StorageAPI to be of type *posix")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPosix posix.RenameFile()
|
||||
func TestPosixRenameFile(t *testing.T) {
|
||||
// create posix test setup
|
||||
|
||||
+58
-19
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
)
|
||||
|
||||
var printEndpointError = func() func(Endpoint, error) {
|
||||
@@ -49,18 +50,26 @@ var printEndpointError = func() func(Endpoint, error) {
|
||||
|
||||
// Migrates backend format of local disks.
|
||||
func formatXLMigrateLocalEndpoints(endpoints EndpointList) error {
|
||||
for _, endpoint := range endpoints {
|
||||
g := errgroup.WithNErrs(len(endpoints))
|
||||
for index, endpoint := range endpoints {
|
||||
if !endpoint.IsLocal {
|
||||
continue
|
||||
}
|
||||
formatPath := pathJoin(endpoint.Path, minioMetaBucket, formatConfigFile)
|
||||
if _, err := os.Stat(formatPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
epPath := endpoints[index].Path
|
||||
formatPath := pathJoin(epPath, minioMetaBucket, formatConfigFile)
|
||||
if _, err := os.Stat(formatPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := formatXLMigrate(endpoint.Path); err != nil {
|
||||
return formatXLMigrate(epPath)
|
||||
}, index)
|
||||
}
|
||||
for _, err := range g.Wait() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -69,21 +78,51 @@ func formatXLMigrateLocalEndpoints(endpoints EndpointList) error {
|
||||
|
||||
// Cleans up tmp directory of local disks.
|
||||
func formatXLCleanupTmpLocalEndpoints(endpoints EndpointList) error {
|
||||
for _, endpoint := range endpoints {
|
||||
g := errgroup.WithNErrs(len(endpoints))
|
||||
for index, endpoint := range endpoints {
|
||||
if !endpoint.IsLocal {
|
||||
continue
|
||||
}
|
||||
formatPath := pathJoin(endpoint.Path, minioMetaBucket, formatConfigFile)
|
||||
if _, err := os.Stat(formatPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
epPath := endpoints[index].Path
|
||||
// If disk is not formatted there is nothing to be cleaned up.
|
||||
formatPath := pathJoin(epPath, minioMetaBucket, formatConfigFile)
|
||||
if _, err := os.Stat(formatPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := removeAll(pathJoin(endpoint.Path, minioMetaTmpBucket)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mkdirAll(pathJoin(endpoint.Path, minioMetaTmpBucket), 0777); err != nil {
|
||||
if _, err := os.Stat(pathJoin(epPath, minioMetaTmpBucket+"-old")); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Need to move temporary objects left behind from previous run of minio
|
||||
// server to a unique directory under `minioMetaTmpBucket-old` to clean
|
||||
// up `minioMetaTmpBucket` for the current run.
|
||||
//
|
||||
// /disk1/.minio.sys/tmp-old/
|
||||
// |__ 33a58b40-aecc-4c9f-a22f-ff17bfa33b62
|
||||
// |__ e870a2c1-d09c-450c-a69c-6eaa54a89b3e
|
||||
//
|
||||
// In this example, `33a58b40-aecc-4c9f-a22f-ff17bfa33b62` directory contains
|
||||
// temporary objects from one of the previous runs of minio server.
|
||||
if err := renameAll(pathJoin(epPath, minioMetaTmpBucket),
|
||||
pathJoin(epPath, minioMetaTmpBucket+"-old", mustGetUUID())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Removal of tmp-old folder is backgrounded completely.
|
||||
go removeAll(pathJoin(epPath, minioMetaTmpBucket+"-old"))
|
||||
|
||||
return mkdirAll(pathJoin(epPath, minioMetaTmpBucket), 0777)
|
||||
}, index)
|
||||
}
|
||||
for _, err := range g.Wait() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -42,7 +42,7 @@ type Client struct {
|
||||
}
|
||||
|
||||
// Call - make a REST call.
|
||||
func (c *Client) Call(method string, values url.Values, body io.Reader) (reply io.ReadCloser, err error) {
|
||||
func (c *Client) Call(method string, values url.Values, body io.Reader, length int64) (reply io.ReadCloser, err error) {
|
||||
req, err := http.NewRequest(http.MethodPost, c.url.String()+"/"+method+"?"+values.Encode(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -50,7 +50,9 @@ func (c *Client) Call(method string, values url.Values, body io.Reader) (reply i
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.newAuthToken())
|
||||
req.Header.Set("X-Minio-Time", time.Now().UTC().Format(time.RFC3339))
|
||||
|
||||
if length > 0 {
|
||||
req.ContentLength = length
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+2
-5
@@ -41,7 +41,7 @@ func registerDistXLRouters(router *mux.Router, endpoints EndpointList) {
|
||||
// Register distributed namespace lock.
|
||||
registerDistNSLockRouter(router)
|
||||
|
||||
// Register S3 peer communication router.
|
||||
// Register peer communication router.
|
||||
registerPeerRPCRouter(router)
|
||||
}
|
||||
|
||||
@@ -104,11 +104,8 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
|
||||
// Add STS router always.
|
||||
registerSTSRouter(router)
|
||||
|
||||
// Add Admin RPC router
|
||||
registerAdminRPCRouter(router)
|
||||
|
||||
// Add Admin router, all APIs are enabled in server mode.
|
||||
registerAdminRouter(router, true)
|
||||
registerAdminRouter(router, true, true)
|
||||
|
||||
// Add healthcheck router
|
||||
registerHealthCheckRouter(router)
|
||||
|
||||
@@ -293,9 +293,6 @@ func serverMain(ctx *cli.Context) {
|
||||
logger.Fatal(uiErrUnexpectedError(err), "Unable to configure one of server's RPC services")
|
||||
}
|
||||
|
||||
// Initialize Admin Peers inter-node communication only in distributed setup.
|
||||
initGlobalAdminPeers(globalEndpoints)
|
||||
|
||||
var getCert certs.GetCertificateFunc
|
||||
if globalTLSCerts != nil {
|
||||
getCert = globalTLSCerts.GetCertificate
|
||||
|
||||
@@ -82,6 +82,9 @@ var errMinDiskSize = errors.New("The disk size is less than the minimum threshol
|
||||
// errLessData - returned when less data available than what was requested.
|
||||
var errLessData = errors.New("less data available than what was requested")
|
||||
|
||||
// errMoreData = returned when more data was sent by the caller than what it was supposed to.
|
||||
var errMoreData = errors.New("more data was sent than what was advertised")
|
||||
|
||||
// hashMisMatchError - represents a bit-rot hash verification failure
|
||||
// error.
|
||||
type hashMismatchError struct {
|
||||
|
||||
@@ -41,8 +41,9 @@ type StorageAPI interface {
|
||||
// File operations.
|
||||
ListDir(volume, dirPath string, count int) ([]string, error)
|
||||
ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error)
|
||||
PrepareFile(volume string, path string, len int64) (err error)
|
||||
AppendFile(volume string, path string, buf []byte) (err error)
|
||||
CreateFile(volume, path string, size int64, reader io.Reader) error
|
||||
ReadFileStream(volume, path string, offset, length int64) (io.ReadCloser, error)
|
||||
RenameFile(srcVolume, srcPath, dstVolume, dstPath string) error
|
||||
StatFile(volume string, path string) (file FileInfo, err error)
|
||||
DeleteFile(volume string, path string) (err error)
|
||||
|
||||
+51
-26
@@ -29,6 +29,9 @@ import (
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/rest"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
@@ -101,6 +104,15 @@ func toStorageErr(err error) error {
|
||||
case errServerTimeMismatch.Error():
|
||||
return errServerTimeMismatch
|
||||
}
|
||||
if strings.Contains(err.Error(), "Bitrot verification mismatch") {
|
||||
var expected string
|
||||
var received string
|
||||
fmt.Sscanf(err.Error(), "Bitrot verification mismatch - expected %s received %s", &expected, &received)
|
||||
// Go's Sscanf %s scans "," that comes after the expected hash, hence remove it. Providing "," in the format string does not help.
|
||||
expected = strings.TrimSuffix(expected, ",")
|
||||
bitrotErr := hashMismatchError{expected, received}
|
||||
return bitrotErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -115,11 +127,11 @@ type storageRESTClient struct {
|
||||
// Wrapper to restClient.Call to handle network errors, in case of network error the connection is makred disconnected
|
||||
// permanently. The only way to restore the storage connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
||||
// after verifying format.json
|
||||
func (client *storageRESTClient) call(method string, values url.Values, body io.Reader) (respBody io.ReadCloser, err error) {
|
||||
func (client *storageRESTClient) call(method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) {
|
||||
if !client.connected {
|
||||
return nil, errDiskNotFound
|
||||
}
|
||||
respBody, err = client.restClient.Call(method, values, body)
|
||||
respBody, err = client.restClient.Call(method, values, body, length)
|
||||
if err == nil {
|
||||
return respBody, nil
|
||||
}
|
||||
@@ -148,7 +160,7 @@ func (client *storageRESTClient) LastError() error {
|
||||
|
||||
// DiskInfo - fetch disk information for a remote disk.
|
||||
func (client *storageRESTClient) DiskInfo() (info DiskInfo, err error) {
|
||||
respBody, err := client.call(storageRESTMethodDiskInfo, nil, nil)
|
||||
respBody, err := client.call(storageRESTMethodDiskInfo, nil, nil, -1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -161,14 +173,14 @@ func (client *storageRESTClient) DiskInfo() (info DiskInfo, err error) {
|
||||
func (client *storageRESTClient) MakeVol(volume string) (err error) {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
respBody, err := client.call(storageRESTMethodMakeVol, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodMakeVol, values, nil, -1)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListVols - List all volumes on a remote disk.
|
||||
func (client *storageRESTClient) ListVols() (volinfo []VolInfo, err error) {
|
||||
respBody, err := client.call(storageRESTMethodListVols, nil, nil)
|
||||
respBody, err := client.call(storageRESTMethodListVols, nil, nil, -1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -181,7 +193,7 @@ func (client *storageRESTClient) ListVols() (volinfo []VolInfo, err error) {
|
||||
func (client *storageRESTClient) StatVol(volume string) (volInfo VolInfo, err error) {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
respBody, err := client.call(storageRESTMethodStatVol, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodStatVol, values, nil, -1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -194,18 +206,7 @@ func (client *storageRESTClient) StatVol(volume string) (volInfo VolInfo, err er
|
||||
func (client *storageRESTClient) DeleteVol(volume string) (err error) {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
respBody, err := client.call(storageRESTMethodDeleteVol, values, nil)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
|
||||
// PrepareFile - to fallocate() disk space for a file.
|
||||
func (client *storageRESTClient) PrepareFile(volume, path string, length int64) error {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
values.Set(storageRESTLength, strconv.Itoa(int(length)))
|
||||
respBody, err := client.call(storageRESTMethodPrepareFile, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodDeleteVol, values, nil, -1)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
@@ -216,7 +217,17 @@ func (client *storageRESTClient) AppendFile(volume, path string, buffer []byte)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
reader := bytes.NewBuffer(buffer)
|
||||
respBody, err := client.call(storageRESTMethodAppendFile, values, reader)
|
||||
respBody, err := client.call(storageRESTMethodAppendFile, values, reader, -1)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
|
||||
func (client *storageRESTClient) CreateFile(volume, path string, length int64, r io.Reader) error {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
values.Set(storageRESTLength, strconv.Itoa(int(length)))
|
||||
respBody, err := client.call(storageRESTMethodCreateFile, values, r, length)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
@@ -227,7 +238,7 @@ func (client *storageRESTClient) WriteAll(volume, path string, buffer []byte) er
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
reader := bytes.NewBuffer(buffer)
|
||||
respBody, err := client.call(storageRESTMethodWriteAll, values, reader)
|
||||
respBody, err := client.call(storageRESTMethodWriteAll, values, reader, -1)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
@@ -237,7 +248,7 @@ func (client *storageRESTClient) StatFile(volume, path string) (info FileInfo, e
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
respBody, err := client.call(storageRESTMethodStatFile, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodStatFile, values, nil, -1)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
@@ -251,7 +262,7 @@ func (client *storageRESTClient) ReadAll(volume, path string) ([]byte, error) {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
respBody, err := client.call(storageRESTMethodReadAll, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodReadAll, values, nil, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -259,6 +270,20 @@ func (client *storageRESTClient) ReadAll(volume, path string) ([]byte, error) {
|
||||
return ioutil.ReadAll(respBody)
|
||||
}
|
||||
|
||||
// ReadFileStream - returns a reader for the requested file.
|
||||
func (client *storageRESTClient) ReadFileStream(volume, path string, offset, length int64) (io.ReadCloser, error) {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
values.Set(storageRESTOffset, strconv.Itoa(int(offset)))
|
||||
values.Set(storageRESTLength, strconv.Itoa(int(length)))
|
||||
respBody, err := client.call(storageRESTMethodReadFileStream, values, nil, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
// ReadFile - reads section of a file.
|
||||
func (client *storageRESTClient) ReadFile(volume, path string, offset int64, buffer []byte, verifier *BitrotVerifier) (int64, error) {
|
||||
values := make(url.Values)
|
||||
@@ -273,7 +298,7 @@ func (client *storageRESTClient) ReadFile(volume, path string, offset int64, buf
|
||||
values.Set(storageRESTBitrotAlgo, "")
|
||||
values.Set(storageRESTBitrotHash, "")
|
||||
}
|
||||
respBody, err := client.call(storageRESTMethodReadFile, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodReadFile, values, nil, -1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -288,7 +313,7 @@ func (client *storageRESTClient) ListDir(volume, dirPath string, count int) (ent
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTDirPath, dirPath)
|
||||
values.Set(storageRESTCount, strconv.Itoa(count))
|
||||
respBody, err := client.call(storageRESTMethodListDir, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodListDir, values, nil, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -302,7 +327,7 @@ func (client *storageRESTClient) DeleteFile(volume, path string) error {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
respBody, err := client.call(storageRESTMethodDeleteFile, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodDeleteFile, values, nil, -1)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
@@ -314,7 +339,7 @@ func (client *storageRESTClient) RenameFile(srcVolume, srcPath, dstVolume, dstPa
|
||||
values.Set(storageRESTSrcPath, srcPath)
|
||||
values.Set(storageRESTDstVolume, dstVolume)
|
||||
values.Set(storageRESTDstPath, dstPath)
|
||||
respBody, err := client.call(storageRESTMethodRenameFile, values, nil)
|
||||
respBody, err := client.call(storageRESTMethodRenameFile, values, nil, -1)
|
||||
defer CloseResponse(respBody)
|
||||
return err
|
||||
}
|
||||
|
||||
+11
-10
@@ -16,7 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
const storageRESTVersion = "v2"
|
||||
const storageRESTVersion = "v3"
|
||||
const storageRESTPath = minioReservedBucketPath + "/storage/" + storageRESTVersion + "/"
|
||||
|
||||
const (
|
||||
@@ -26,15 +26,16 @@ const (
|
||||
storageRESTMethodDeleteVol = "deletevol"
|
||||
storageRESTMethodListVols = "listvols"
|
||||
|
||||
storageRESTMethodPrepareFile = "preparefile"
|
||||
storageRESTMethodAppendFile = "appendfile"
|
||||
storageRESTMethodWriteAll = "writeall"
|
||||
storageRESTMethodStatFile = "statfile"
|
||||
storageRESTMethodReadAll = "readall"
|
||||
storageRESTMethodReadFile = "readfile"
|
||||
storageRESTMethodListDir = "listdir"
|
||||
storageRESTMethodDeleteFile = "deletefile"
|
||||
storageRESTMethodRenameFile = "renamefile"
|
||||
storageRESTMethodAppendFile = "appendfile"
|
||||
storageRESTMethodCreateFile = "createfile"
|
||||
storageRESTMethodWriteAll = "writeall"
|
||||
storageRESTMethodStatFile = "statfile"
|
||||
storageRESTMethodReadAll = "readall"
|
||||
storageRESTMethodReadFile = "readfile"
|
||||
storageRESTMethodReadFileStream = "readfilestream"
|
||||
storageRESTMethodListDir = "listdir"
|
||||
storageRESTMethodDeleteFile = "deletefile"
|
||||
storageRESTMethodRenameFile = "renamefile"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+55
-28
@@ -133,27 +133,7 @@ func (s *storageRESTServer) DeleteVolHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareFileHandler - fallocate() space for a file.
|
||||
func (s *storageRESTServer) PrepareFileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
volume := vars[storageRESTVolume]
|
||||
filePath := vars[storageRESTFilePath]
|
||||
fileSizeStr := vars[storageRESTLength]
|
||||
fileSize, err := strconv.Atoi(fileSizeStr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
err = s.storage.PrepareFile(volume, filePath, int64(fileSize))
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// AppendFileHandler - append to a file.
|
||||
// AppendFileHandler - append data from the request to the file specified.
|
||||
func (s *storageRESTServer) AppendFileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
return
|
||||
@@ -162,11 +142,6 @@ func (s *storageRESTServer) AppendFileHandler(w http.ResponseWriter, r *http.Req
|
||||
volume := vars[storageRESTVolume]
|
||||
filePath := vars[storageRESTFilePath]
|
||||
|
||||
if r.ContentLength < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, r.ContentLength)
|
||||
_, err := io.ReadFull(r.Body, buf)
|
||||
if err != nil {
|
||||
@@ -179,6 +154,27 @@ func (s *storageRESTServer) AppendFileHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFileHandler - fallocate() space for a file and copy the contents from the request.
|
||||
func (s *storageRESTServer) CreateFileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
volume := vars[storageRESTVolume]
|
||||
filePath := vars[storageRESTFilePath]
|
||||
|
||||
fileSizeStr := vars[storageRESTLength]
|
||||
fileSize, err := strconv.Atoi(fileSizeStr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
err = s.storage.CreateFile(volume, filePath, int64(fileSize), r.Body)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAllHandler - write to file all content.
|
||||
func (s *storageRESTServer) WriteAllHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
@@ -285,6 +281,34 @@ func (s *storageRESTServer) ReadFileHandler(w http.ResponseWriter, r *http.Reque
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
// ReadFileHandler - read section of a file.
|
||||
func (s *storageRESTServer) ReadFileStreamHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
volume := vars[storageRESTVolume]
|
||||
filePath := vars[storageRESTFilePath]
|
||||
offset, err := strconv.Atoi(vars[storageRESTOffset])
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
length, err := strconv.Atoi(vars[storageRESTLength])
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
rc, err := s.storage.ReadFileStream(volume, filePath, int64(offset), int64(length))
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
w.Header().Set("Content-Length", strconv.Itoa(length))
|
||||
io.Copy(w, rc)
|
||||
}
|
||||
|
||||
// ListDirHandler - list a directory.
|
||||
func (s *storageRESTServer) ListDirHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
@@ -359,18 +383,21 @@ func registerStorageRESTHandlers(router *mux.Router, endpoints EndpointList) {
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodDeleteVol).HandlerFunc(httpTraceHdrs(server.DeleteVolHandler)).Queries(restQueries(storageRESTVolume)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodListVols).HandlerFunc(httpTraceHdrs(server.ListVolsHandler))
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodPrepareFile).HandlerFunc(httpTraceHdrs(server.PrepareFileHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTLength)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodAppendFile).HandlerFunc(httpTraceHdrs(server.AppendFileHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodWriteAll).HandlerFunc(httpTraceHdrs(server.WriteAllHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodCreateFile).HandlerFunc(httpTraceHdrs(server.CreateFileHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTLength)...)
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodStatFile).HandlerFunc(httpTraceHdrs(server.StatFileHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodReadAll).HandlerFunc(httpTraceHdrs(server.ReadAllHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodReadFile).HandlerFunc(httpTraceHdrs(server.ReadFileHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTOffset, storageRESTLength, storageRESTBitrotAlgo, storageRESTBitrotHash)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodReadFileStream).HandlerFunc(httpTraceHdrs(server.ReadFileStreamHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTOffset, storageRESTLength)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodListDir).HandlerFunc(httpTraceHdrs(server.ListDirHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTDirPath, storageRESTCount)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodDeleteFile).HandlerFunc(httpTraceHdrs(server.DeleteFileHandler)).
|
||||
|
||||
@@ -365,38 +365,6 @@ func testStorageAPIReadFile(t *testing.T, storage StorageAPI) {
|
||||
}
|
||||
}
|
||||
|
||||
func testStorageAPIPrepareFile(t *testing.T, storage StorageAPI) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
globalServerConfig = tmpGlobalServerConfig
|
||||
}()
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
err := storage.MakeVol("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
volumeName string
|
||||
objectName string
|
||||
expectErr bool
|
||||
}{
|
||||
{"foo", "myobject", false},
|
||||
// volume not found error.
|
||||
{"bar", "myobject", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := storage.PrepareFile(testCase.volumeName, testCase.objectName, 1)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testStorageAPIAppendFile(t *testing.T, storage StorageAPI) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
@@ -648,17 +616,6 @@ func TestStorageRESTClientReadFile(t *testing.T) {
|
||||
testStorageAPIReadFile(t, restClient)
|
||||
}
|
||||
|
||||
func TestStorageRESTClientPrepareFile(t *testing.T) {
|
||||
httpServer, restClient, prevGlobalServerConfig, endpointPath := newStorageRESTHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
defer os.RemoveAll(endpointPath)
|
||||
|
||||
testStorageAPIPrepareFile(t, restClient)
|
||||
}
|
||||
|
||||
func TestStorageRESTClientAppendFile(t *testing.T) {
|
||||
httpServer, restClient, prevGlobalServerConfig, endpointPath := newStorageRESTHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
|
||||
+8
-8
@@ -123,10 +123,10 @@ func (sts *stsAPIHandlers) AssumeRoleWithWebIdentity(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload temp users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,10 +207,10 @@ func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload temp users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ func init() {
|
||||
// Set system resources to maximum.
|
||||
setMaxResources()
|
||||
|
||||
logger.Disable = true
|
||||
// Uncomment the following line to see trace logs during unit tests.
|
||||
// logger.AddTarget(console.New())
|
||||
}
|
||||
|
||||
// concurreny level for certain parallel tests.
|
||||
|
||||
+4
-3
@@ -119,9 +119,10 @@ Refer to the link https://github.com/minio/minio/tree/master/docs/erasure/storag
|
||||
uiErrInvalidAddressFlag = newUIErrFn(
|
||||
"--address input is invalid",
|
||||
"Please check --address parameter",
|
||||
`--address binds to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname (default port is ':9000')
|
||||
Examples: --address ':443'
|
||||
--address '172.16.34.31:9000'`,
|
||||
`--address binds to a specific ADDRESS:PORT, ADDRESS can be an IPv4/IPv6 address or hostname (default port is ':9000')
|
||||
Examples: --address ':443'
|
||||
--address '172.16.34.31:9000'
|
||||
--address '[fe80::da00:a6c8:e3ae:ddd7]:9000'`,
|
||||
)
|
||||
|
||||
uiErrInvalidFSEndpoint = newUIErrFn(
|
||||
|
||||
@@ -208,6 +208,26 @@ func (p profilerWrapper) Path() string {
|
||||
return p.pathFn()
|
||||
}
|
||||
|
||||
// Returns current profile data, returns error if there is no active
|
||||
// profiling in progress. Stops an active profile.
|
||||
func getProfileData() ([]byte, error) {
|
||||
if globalProfiler == nil {
|
||||
return nil, errors.New("profiler not enabled")
|
||||
}
|
||||
|
||||
profilerPath := globalProfiler.Path()
|
||||
|
||||
// Stop the profiler
|
||||
globalProfiler.Stop()
|
||||
|
||||
profilerFile, err := os.Open(profilerPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ioutil.ReadAll(profilerFile)
|
||||
}
|
||||
|
||||
// Starts a profiler returns nil if profiler is not enabled, caller needs to handle this.
|
||||
func startProfiler(profilerType, dirPath string) (interface {
|
||||
Stop()
|
||||
|
||||
+33
-25
@@ -287,6 +287,12 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
|
||||
return toJSONError(authErr)
|
||||
}
|
||||
|
||||
// Set prefix value for "s3:prefix" policy conditionals.
|
||||
r.Header.Set("prefix", "")
|
||||
|
||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||
r.Header.Set("delimiter", slashSeparator)
|
||||
|
||||
// If etcd, dns federation configured list buckets from etcd.
|
||||
if globalDNSConfig != nil {
|
||||
dnsBuckets, err := globalDNSConfig.List()
|
||||
@@ -300,7 +306,7 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.ListBucketAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
}) {
|
||||
@@ -320,7 +326,7 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.ListBucketAction,
|
||||
BucketName: bucket.Name,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
}) {
|
||||
@@ -416,16 +422,17 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
|
||||
claims, owner, authErr := webRequestAuthenticate(r)
|
||||
if authErr != nil {
|
||||
if authErr == errNoAuthToken {
|
||||
// Add this for checking ListObjects conditional.
|
||||
if args.Prefix != "" {
|
||||
r.Header.Set("prefix", args.Prefix)
|
||||
}
|
||||
// Set prefix value for "s3:prefix" policy conditionals.
|
||||
r.Header.Set("prefix", args.Prefix)
|
||||
|
||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||
r.Header.Set("delimiter", slashSeparator)
|
||||
|
||||
// Check if anonymous (non-owner) has access to download objects.
|
||||
readable := globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.ListBucketAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
})
|
||||
|
||||
@@ -433,7 +440,7 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
|
||||
writable := globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
@@ -454,16 +461,17 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
|
||||
|
||||
// For authenticated users apply IAM policy.
|
||||
if authErr == nil {
|
||||
// Add this for checking ListObjects conditional.
|
||||
if args.Prefix != "" {
|
||||
r.Header.Set("prefix", args.Prefix)
|
||||
}
|
||||
// Set prefix value for "s3:prefix" policy conditionals.
|
||||
r.Header.Set("prefix", args.Prefix)
|
||||
|
||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||
r.Header.Set("delimiter", slashSeparator)
|
||||
|
||||
readable := globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.ListBucketAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
})
|
||||
|
||||
@@ -471,7 +479,7 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.PutObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
@@ -603,7 +611,7 @@ next:
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.DeleteObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
@@ -620,7 +628,7 @@ next:
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.DeleteObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
@@ -752,10 +760,10 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
|
||||
|
||||
if errs := globalNotificationSys.LoadCredentials(); len(errs) != 0 {
|
||||
reply.PeerErrMsgs = make(map[string]string)
|
||||
for host, err := range errs {
|
||||
err = fmt.Errorf("Unable to update credentials on server %v: %v", host, err)
|
||||
for _, nerr := range errs {
|
||||
err = fmt.Errorf("Unable to update credentials on server %v: %v", nerr.Host, nerr.Err)
|
||||
logger.LogIf(context.Background(), err)
|
||||
reply.PeerErrMsgs[host.String()] = err.Error()
|
||||
reply.PeerErrMsgs[nerr.Host.String()] = err.Error()
|
||||
}
|
||||
} else {
|
||||
reply.Token, err = authenticateWeb(creds.AccessKey, creds.SecretKey)
|
||||
@@ -845,7 +853,7 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
@@ -864,7 +872,7 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.PutObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
@@ -1032,7 +1040,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
@@ -1051,7 +1059,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.GetObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
@@ -1187,7 +1195,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
ObjectName: pathJoin(args.Prefix, object),
|
||||
}) {
|
||||
@@ -1208,7 +1216,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.GetObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: pathJoin(args.Prefix, object),
|
||||
}) {
|
||||
|
||||
+6
-14
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/gorilla/handlers"
|
||||
@@ -57,7 +56,7 @@ func assetFS() *assetfs.AssetFS {
|
||||
}
|
||||
|
||||
// specialAssets are files which are unique files not embedded inside index_bundle.js.
|
||||
const specialAssets = ".*index_bundle.*.js$|.*loader.css$|.*logo.svg$|.*firefox.png$|.*safari.png$|.*chrome.png$|.*favicon.ico$"
|
||||
const specialAssets = "index_bundle.*.js|loader.css|logo.svg|firefox.png|safari.png|chrome.png|favicon.ico"
|
||||
|
||||
// registerWebRouter - registers web router for serving minio browser.
|
||||
func registerWebRouter(router *mux.Router) error {
|
||||
@@ -92,21 +91,14 @@ func registerWebRouter(router *mux.Router) error {
|
||||
webBrowserRouter.Methods("GET").Path("/download/{bucket}/{object:.+}").Queries("token", "{token:.*}").HandlerFunc(httpTraceHdrs(web.Download))
|
||||
webBrowserRouter.Methods("POST").Path("/zip").Queries("token", "{token:.*}").HandlerFunc(httpTraceHdrs(web.DownloadZip))
|
||||
|
||||
// Add compression for assets.
|
||||
h := http.FileServer(assetFS())
|
||||
compressedAssets := handlers.CompressHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
idx := strings.LastIndex(r.URL.Path, slashSeparator)
|
||||
if idx != -1 {
|
||||
r.URL.Path = r.URL.Path[idx+1:]
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}))
|
||||
// Create compressed assets handler
|
||||
compressAssets := handlers.CompressHandler(http.StripPrefix(minioReservedBucketPath, http.FileServer(assetFS())))
|
||||
|
||||
// Serve javascript files and favicon from assets.
|
||||
webBrowserRouter.Path(fmt.Sprintf("/{assets:%s}", specialAssets)).Handler(compressedAssets)
|
||||
webBrowserRouter.Path(fmt.Sprintf("/{assets:%s}", specialAssets)).Handler(compressAssets)
|
||||
|
||||
// Serve index.html for rest of the requests.
|
||||
webBrowserRouter.Path("/{index:.*}").Handler(indexHandler{http.StripPrefix(minioReservedBucketPath, h)})
|
||||
// Serve index.html from assets for rest of the requests.
|
||||
webBrowserRouter.Path("/{index:.*}").Handler(indexHandler{compressAssets})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1029,6 +1029,18 @@ func (s *xlSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.HealRe
|
||||
}
|
||||
}(storageDisks)
|
||||
|
||||
for i, disk := range storageDisks {
|
||||
info, err := disk.DiskInfo()
|
||||
if err != nil {
|
||||
storageDisks[i] = nil
|
||||
}
|
||||
if info.RootDisk {
|
||||
// We should not heal on root disk. i.e in a situation where the minio-administrator has unmounted a
|
||||
// defective drive we should not heal a path on the root disk.
|
||||
storageDisks[i] = nil
|
||||
}
|
||||
}
|
||||
|
||||
formats, sErrs := loadFormatXLAll(storageDisks)
|
||||
if err = checkFormatXLValues(formats); err != nil {
|
||||
return madmin.HealResultItem{}, err
|
||||
|
||||
+16
-23
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -161,7 +160,6 @@ func getLatestXLMeta(ctx context.Context, partsMetadata []xlMetaV1, errs []error
|
||||
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []xlMetaV1, errs []error, bucket,
|
||||
object string) ([]StorageAPI, []error) {
|
||||
availableDisks := make([]StorageAPI, len(onlineDisks))
|
||||
buffer := []byte{}
|
||||
dataErrs := make([]error, len(onlineDisks))
|
||||
|
||||
for i, onlineDisk := range onlineDisks {
|
||||
@@ -170,31 +168,26 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
continue
|
||||
}
|
||||
|
||||
erasureInfo := partsMetadata[i].Erasure
|
||||
erasure, err := NewErasure(ctx, erasureInfo.DataBlocks, erasureInfo.ParityBlocks, erasureInfo.BlockSize)
|
||||
if err != nil {
|
||||
dataErrs[i] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// disk has a valid xl.json but may not have all the
|
||||
// parts. This is considered an outdated disk, since
|
||||
// it needs healing too.
|
||||
for _, part := range partsMetadata[i].Parts {
|
||||
partPath := filepath.Join(object, part.Name)
|
||||
checksumInfo := partsMetadata[i].Erasure.GetChecksumInfo(part.Name)
|
||||
verifier := NewBitrotVerifier(checksumInfo.Algorithm, checksumInfo.Hash)
|
||||
|
||||
// verification happens even if a 0-length
|
||||
// buffer is passed
|
||||
_, hErr := onlineDisk.ReadFile(bucket, partPath, 0, buffer, verifier)
|
||||
|
||||
isCorrupt := false
|
||||
if hErr != nil {
|
||||
isCorrupt = strings.HasPrefix(hErr.Error(), "Bitrot verification mismatch - expected ")
|
||||
}
|
||||
switch {
|
||||
case isCorrupt:
|
||||
fallthrough
|
||||
case hErr == errFileNotFound, hErr == errVolumeNotFound:
|
||||
dataErrs[i] = hErr
|
||||
break
|
||||
case hErr != nil:
|
||||
logger.LogIf(ctx, hErr)
|
||||
dataErrs[i] = hErr
|
||||
checksumInfo := erasureInfo.GetChecksumInfo(part.Name)
|
||||
tillOffset := erasure.ShardFileTillOffset(0, part.Size, part.Size)
|
||||
err = bitrotCheckFile(onlineDisk, bucket, pathJoin(object, part.Name), tillOffset, checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
|
||||
if err != nil {
|
||||
isCorrupt := strings.HasPrefix(err.Error(), "Bitrot verification mismatch - expected ")
|
||||
if !isCorrupt && err != errFileNotFound && err != errVolumeNotFound {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
dataErrs[i] = err
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -85,28 +86,6 @@ func TestCommonTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// partsMetaFromModTimes - returns slice of modTimes given metadata of
|
||||
// an object part.
|
||||
func partsMetaFromModTimes(modTimes []time.Time, algorithm BitrotAlgorithm, checksums []ChecksumInfo) []xlMetaV1 {
|
||||
var partsMetadata []xlMetaV1
|
||||
for _, modTime := range modTimes {
|
||||
partsMetadata = append(partsMetadata, xlMetaV1{
|
||||
Erasure: ErasureInfo{
|
||||
Checksums: checksums,
|
||||
},
|
||||
Stat: statInfo{
|
||||
ModTime: modTime,
|
||||
},
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Name: "part.1",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return partsMetadata
|
||||
}
|
||||
|
||||
// TestListOnlineDisks - checks if listOnlineDisks and outDatedDisks
|
||||
// are consistent with each other.
|
||||
func TestListOnlineDisks(t *testing.T) {
|
||||
@@ -205,12 +184,6 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
t.Fatalf("Failed to putObject %v", err)
|
||||
}
|
||||
|
||||
// Fetch xl.json from first disk to construct partsMetadata for the tests.
|
||||
xlMeta, err := readXLMeta(context.Background(), xlDisks[0], bucket, object)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to read xl.json %v", i+1, err)
|
||||
}
|
||||
|
||||
tamperedIndex := -1
|
||||
switch test._tamperBackend {
|
||||
case deletePart:
|
||||
@@ -240,17 +213,25 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
// and check if that disk
|
||||
// appears in outDatedDisks.
|
||||
tamperedIndex = index
|
||||
dErr := xlDisks[index].AppendFile(bucket, filepath.Join(object, "part.1"), []byte("corruption"))
|
||||
if dErr != nil {
|
||||
t.Fatalf("Test %d: Failed to append corrupting data at the end of file %s - %v",
|
||||
i+1, filepath.Join(object, "part.1"), dErr)
|
||||
filePath := pathJoin(xlDisks[index].String(), bucket, object, "part.1")
|
||||
f, err := os.OpenFile(filePath, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open %s: %s\n", filePath, err)
|
||||
}
|
||||
f.Write([]byte("oops")) // Will cause bitrot error
|
||||
f.Close()
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
partsMetadata := partsMetaFromModTimes(test.modTimes, DefaultBitrotAlgorithm, xlMeta.Erasure.Checksums)
|
||||
partsMetadata, errs := readAllXLMetadata(context.Background(), xlDisks, bucket, object)
|
||||
for i := range partsMetadata {
|
||||
if errs[i] != nil {
|
||||
t.Fatalf("Test %d: expected error to be nil: %s", i+1, errs[i].Error())
|
||||
}
|
||||
partsMetadata[i].Stat.ModTime = test.modTimes[i]
|
||||
}
|
||||
|
||||
onlineDisks, modTime := listOnlineDisks(xlDisks, partsMetadata, test.errs)
|
||||
if !modTime.Equal(test.expectedTime) {
|
||||
@@ -303,6 +284,29 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
t.Fatalf("Failed to read xl meta data %v", reducedErr)
|
||||
}
|
||||
|
||||
// Test that all disks are returned without any failures with
|
||||
// unmodified meta data
|
||||
partsMetadata, errs = readAllXLMetadata(ctx, xlDisks, bucket, object)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read xl meta data %v", err)
|
||||
}
|
||||
|
||||
filteredDisks, errs := disksWithAllParts(ctx, xlDisks, partsMetadata, errs, bucket, object)
|
||||
|
||||
if len(filteredDisks) != len(xlDisks) {
|
||||
t.Errorf("Unexpected number of disks: %d", len(filteredDisks))
|
||||
}
|
||||
|
||||
for diskIndex, disk := range filteredDisks {
|
||||
if errs[diskIndex] != nil {
|
||||
t.Errorf("Unexpected error %s", errs[diskIndex])
|
||||
}
|
||||
|
||||
if disk == nil {
|
||||
t.Errorf("Disk erroneously filtered, diskIndex: %d", diskIndex)
|
||||
}
|
||||
}
|
||||
|
||||
diskFailures := make(map[int]string)
|
||||
// key = disk index, value = part name with hash mismatch
|
||||
diskFailures[0] = "part.3"
|
||||
@@ -310,15 +314,21 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
diskFailures[15] = "part.2"
|
||||
|
||||
for diskIndex, partName := range diskFailures {
|
||||
for index, info := range partsMetadata[diskIndex].Erasure.Checksums {
|
||||
for _, info := range partsMetadata[diskIndex].Erasure.Checksums {
|
||||
if info.Name == partName {
|
||||
partsMetadata[diskIndex].Erasure.Checksums[index].Hash[0]++
|
||||
filePath := pathJoin(xlDisks[diskIndex].String(), bucket, object, partName)
|
||||
f, err := os.OpenFile(filePath, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open %s: %s\n", filePath, err)
|
||||
}
|
||||
f.Write([]byte("oops")) // Will cause bitrot error
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errs = make([]error, len(xlDisks))
|
||||
filteredDisks, errs := disksWithAllParts(ctx, xlDisks, partsMetadata, errs, bucket, object)
|
||||
filteredDisks, errs = disksWithAllParts(ctx, xlDisks, partsMetadata, errs, bucket, object)
|
||||
|
||||
if len(filteredDisks) != len(xlDisks) {
|
||||
t.Errorf("Unexpected number of disks: %d", len(filteredDisks))
|
||||
@@ -343,27 +353,4 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test that all disks are returned without any failures with
|
||||
// unmodified meta data
|
||||
partsMetadata, errs = readAllXLMetadata(ctx, xlDisks, bucket, object)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read xl meta data %v", err)
|
||||
}
|
||||
|
||||
filteredDisks, errs = disksWithAllParts(ctx, xlDisks, partsMetadata, errs, bucket, object)
|
||||
|
||||
if len(filteredDisks) != len(xlDisks) {
|
||||
t.Errorf("Unexpected number of disks: %d", len(filteredDisks))
|
||||
}
|
||||
|
||||
for diskIndex, disk := range filteredDisks {
|
||||
if errs[diskIndex] != nil {
|
||||
t.Errorf("Unexpected error %s", errs[diskIndex])
|
||||
}
|
||||
|
||||
if disk == nil {
|
||||
t.Errorf("Disk erroneously filtered, diskIndex: %d", diskIndex)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+101
-87
@@ -19,8 +19,10 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -132,30 +134,19 @@ func healBucket(ctx context.Context, storageDisks []StorageAPI, bucket string, w
|
||||
DiskCount: len(storageDisks),
|
||||
}
|
||||
for i, before := range beforeState {
|
||||
if storageDisks[i] == nil {
|
||||
if storageDisks[i] != nil {
|
||||
drive := storageDisks[i].String()
|
||||
res.Before.Drives = append(res.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: "",
|
||||
Endpoint: drive,
|
||||
State: before,
|
||||
})
|
||||
res.After.Drives = append(res.After.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: "",
|
||||
Endpoint: drive,
|
||||
State: afterState[i],
|
||||
})
|
||||
continue
|
||||
}
|
||||
drive := storageDisks[i].String()
|
||||
res.Before.Drives = append(res.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
State: before,
|
||||
})
|
||||
res.After.Drives = append(res.After.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
State: afterState[i],
|
||||
})
|
||||
}
|
||||
|
||||
reducedErr := reduceWriteQuorumErrs(ctx, dErrs, bucketOpIgnoredErrs, writeQuorum)
|
||||
@@ -247,10 +238,33 @@ func listAllBuckets(storageDisks []StorageAPI) (buckets map[string]VolInfo,
|
||||
return buckets, bucketsOcc, nil
|
||||
}
|
||||
|
||||
// Only heal on disks where we are sure that healing is needed. We can expand
|
||||
// this list as and when we figure out more errors can be added to this list safely.
|
||||
func shouldHealObjectOnDisk(xlErr, dataErr error, meta xlMetaV1, quorumModTime time.Time) bool {
|
||||
switch xlErr {
|
||||
case errFileNotFound:
|
||||
return true
|
||||
case errCorruptedFormat:
|
||||
return true
|
||||
}
|
||||
if xlErr == nil {
|
||||
// If xl.json was read fine but there is some problem with the part.N files.
|
||||
if dataErr == errFileNotFound {
|
||||
return true
|
||||
}
|
||||
if _, ok := dataErr.(hashMismatchError); ok {
|
||||
return true
|
||||
}
|
||||
if quorumModTime != meta.Stat.ModTime {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Heals an object by re-writing corrupt/missing erasure blocks.
|
||||
func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, object string,
|
||||
quorum int, dryRun bool) (result madmin.HealResultItem, err error) {
|
||||
|
||||
partsMetadata, errs := readAllXLMetadata(ctx, storageDisks, bucket, object)
|
||||
|
||||
errCount := 0
|
||||
@@ -316,17 +330,13 @@ func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, o
|
||||
driveState = madmin.DriveStateCorrupt
|
||||
}
|
||||
|
||||
// an online disk without valid data/metadata is
|
||||
// outdated and can be healed.
|
||||
if errs[i] != errDiskNotFound && v == nil {
|
||||
var drive string
|
||||
if storageDisks[i] != nil {
|
||||
drive = storageDisks[i].String()
|
||||
}
|
||||
if shouldHealObjectOnDisk(errs[i], dataErrs[i], partsMetadata[i], modTime) {
|
||||
outDatedDisks[i] = storageDisks[i]
|
||||
disksToHealCount++
|
||||
}
|
||||
var drive string
|
||||
if v == nil {
|
||||
if errs[i] != errDiskNotFound {
|
||||
drive = outDatedDisks[i].String()
|
||||
}
|
||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
@@ -339,7 +349,6 @@ func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, o
|
||||
})
|
||||
continue
|
||||
}
|
||||
drive = v.String()
|
||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
@@ -405,15 +414,16 @@ func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, o
|
||||
latestDisks = shuffleDisks(latestDisks, latestMeta.Erasure.Distribution)
|
||||
outDatedDisks = shuffleDisks(outDatedDisks, latestMeta.Erasure.Distribution)
|
||||
partsMetadata = shufflePartsMetadata(partsMetadata, latestMeta.Erasure.Distribution)
|
||||
for i := range outDatedDisks {
|
||||
if outDatedDisks[i] == nil {
|
||||
continue
|
||||
}
|
||||
partsMetadata[i] = newXLMetaFromXLMeta(latestMeta)
|
||||
}
|
||||
|
||||
// We write at temporary location and then rename to final location.
|
||||
tmpID := mustGetUUID()
|
||||
|
||||
// Checksum of the part files. checkSumInfos[index] will
|
||||
// contain checksums of all the part files in the
|
||||
// outDatedDisks[index]
|
||||
checksumInfos := make([][]ChecksumInfo, len(outDatedDisks))
|
||||
|
||||
// Heal each part. erasureHealFile() will write the healed
|
||||
// part to .minio/tmp/uuid/ which needs to be renamed later to
|
||||
// the final location.
|
||||
@@ -423,29 +433,32 @@ func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, o
|
||||
return result, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
erasureInfo := latestMeta.Erasure
|
||||
for partIndex := 0; partIndex < len(latestMeta.Parts); partIndex++ {
|
||||
partName := latestMeta.Parts[partIndex].Name
|
||||
partSize := latestMeta.Parts[partIndex].Size
|
||||
erasureInfo := latestMeta.Erasure
|
||||
var algorithm BitrotAlgorithm
|
||||
bitrotReaders := make([]*bitrotReader, len(latestDisks))
|
||||
partActualSize := latestMeta.Parts[partIndex].ActualSize
|
||||
partNumber := latestMeta.Parts[partIndex].Number
|
||||
tillOffset := erasure.ShardFileTillOffset(0, partSize, partSize)
|
||||
readers := make([]io.ReaderAt, len(latestDisks))
|
||||
checksumAlgo := erasureInfo.GetChecksumInfo(partName).Algorithm
|
||||
for i, disk := range latestDisks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
info := partsMetadata[i].Erasure.GetChecksumInfo(partName)
|
||||
algorithm = info.Algorithm
|
||||
endOffset := getErasureShardFileEndOffset(0, partSize, partSize, erasureInfo.BlockSize, erasure.dataBlocks)
|
||||
bitrotReaders[i] = newBitrotReader(disk, bucket, pathJoin(object, partName), algorithm, endOffset, info.Hash)
|
||||
checksumInfo := partsMetadata[i].Erasure.GetChecksumInfo(partName)
|
||||
readers[i] = newBitrotReader(disk, bucket, pathJoin(object, partName), tillOffset, checksumAlgo, checksumInfo.Hash, erasure.ShardSize())
|
||||
}
|
||||
bitrotWriters := make([]*bitrotWriter, len(outDatedDisks))
|
||||
writers := make([]io.Writer, len(outDatedDisks))
|
||||
for i, disk := range outDatedDisks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
bitrotWriters[i] = newBitrotWriter(disk, minioMetaTmpBucket, pathJoin(tmpID, partName), algorithm)
|
||||
writers[i] = newBitrotWriter(disk, minioMetaTmpBucket, pathJoin(tmpID, partName), tillOffset, checksumAlgo, erasure.ShardSize())
|
||||
}
|
||||
hErr := erasure.Heal(ctx, bitrotReaders, bitrotWriters, partSize)
|
||||
hErr := erasure.Heal(ctx, readers, writers, partSize)
|
||||
closeBitrotReaders(readers)
|
||||
closeBitrotWriters(writers)
|
||||
if hErr != nil {
|
||||
return result, toObjectErr(hErr, bucket, object)
|
||||
}
|
||||
@@ -457,14 +470,13 @@ func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, o
|
||||
}
|
||||
// A non-nil stale disk which did not receive
|
||||
// a healed part checksum had a write error.
|
||||
if bitrotWriters[i] == nil {
|
||||
if writers[i] == nil {
|
||||
outDatedDisks[i] = nil
|
||||
disksToHealCount--
|
||||
continue
|
||||
}
|
||||
// append part checksums
|
||||
checksumInfos[i] = append(checksumInfos[i],
|
||||
ChecksumInfo{partName, algorithm, bitrotWriters[i].Sum()})
|
||||
partsMetadata[i].AddObjectPart(partNumber, partName, "", partSize, partActualSize)
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{partName, checksumAlgo, bitrotWriterSum(writers[i])})
|
||||
}
|
||||
|
||||
// If all disks are having errors, we give up.
|
||||
@@ -473,15 +485,6 @@ func healObject(ctx context.Context, storageDisks []StorageAPI, bucket string, o
|
||||
}
|
||||
}
|
||||
|
||||
// xl.json should be written to all the healed disks.
|
||||
for index, disk := range outDatedDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
partsMetadata[index] = latestMeta
|
||||
partsMetadata[index].Erasure.Checksums = checksumInfos[index]
|
||||
}
|
||||
|
||||
// Generate and write `xl.json` generated from other disks.
|
||||
outDatedDisks, aErr := writeUniqueXLMetadata(ctx, outDatedDisks, minioMetaTmpBucket, tmpID,
|
||||
partsMetadata, diskCount(outDatedDisks))
|
||||
@@ -532,44 +535,55 @@ func (xl xlObjects) healObjectDir(ctx context.Context, bucket, object string, dr
|
||||
ObjectSize: 0,
|
||||
}
|
||||
|
||||
hr.Before.Drives = make([]madmin.HealDriveInfo, len(storageDisks))
|
||||
hr.After.Drives = make([]madmin.HealDriveInfo, len(storageDisks))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Prepare object creation in all disks
|
||||
for _, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
hr.Before.Drives = append(hr.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
State: madmin.DriveStateOffline,
|
||||
})
|
||||
hr.After.Drives = append(hr.After.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
State: madmin.DriveStateMissing,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
drive := disk.String()
|
||||
hr.Before.Drives = append(hr.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
State: madmin.DriveStateMissing,
|
||||
})
|
||||
hr.After.Drives = append(hr.After.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
State: madmin.DriveStateMissing,
|
||||
})
|
||||
|
||||
if !dryRun {
|
||||
if err := disk.MakeVol(pathJoin(bucket, object)); err != nil && err != errVolumeExists {
|
||||
return hr, toObjectErr(err, bucket, object)
|
||||
for i, d := range storageDisks {
|
||||
wg.Add(1)
|
||||
go func(idx int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
if disk == nil {
|
||||
hr.Before.Drives[idx] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
|
||||
hr.After.Drives[idx] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range hr.Before.Drives {
|
||||
if v.Endpoint == drive {
|
||||
hr.After.Drives[i].State = madmin.DriveStateOk
|
||||
}
|
||||
drive := disk.String()
|
||||
hr.Before.Drives[idx] = madmin.HealDriveInfo{UUID: "", Endpoint: drive, State: madmin.DriveStateOffline}
|
||||
hr.After.Drives[idx] = madmin.HealDriveInfo{UUID: "", Endpoint: drive, State: madmin.DriveStateOffline}
|
||||
|
||||
_, statErr := disk.StatVol(pathJoin(bucket, object))
|
||||
switch statErr {
|
||||
case nil:
|
||||
hr.Before.Drives[idx].State = madmin.DriveStateOk
|
||||
hr.After.Drives[idx].State = madmin.DriveStateOk
|
||||
// Object is fine in this disk, nothing to be done anymore, exiting
|
||||
return
|
||||
case errVolumeNotFound:
|
||||
hr.Before.Drives[idx].State = madmin.DriveStateMissing
|
||||
hr.After.Drives[idx].State = madmin.DriveStateMissing
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return
|
||||
}
|
||||
|
||||
if err := disk.MakeVol(pathJoin(bucket, object)); err == nil || err == errVolumeExists {
|
||||
hr.After.Drives[idx].State = madmin.DriveStateOk
|
||||
} else {
|
||||
logger.LogIf(ctx, err)
|
||||
hr.After.Drives[idx].State = madmin.DriveStateOffline
|
||||
}
|
||||
}(i, d)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return hr, nil
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -58,7 +58,7 @@ type ChecksumInfo struct {
|
||||
type checksumInfoJSON struct {
|
||||
Name string `json:"name"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
Hash string `json:"hash"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the ChecksumInfo struct
|
||||
@@ -186,6 +186,15 @@ func newXLMetaV1(object string, dataBlocks, parityBlocks int) (xlMeta xlMetaV1)
|
||||
return xlMeta
|
||||
}
|
||||
|
||||
// Return a new xlMetaV1 initialized using the given xlMetaV1. Used in healing to make sure that we do not copy
|
||||
// over any part's checksum info which will differ for different disks.
|
||||
func newXLMetaFromXLMeta(meta xlMetaV1) xlMetaV1 {
|
||||
xlMeta := meta
|
||||
xlMeta.Erasure.Checksums = nil
|
||||
xlMeta.Parts = nil
|
||||
return xlMeta
|
||||
}
|
||||
|
||||
// IsValid - tells if the format is sane by validating the version
|
||||
// string, format and erasure info fields.
|
||||
func (m xlMetaV1) IsValid() bool {
|
||||
|
||||
+5
-12
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -351,14 +352,6 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
// Delete the temporary object part. If PutObjectPart succeeds there would be nothing to delete.
|
||||
defer xl.deleteObject(ctx, minioMetaTmpBucket, tmpPart, writeQuorum, false)
|
||||
|
||||
if data.Size() >= 0 {
|
||||
if pErr := xl.prepareFile(ctx, minioMetaTmpBucket, tmpPartPath, data.Size(),
|
||||
onlineDisks, xlMeta.Erasure.BlockSize, xlMeta.Erasure.DataBlocks, writeQuorum); pErr != nil {
|
||||
return pi, toObjectErr(pErr, bucket, object)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
erasure, err := NewErasure(ctx, xlMeta.Erasure.DataBlocks, xlMeta.Erasure.ParityBlocks, xlMeta.Erasure.BlockSize)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, bucket, object)
|
||||
@@ -380,16 +373,16 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
if len(buffer) > int(xlMeta.Erasure.BlockSize) {
|
||||
buffer = buffer[:xlMeta.Erasure.BlockSize]
|
||||
}
|
||||
|
||||
writers := make([]*bitrotWriter, len(onlineDisks))
|
||||
writers := make([]io.Writer, len(onlineDisks))
|
||||
for i, disk := range onlineDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, minioMetaTmpBucket, tmpPartPath, DefaultBitrotAlgorithm)
|
||||
writers[i] = newBitrotWriter(disk, minioMetaTmpBucket, tmpPartPath, erasure.ShardFileSize(data.Size()), DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
|
||||
n, err := erasure.Encode(ctx, data, writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
@@ -455,7 +448,7 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
}
|
||||
partsMetadata[i].Stat = xlMeta.Stat
|
||||
partsMetadata[i].Parts = xlMeta.Parts
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{partSuffix, DefaultBitrotAlgorithm, writers[i].Sum()})
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{partSuffix, DefaultBitrotAlgorithm, bitrotWriterSum(writers[i])})
|
||||
}
|
||||
|
||||
// Write all the checksum metadata.
|
||||
|
||||
+16
-36
@@ -57,25 +57,6 @@ func (xl xlObjects) putObjectDir(ctx context.Context, bucket, object string, wri
|
||||
return reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
}
|
||||
|
||||
// prepareFile hints the bottom layer to optimize the creation of a new object
|
||||
func (xl xlObjects) prepareFile(ctx context.Context, bucket, object string, size int64, onlineDisks []StorageAPI, blockSize int64, dataBlocks, writeQuorum int) error {
|
||||
pErrs := make([]error, len(onlineDisks))
|
||||
// Calculate the real size of the part in one disk.
|
||||
actualSize := getErasureShardFileSize(blockSize, size, dataBlocks)
|
||||
// Prepare object creation in a all disks
|
||||
for index, disk := range onlineDisks {
|
||||
if disk != nil {
|
||||
if err := disk.PrepareFile(bucket, object, actualSize); err != nil {
|
||||
// Save error to reduce it later
|
||||
pErrs[index] = err
|
||||
// Ignore later access to disk which generated the error
|
||||
onlineDisks[index] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return reduceWriteQuorumErrs(ctx, pErrs, objectOpIgnoredErrs, writeQuorum)
|
||||
}
|
||||
|
||||
/// Object Operations
|
||||
|
||||
// CopyObject - copy object source object to destination object.
|
||||
@@ -348,22 +329,24 @@ func (xl xlObjects) getObject(ctx context.Context, bucket, object string, startO
|
||||
partLength = length - totalBytesRead
|
||||
}
|
||||
|
||||
tillOffset := erasure.ShardFileTillOffset(partOffset, partLength, partSize)
|
||||
// Get the checksums of the current part.
|
||||
bitrotReaders := make([]*bitrotReader, len(onlineDisks))
|
||||
readers := make([]io.ReaderAt, len(onlineDisks))
|
||||
for index, disk := range onlineDisks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
checksumInfo := metaArr[index].Erasure.GetChecksumInfo(partName)
|
||||
endOffset := getErasureShardFileEndOffset(partOffset, partLength, partSize, xlMeta.Erasure.BlockSize, xlMeta.Erasure.DataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, bucket, pathJoin(object, partName), checksumInfo.Algorithm, endOffset, checksumInfo.Hash)
|
||||
readers[index] = newBitrotReader(disk, bucket, pathJoin(object, partName), tillOffset, checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
|
||||
}
|
||||
|
||||
err := erasure.Decode(ctx, writer, bitrotReaders, partOffset, partLength, partSize)
|
||||
err := erasure.Decode(ctx, writer, readers, partOffset, partLength, partSize)
|
||||
// Note: we should not be defer'ing the following closeBitrotReaders() call as we are inside a for loop i.e if we use defer, we would accumulate a lot of open files by the time
|
||||
// we return from this function.
|
||||
closeBitrotReaders(readers)
|
||||
if err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
for i, r := range bitrotReaders {
|
||||
for i, r := range readers {
|
||||
if r == nil {
|
||||
onlineDisks[i] = OfflineDisk
|
||||
}
|
||||
@@ -681,7 +664,6 @@ func (xl xlObjects) putObject(ctx context.Context, bucket string, object string,
|
||||
|
||||
uniqueID := mustGetUUID()
|
||||
tempObj := uniqueID
|
||||
|
||||
// No metadata is set, allocate a new one.
|
||||
if metadata == nil {
|
||||
metadata = make(map[string]string)
|
||||
@@ -793,6 +775,7 @@ func (xl xlObjects) putObject(ctx context.Context, bucket string, object string,
|
||||
var curPartSize int64
|
||||
curPartSize, err = calculatePartSizeFromIdx(ctx, data.Size(), globalPutPartSize, partIdx)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -800,27 +783,24 @@ func (xl xlObjects) putObject(ctx context.Context, bucket string, object string,
|
||||
// This is only an optimization.
|
||||
var curPartReader io.Reader
|
||||
|
||||
if curPartSize >= 0 {
|
||||
pErr := xl.prepareFile(ctx, minioMetaTmpBucket, tempErasureObj, curPartSize, onlineDisks, xlMeta.Erasure.BlockSize, xlMeta.Erasure.DataBlocks, writeQuorum)
|
||||
if pErr != nil {
|
||||
return ObjectInfo{}, toObjectErr(pErr, bucket, object)
|
||||
}
|
||||
}
|
||||
|
||||
if curPartSize < data.Size() {
|
||||
curPartReader = io.LimitReader(reader, curPartSize)
|
||||
} else {
|
||||
curPartReader = reader
|
||||
}
|
||||
|
||||
writers := make([]*bitrotWriter, len(onlineDisks))
|
||||
writers := make([]io.Writer, len(onlineDisks))
|
||||
for i, disk := range onlineDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, minioMetaTmpBucket, tempErasureObj, DefaultBitrotAlgorithm)
|
||||
writers[i] = newBitrotWriter(disk, minioMetaTmpBucket, tempErasureObj, erasure.ShardFileSize(curPartSize), DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
|
||||
n, erasureErr := erasure.Encode(ctx, curPartReader, writers, buffer, erasure.dataBlocks+1)
|
||||
// Note: we should not be defer'ing the following closeBitrotWriters() call as we are inside a for loop i.e if we use defer, we would accumulate a lot of open files by the time
|
||||
// we return from this function.
|
||||
closeBitrotWriters(writers)
|
||||
if erasureErr != nil {
|
||||
return ObjectInfo{}, toObjectErr(erasureErr, minioMetaTmpBucket, tempErasureObj)
|
||||
}
|
||||
@@ -853,7 +833,7 @@ func (xl xlObjects) putObject(ctx context.Context, bucket string, object string,
|
||||
continue
|
||||
}
|
||||
partsMetadata[i].AddObjectPart(partIdx, partName, "", n, data.ActualSize())
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{partName, DefaultBitrotAlgorithm, w.Sum()})
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{partName, DefaultBitrotAlgorithm, bitrotWriterSum(w)})
|
||||
}
|
||||
|
||||
// We wrote everything, break out.
|
||||
|
||||
@@ -160,9 +160,9 @@ The Minio server configuration file is stored on the backend in json format. The
|
||||
| `broker` | _string_ | (Required) MQTT server endpoint, e.g. `tcp://localhost:1883` |
|
||||
| `topic` | _string_ | (Required) Name of the MQTT topic to publish on, e.g. `minio` |
|
||||
| `qos` | _int_ | Set the Quality of Service Level |
|
||||
| `clientId` | _string_ | Unique ID for the MQTT broker to identify Minio |
|
||||
| `username` | _string_ | Username to connect to the MQTT server (if required) |
|
||||
| `password` | _string_ | Password to connect to the MQTT server (if required) |
|
||||
| `queueDir` | _string_ | Persistent store for events when MQTT broker is offline |
|
||||
|
||||
An example configuration for MQTT is shown below:
|
||||
|
||||
@@ -173,12 +173,15 @@ An example configuration for MQTT is shown below:
|
||||
"broker": "tcp://localhost:1883",
|
||||
"topic": "minio",
|
||||
"qos": 1,
|
||||
"clientId": "minio",
|
||||
"username": "",
|
||||
"password": ""
|
||||
"password": "",
|
||||
"queueDir": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
MQTT supports persistent event store in the client-side. The persistent store will backup events when the MQTT broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field in the mqtt config. For eg, the `queueDir` can be `/home/events`.
|
||||
|
||||
|
||||
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
|
||||
```sh
|
||||
$ mc admin config get myminio/ > /tmp/myconfig
|
||||
@@ -217,12 +220,14 @@ import paho.mqtt.client as mqtt
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
print("Connected with result code "+str(rc))
|
||||
client.subscribe("minio")
|
||||
# qos level is set to 1
|
||||
client.subscribe("minio", 1)
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
print(msg.payload)
|
||||
|
||||
client = mqtt.Client()
|
||||
# client_id is a randomly generated unique ID for the mqtt broker to identify the connection.
|
||||
client = mqtt.Client(client_id="myclientid",clean_session=False)
|
||||
|
||||
client.on_connect = on_connect
|
||||
client.on_message = on_message
|
||||
@@ -1031,7 +1036,7 @@ An example configuration for NSQ is shown below:
|
||||
"nsqdAddress": "127.0.0.1:4150",
|
||||
"topic": "minio",
|
||||
"tls": {
|
||||
"enable": false,
|
||||
"enable": false,
|
||||
"skipVerify": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +82,11 @@
|
||||
"broker": "",
|
||||
"topic": "",
|
||||
"qos": 0,
|
||||
"clientId": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"reconnectInterval": 0,
|
||||
"keepAliveInterval": 0
|
||||
"keepAliveInterval": 0,
|
||||
"queueDir": ""
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
Minio Gateway adds Amazon S3 compatibility to third party cloud storage providers.
|
||||
- [NAS](https://github.com/minio/minio/blob/master/docs/gateway/nas.md)
|
||||
- [Microsoft Azure Blob Storage](https://github.com/minio/minio/blob/master/docs/gateway/azure.md)
|
||||
- [Google Cloud Storage](https://github.com/minio/minio/blob/master/docs/gateway/gcs.md)
|
||||
- [Backblaze B2](https://github.com/minio/minio/blob/master/docs/gateway/b2.md)
|
||||
- [S3](https://github.com/minio/minio/blob/master/docs/gateway/s3.md)
|
||||
- [Sia Decentralized Cloud Storage](https://github.com/minio/minio/blob/master/docs/gateway/sia.md) _Alpha release_
|
||||
- [Manta Object Storage](https://github.com/minio/minio/blob/master/docs/gateway/manta.md) _Alpha release_
|
||||
- [Google Cloud Storage](https://github.com/minio/minio/blob/master/docs/gateway/gcs.md)
|
||||
- [Alibaba Cloud Storage](https://github.com/minio/minio/blob/master/docs/gateway/oss.md)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Minio B2 Gateway [](https://slack.minio.io)
|
||||
Minio Gateway adds Amazon S3 compatibility to Backblaze B2 Cloud Storage.
|
||||
|
||||
## Run Minio Gateway for Backblaze B2 Cloud Storage
|
||||
Please follow this [guide](https://www.backblaze.com/b2/docs/quick_account.html) to create an account on backblaze.com to obtain your access credentials for B2 Cloud storage.
|
||||
|
||||
### Using Docker
|
||||
```
|
||||
docker run -p 9000:9000 --name b2-s3 \
|
||||
-e "MINIO_ACCESS_KEY=b2_account_id" \
|
||||
-e "MINIO_SECRET_KEY=b2_application_key" \
|
||||
minio/minio gateway b2
|
||||
```
|
||||
|
||||
### Using Binary
|
||||
```
|
||||
export MINIO_ACCESS_KEY=b2_account_id
|
||||
export MINIO_SECRET_KEY=b2_application_key
|
||||
minio gateway b2
|
||||
```
|
||||
|
||||
## Test using Minio Browser
|
||||
Minio Gateway comes with an embedded web based object browser. Point your web browser to http://127.0.0.1:9000 to ensure that your server has started successfully.
|
||||
|
||||

|
||||
|
||||
## Test using Minio Client `mc`
|
||||
`mc` provides a modern alternative to UNIX commands such as ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services.
|
||||
|
||||
### Configure `mc`
|
||||
```
|
||||
mc config host add myb2 http://gateway-ip:9000 b2_account_id b2_application_key
|
||||
```
|
||||
|
||||
### List buckets on Backblaze B2
|
||||
```
|
||||
mc ls myb2
|
||||
[2017-02-22 01:50:43 PST] 0B ferenginar/
|
||||
[2017-02-26 21:43:51 PST] 0B my-bucket/
|
||||
[2017-02-26 22:10:11 PST] 0B test-bucket1/
|
||||
```
|
||||
|
||||
### Known limitations
|
||||
Gateway inherits the following B2 limitations:
|
||||
|
||||
- No support for CopyObject S3 API (There are no equivalent APIs available on Backblaze B2).
|
||||
- No support for CopyObjectPart S3 API (There are no equivalent APIs available on Backblaze B2).
|
||||
- Only read-only bucket policy supported at bucket level, all other variations will return API Notimplemented error.
|
||||
- DeleteObject() might not delete the object right away on Backblaze B2, so you might see the object immediately after a Delete request.
|
||||
|
||||
Other limitations:
|
||||
|
||||
- Bucket notification APIs are not supported.
|
||||
|
||||
## Explore Further
|
||||
- [`mc` command-line interface](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
- [`aws` command-line interface](https://docs.minio.io/docs/aws-cli-with-minio)
|
||||
- [`minio-go` Go SDK](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
||||
@@ -1,78 +0,0 @@
|
||||
# Minio Manta Gateway [](https://slack.minio.io)
|
||||
Minio Gateway adds Amazon S3 compatibility to Manta Object Storage.
|
||||
|
||||
## Run Minio Gateway for Manta Object Storage
|
||||
### Using Docker
|
||||
```
|
||||
docker run -p 9000:9000 --name manta-s3 \
|
||||
-e "MINIO_ACCESS_KEY=joyentaccountname" \
|
||||
-e "MINIO_SECRET_KEY=joyentkeyid" \
|
||||
-e "MANTA_KEY_MATERIAL=~/.ssh/id_rsa" \
|
||||
-e "MANTA_SUBUSER=devuser"
|
||||
minio/minio gateway manta
|
||||
```
|
||||
|
||||
### Using Binary
|
||||
```
|
||||
export MINIO_ACCESS_KEY=joyentaccountname
|
||||
export MINIO_SECRET_KEY=joyentkeyid
|
||||
export MANTA_KEY_MATERIAL=~/.ssh/id_rsa
|
||||
export MANTA_SUBUSER=devuser
|
||||
minio gateway manta
|
||||
```
|
||||
|
||||
## Run Minio Gateway for Manta Object Storage Custom Endpoints
|
||||
### Using Docker
|
||||
```
|
||||
docker run -p 9000:9000 --name manta-s3 \
|
||||
-e "MINIO_ACCESS_KEY=joyentaccountname" \
|
||||
-e "MINIO_SECRET_KEY=joyentkeyid" \
|
||||
-e "MANTA_KEY_MATERIAL=~/.ssh/id_rsa" \
|
||||
-e "MANTA_SUBUSER=devuser"
|
||||
minio/minio gateway manta https://manta_service_endpoint:port
|
||||
```
|
||||
|
||||
### Using Binary
|
||||
```
|
||||
export MINIO_ACCESS_KEY=joyentaccountname
|
||||
export MINIO_SECRET_KEY=joyentkeyid
|
||||
export MANTA_KEY_MATERIAL=~/.ssh/id_rsa
|
||||
export MANTA_SUBUSER=devuser
|
||||
minio gateway manta https://manta_service_endpoint:port
|
||||
```
|
||||
|
||||
## Test using Minio Browser
|
||||
Minio Gateway comes with an embedded web based object browser. Point your web browser to http://127.0.0.1:9000 to ensure that your server has started successfully.
|
||||
|
||||

|
||||
## Test using Minio Client `mc`
|
||||
`mc` provides a modern alternative to UNIX commands such as ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services.
|
||||
|
||||
### Configure `mc`
|
||||
```
|
||||
export MINIO_KEY_MATERIAL=~/.ssh/id_rsa
|
||||
mc config host add mymanta http://gateway-ip:9000 joyentaccountname joyentkeyid
|
||||
```
|
||||
|
||||
### List containers on Manta Object Storage
|
||||
```
|
||||
mc ls mymanta
|
||||
[2017-02-22 01:50:43 PST] 0B ferenginar/
|
||||
[2017-02-26 21:43:51 PST] 0B my-container/
|
||||
[2017-02-26 22:10:11 PST] 0B test-container1/
|
||||
```
|
||||
|
||||
### Known limitations
|
||||
Gateway inherits the following Manta limitations:
|
||||
|
||||
- No support for MultiPartUpload.
|
||||
- No support for bucket policies.
|
||||
|
||||
Other limitations:
|
||||
|
||||
- Bucket notification APIs are not supported.
|
||||
|
||||
## Explore Further
|
||||
- [`mc` command-line interface](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
- [`aws` command-line interface](https://docs.minio.io/docs/aws-cli-with-minio)
|
||||
- [`minio-go` Go SDK](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
||||
+2
-2
@@ -28,11 +28,11 @@ export MINIO_SECRET_KEY=custom_secret_key
|
||||
minio gateway s3
|
||||
```
|
||||
|
||||
Minio gateway will automatically look for list of credential styles in following order.
|
||||
Minio gateway will automatically look for list of credential styles in following order, if your backend URL is AWS S3.
|
||||
|
||||
- AWS env vars (i.e. AWS_ACCESS_KEY_ID)
|
||||
- IAM profile based credentials. (performs an HTTP call to a pre-defined endpoint, only valid inside configured ec2 instances)
|
||||
- AWS creds file (i.e. AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials)
|
||||
- IAM profile based credentials. (performs an HTTP call to a pre-defined endpoint, only valid inside configured ec2 instances)
|
||||
|
||||
## Run Minio Gateway for AWS S3 compatible services
|
||||
As a prerequisite to run Minio S3 gateway on an AWS S3 compatible service, you need valid access key, secret key and service endpoint.
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# Minio Sia Gateway [](https://slack.minio.io)
|
||||
Minio Sia Gateway adds Amazon S3 compatibility to Sia Decentralized Cloud Storage.
|
||||
|
||||
## What is Sia?
|
||||
Sia is a blockchain-based decentralized storage service with built-in privacy and redundancy that costs up to 10x LESS than Amazon S3 and most other cloud providers! See [sia.tech](https://sia.tech) to learn how awesome Sia truly is.
|
||||
|
||||
## Getting Started with Sia
|
||||
|
||||
### Install Sia Daemon
|
||||
To use Sia for backend storage, Minio will need access to a running Sia daemon that is:
|
||||
1. fully synchronized with the Sia network,
|
||||
2. has sufficient rental contract allowances, and
|
||||
3. has an unlocked wallet.
|
||||
|
||||
To download and install Sia for your platform, visit [sia.tech](http://sia.tech).
|
||||
|
||||
To purchase inexpensive rental contracts with Sia, you have to possess some Siacoin in your wallet. To obtain Siacoin, you will need to purchase some on an exchange such as Bittrex using bitcoin. To obtain bitcoin, you'll need to use a service such as Coinbase to buy bitcoin using a bank account or credit card. If you need help, there are many friendly people active on [Sia's Slack](http://slackin.sia.tech).
|
||||
|
||||
### Configuration
|
||||
Once you have the Sia Daemon running and synchronized, with rental allowances created, you just need to configure the Minio server to use Sia. Configuration is accomplished using environment variables, and is only necessary if the default values need to be changed. On a linux machine using bash shell, you can easily set environment variables by adding export statements to the "~/.bash_profile" file. For example:
|
||||
```
|
||||
export MY_ENV_VAR=VALUE
|
||||
```
|
||||
Just remember to reload the profile by executing: "source ~/.bash_profile" on the command prompt.
|
||||
|
||||
#### Supported Environment Variables
|
||||
Environment Variable | Description | Default Value
|
||||
--- | --- | ---
|
||||
`MINIO_ACCESS_KEY` | The access key required to access Minio. | (empty)
|
||||
`MINIO_SECRET_KEY` | The secret key required to access Minio. | (empty)
|
||||
`SIA_TEMP_DIR` | The local directory to use for temporary storage. | .sia_temp
|
||||
`SIA_API_PASSWORD` | The API password required to access the Sia daemon, if needed. | (empty)
|
||||
|
||||
### Running Minio with Sia Gateway
|
||||
```
|
||||
export MINIO_ACCESS_KEY=minioaccesskey
|
||||
export MINIO_SECRET_KEY=miniosecretkey
|
||||
minio gateway sia [SIA_DAEMON_ADDRESS]
|
||||
```
|
||||
The [SIA_DAEMON_ADDRESS] is optional, and it defaults to "127.0.0.1:9980".
|
||||
Access information should then be presented on-screen. To connect to the server and upload files using your web browser, open a web browser and point it to the address displayed for "Browser Access." Then log in using the "AccessKey" and "SecretKey" that are also displayed on-screen. You should then be able to create buckets (folders) and upload files.
|
||||
|
||||

|
||||
|
||||
### Known limitations
|
||||
|
||||
Gateway inherits the following Sia limitations:
|
||||
|
||||
- Multipart uploads are not currently supported.
|
||||
- Bucket policies are not currently supported.
|
||||
|
||||
Other limitations:
|
||||
|
||||
- Bucket notification APIs are not supported.
|
||||
|
||||
## Explore Further
|
||||
- [`mc` command-line interface](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
- [`aws` command-line interface](https://docs.minio.io/docs/aws-cli-with-minio)
|
||||
- [`minio-go` Go SDK](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ vault auth enable approle # enable approle style auth
|
||||
vault secrets enable transit # enable transit secrets engine
|
||||
vault write -f transit/keys/my-minio-key #define a encryption key-ring for the transit path
|
||||
vault policy write minio-policy ./vaultpolicy.hcl #define a policy for AppRole to access transit path
|
||||
vault write auth/approle/role/my-role token_num_uses=0 secret_id_num_uses=0 period=60s # period indicates it is renewable if token is renewed before the period is over
|
||||
vault write auth/approle/role/my-role token_num_uses=0 secret_id_num_uses=0 period=5m # period indicates it is renewable if token is renewed before the period is over
|
||||
# define an AppRole
|
||||
vault write auth/approle/role/my-role policies=minio-policy # apply policy to role
|
||||
vault read auth/approle/role/my-role/role-id # get Approle ID
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user