2016-12-16 07:26:15 +01:00
|
|
|
/*
|
2017-01-17 19:02:58 +01:00
|
|
|
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
2016-12-16 07:26:15 +01:00
|
|
|
*
|
|
|
|
* 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 (
|
2017-02-27 20:40:27 +01:00
|
|
|
"bytes"
|
2018-04-06 00:04:40 +02:00
|
|
|
"context"
|
2016-12-16 07:26:15 +01:00
|
|
|
"encoding/json"
|
2018-04-06 00:04:40 +02:00
|
|
|
"errors"
|
2017-02-27 20:40:27 +01:00
|
|
|
"fmt"
|
2018-01-11 08:06:36 +01:00
|
|
|
"io"
|
2016-12-16 07:26:15 +01:00
|
|
|
"net/http"
|
2017-01-04 08:39:22 +01:00
|
|
|
"net/url"
|
2018-08-15 06:41:47 +02:00
|
|
|
"path"
|
2017-04-21 16:15:53 +02:00
|
|
|
"sync"
|
2017-01-04 08:39:22 +01:00
|
|
|
"time"
|
2017-10-31 19:54:32 +01:00
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
"github.com/gorilla/mux"
|
2018-04-06 00:04:40 +02:00
|
|
|
"github.com/minio/minio/cmd/logger"
|
2017-10-31 19:54:32 +01:00
|
|
|
"github.com/minio/minio/pkg/auth"
|
2018-03-03 00:23:04 +01:00
|
|
|
"github.com/minio/minio/pkg/handlers"
|
2018-01-22 23:54:55 +01:00
|
|
|
"github.com/minio/minio/pkg/madmin"
|
2018-04-14 00:14:19 +02:00
|
|
|
"github.com/minio/minio/pkg/quick"
|
2016-12-16 07:26:15 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2017-02-27 20:40:27 +01:00
|
|
|
minioConfigTmpFormat = "config-%s.json"
|
2018-01-11 08:06:36 +01:00
|
|
|
|
|
|
|
maxConfigJSONSize = 256 * 1024 // 256KiB
|
2016-12-16 07:26:15 +01:00
|
|
|
)
|
|
|
|
|
2017-01-17 19:02:58 +01:00
|
|
|
// Type-safe query params.
|
|
|
|
type mgmtQueryKey string
|
|
|
|
|
2017-03-16 08:15:06 +01:00
|
|
|
// Only valid query params for mgmt admin APIs.
|
2017-01-17 19:02:58 +01:00
|
|
|
const (
|
2018-01-22 23:54:55 +01:00
|
|
|
mgmtBucket mgmtQueryKey = "bucket"
|
|
|
|
mgmtPrefix mgmtQueryKey = "prefix"
|
|
|
|
mgmtLockOlderThan mgmtQueryKey = "older-than"
|
|
|
|
mgmtClientToken mgmtQueryKey = "clientToken"
|
|
|
|
mgmtForceStart mgmtQueryKey = "forceStart"
|
2017-01-17 19:02:58 +01:00
|
|
|
)
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
var (
|
|
|
|
// This struct literal represents the Admin API version that
|
|
|
|
// the server uses.
|
2018-02-16 02:45:57 +01:00
|
|
|
adminAPIVersionInfo = madmin.AdminAPIVersionInfo{
|
|
|
|
Version: "1",
|
|
|
|
}
|
2018-01-22 23:54:55 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// VersionHandler - GET /minio/admin/version
|
|
|
|
// -----------
|
|
|
|
// Returns Administration API version
|
|
|
|
func (a adminAPIHandlers) VersionHandler(w http.ResponseWriter, r *http.Request) {
|
2018-06-07 19:37:31 +02:00
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2018-01-22 23:54:55 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-06-07 03:10:51 +02:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2018-01-22 23:54:55 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jsonBytes, err := json.Marshal(adminAPIVersionInfo)
|
|
|
|
if err != nil {
|
|
|
|
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), err)
|
2018-01-22 23:54:55 +01:00
|
|
|
return
|
|
|
|
}
|
2017-01-23 17:56:06 +01:00
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
writeSuccessResponseJSON(w, jsonBytes)
|
2017-01-23 17:56:06 +01:00
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// ServiceStatusHandler - GET /minio/admin/v1/service
|
2016-12-21 03:49:48 +01:00
|
|
|
// ----------
|
2018-01-22 23:54:55 +01:00
|
|
|
// Returns server version and uptime.
|
|
|
|
func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
2018-06-07 19:37:31 +02:00
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2016-12-16 07:26:15 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2016-12-16 07:26:15 +01:00
|
|
|
return
|
|
|
|
}
|
2017-01-23 17:56:06 +01:00
|
|
|
|
|
|
|
// Fetch server version
|
2018-01-22 23:54:55 +01:00
|
|
|
serverVersion := madmin.ServerVersion{
|
|
|
|
Version: Version,
|
|
|
|
CommitID: CommitID,
|
|
|
|
}
|
2017-01-23 17:56:06 +01:00
|
|
|
|
2017-02-08 09:13:02 +01:00
|
|
|
// Fetch uptimes from all peers. This may fail to due to lack
|
|
|
|
// of read-quorum availability.
|
|
|
|
uptime, err := getPeerUptimes(globalAdminPeers)
|
|
|
|
if err != nil {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), err)
|
2017-02-08 09:13:02 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-01-23 17:56:06 +01:00
|
|
|
// Create API response
|
2018-01-22 23:54:55 +01:00
|
|
|
serverStatus := madmin.ServiceStatus{
|
2017-01-23 17:56:06 +01:00
|
|
|
ServerVersion: serverVersion,
|
2017-02-08 09:13:02 +01:00
|
|
|
Uptime: uptime,
|
2017-01-23 17:56:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Marshal API response
|
|
|
|
jsonBytes, err := json.Marshal(serverStatus)
|
2016-12-16 07:26:15 +01:00
|
|
|
if err != nil {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), err)
|
2016-12-21 03:05:25 +01:00
|
|
|
return
|
2016-12-16 07:26:15 +01:00
|
|
|
}
|
2017-01-04 08:39:22 +01:00
|
|
|
// Reply with storage information (across nodes in a
|
|
|
|
// distributed setup) as json.
|
2017-01-06 09:37:00 +01:00
|
|
|
writeSuccessResponseJSON(w, jsonBytes)
|
2016-12-16 07:26:15 +01:00
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// ServiceStopNRestartHandler - POST /minio/admin/v1/service
|
|
|
|
// Body: {"action": <restart-action>}
|
2016-12-21 03:49:48 +01:00
|
|
|
// ----------
|
2018-01-22 23:54:55 +01:00
|
|
|
// Restarts/Stops minio server gracefully. In a distributed setup,
|
|
|
|
// restarts all the servers in the cluster.
|
|
|
|
func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *http.Request) {
|
2018-06-07 19:37:31 +02:00
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2016-12-16 07:26:15 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2017-01-17 23:25:59 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
var sa madmin.ServiceAction
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&sa)
|
2017-01-17 23:25:59 +01:00
|
|
|
if err != nil {
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), err)
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
|
2017-01-17 23:25:59 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
var serviceSig serviceSignal
|
|
|
|
switch sa.Action {
|
|
|
|
case madmin.ServiceActionValueRestart:
|
|
|
|
serviceSig = serviceRestart
|
|
|
|
case madmin.ServiceActionValueStop:
|
|
|
|
serviceSig = serviceStop
|
|
|
|
default:
|
|
|
|
writeErrorResponseJSON(w, ErrMalformedPOSTRequest, r.URL)
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), errors.New("Invalid service action received"))
|
2017-01-17 23:25:59 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// Reply to the client before restarting minio server.
|
|
|
|
writeSuccessResponseHeadersOnly(w)
|
2017-01-17 23:25:59 +01:00
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
sendServiceCmd(globalAdminPeers, serviceSig)
|
2017-01-17 23:25:59 +01:00
|
|
|
}
|
|
|
|
|
2017-02-15 19:45:45 +01:00
|
|
|
// ServerProperties holds some server information such as, version, region
|
|
|
|
// uptime, etc..
|
|
|
|
type ServerProperties struct {
|
|
|
|
Uptime time.Duration `json:"uptime"`
|
|
|
|
Version string `json:"version"`
|
|
|
|
CommitID string `json:"commitID"`
|
|
|
|
Region string `json:"region"`
|
|
|
|
SQSARN []string `json:"sqsARN"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ServerConnStats holds transferred bytes from/to the server
|
|
|
|
type ServerConnStats struct {
|
|
|
|
TotalInputBytes uint64 `json:"transferred"`
|
|
|
|
TotalOutputBytes uint64 `json:"received"`
|
|
|
|
Throughput uint64 `json:"throughput,omitempty"`
|
|
|
|
}
|
|
|
|
|
2017-04-07 08:08:33 +02:00
|
|
|
// ServerHTTPMethodStats holds total number of HTTP operations from/to the server,
|
|
|
|
// including the average duration the call was spent.
|
|
|
|
type ServerHTTPMethodStats struct {
|
|
|
|
Count uint64 `json:"count"`
|
|
|
|
AvgDuration string `json:"avgDuration"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ServerHTTPStats holds all type of http operations performed to/from the server
|
|
|
|
// including their average execution time.
|
|
|
|
type ServerHTTPStats struct {
|
|
|
|
TotalHEADStats ServerHTTPMethodStats `json:"totalHEADs"`
|
|
|
|
SuccessHEADStats ServerHTTPMethodStats `json:"successHEADs"`
|
|
|
|
TotalGETStats ServerHTTPMethodStats `json:"totalGETs"`
|
|
|
|
SuccessGETStats ServerHTTPMethodStats `json:"successGETs"`
|
|
|
|
TotalPUTStats ServerHTTPMethodStats `json:"totalPUTs"`
|
|
|
|
SuccessPUTStats ServerHTTPMethodStats `json:"successPUTs"`
|
|
|
|
TotalPOSTStats ServerHTTPMethodStats `json:"totalPOSTs"`
|
|
|
|
SuccessPOSTStats ServerHTTPMethodStats `json:"successPOSTs"`
|
|
|
|
TotalDELETEStats ServerHTTPMethodStats `json:"totalDELETEs"`
|
|
|
|
SuccessDELETEStats ServerHTTPMethodStats `json:"successDELETEs"`
|
|
|
|
}
|
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
// ServerInfoData holds storage, connections and other
|
|
|
|
// information of a given server.
|
|
|
|
type ServerInfoData struct {
|
2017-02-15 19:45:45 +01:00
|
|
|
StorageInfo StorageInfo `json:"storage"`
|
|
|
|
ConnStats ServerConnStats `json:"network"`
|
2017-04-07 08:08:33 +02:00
|
|
|
HTTPStats ServerHTTPStats `json:"http"`
|
2017-02-15 19:45:45 +01:00
|
|
|
Properties ServerProperties `json:"server"`
|
|
|
|
}
|
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
// ServerInfo holds server information result of one node
|
|
|
|
type ServerInfo struct {
|
2017-05-15 16:28:47 +02:00
|
|
|
Error string `json:"error"`
|
|
|
|
Addr string `json:"addr"`
|
|
|
|
Data *ServerInfoData `json:"data"`
|
2017-04-21 16:15:53 +02:00
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// ServerInfoHandler - GET /minio/admin/v1/info
|
2017-02-15 19:45:45 +01:00
|
|
|
// ----------
|
|
|
|
// Get server information
|
2018-01-22 23:54:55 +01:00
|
|
|
func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
|
2017-02-15 19:45:45 +01:00
|
|
|
// Authenticate request
|
2018-06-07 19:37:31 +02:00
|
|
|
|
|
|
|
// Setting the region as empty so as the mc server info command is irrespective to the region.
|
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2017-02-15 19:45:45 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2017-02-15 19:45:45 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
// Web service response
|
|
|
|
reply := make([]ServerInfo, len(globalAdminPeers))
|
2017-02-15 19:45:45 +01:00
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
var wg sync.WaitGroup
|
2017-02-15 19:45:45 +01:00
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
// Gather server information for all nodes
|
|
|
|
for i, p := range globalAdminPeers {
|
|
|
|
wg.Add(1)
|
2017-02-15 19:45:45 +01:00
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
// Gather information from a peer in a goroutine
|
|
|
|
go func(idx int, peer adminPeer) {
|
|
|
|
defer wg.Done()
|
2017-02-15 19:45:45 +01:00
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
// Initialize server info at index
|
|
|
|
reply[idx] = ServerInfo{Addr: peer.addr}
|
|
|
|
|
2018-06-06 10:51:56 +02:00
|
|
|
serverInfoData, err := peer.cmdRunner.ServerInfo()
|
2017-04-21 16:15:53 +02:00
|
|
|
if err != nil {
|
2018-04-06 00:04:40 +02:00
|
|
|
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peer.addr)
|
|
|
|
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
|
|
logger.LogIf(ctx, err)
|
2017-05-15 16:28:47 +02:00
|
|
|
reply[idx].Error = err.Error()
|
2017-04-21 16:15:53 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
reply[idx].Data = &serverInfoData
|
|
|
|
}(i, p)
|
2017-02-15 19:45:45 +01:00
|
|
|
}
|
|
|
|
|
2017-04-21 16:15:53 +02:00
|
|
|
wg.Wait()
|
|
|
|
|
2017-02-15 19:45:45 +01:00
|
|
|
// Marshal API response
|
2017-04-21 16:15:53 +02:00
|
|
|
jsonBytes, err := json.Marshal(reply)
|
2017-02-15 19:45:45 +01:00
|
|
|
if err != nil {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), err)
|
2017-02-15 19:45:45 +01:00
|
|
|
return
|
|
|
|
}
|
2017-04-07 08:08:33 +02:00
|
|
|
|
2017-02-15 19:45:45 +01:00
|
|
|
// Reply with storage information (across nodes in a
|
|
|
|
// distributed setup) as json.
|
|
|
|
writeSuccessResponseJSON(w, jsonBytes)
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// extractHealInitParams - Validates params for heal init API.
|
|
|
|
func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
|
|
|
hs madmin.HealOpts, clientToken string, forceStart bool,
|
|
|
|
err APIErrorCode) {
|
2017-01-17 19:02:58 +01:00
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
vars := mux.Vars(r)
|
|
|
|
bucket = vars[string(mgmtBucket)]
|
|
|
|
objPrefix = vars[string(mgmtPrefix)]
|
2017-01-17 19:02:58 +01:00
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
if bucket == "" {
|
|
|
|
if objPrefix != "" {
|
|
|
|
// Bucket is required if object-prefix is given
|
|
|
|
err = ErrHealMissingBucket
|
|
|
|
return
|
2017-03-16 08:15:06 +01:00
|
|
|
}
|
2018-01-22 23:54:55 +01:00
|
|
|
} else if !IsValidBucketName(bucket) {
|
|
|
|
err = ErrInvalidBucketName
|
2017-01-17 19:02:58 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// empty prefix is valid.
|
|
|
|
if !IsValidObjectPrefix(objPrefix) {
|
|
|
|
err = ErrInvalidObjectName
|
2018-01-11 19:21:41 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
qParms := r.URL.Query()
|
|
|
|
if len(qParms[string(mgmtClientToken)]) > 0 {
|
|
|
|
clientToken = qParms[string(mgmtClientToken)][0]
|
2017-01-17 19:02:58 +01:00
|
|
|
}
|
2018-01-22 23:54:55 +01:00
|
|
|
if _, ok := qParms[string(mgmtForceStart)]; ok {
|
|
|
|
forceStart = true
|
2017-01-17 19:02:58 +01:00
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// ignore body if clientToken is provided
|
|
|
|
if clientToken == "" {
|
|
|
|
jerr := json.NewDecoder(r.Body).Decode(&hs)
|
|
|
|
if jerr != nil {
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), jerr)
|
2018-01-22 23:54:55 +01:00
|
|
|
err = ErrRequestBodyParse
|
|
|
|
return
|
|
|
|
}
|
2017-04-14 19:28:35 +02:00
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
err = ErrNone
|
|
|
|
return
|
2017-04-01 02:55:15 +02:00
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// HealHandler - POST /minio/admin/v1/heal/
|
|
|
|
// -----------
|
|
|
|
// Start heal processing and return heal status items.
|
|
|
|
//
|
|
|
|
// On a successful heal sequence start, a unique client token is
|
|
|
|
// returned. Subsequent requests to this endpoint providing the client
|
|
|
|
// token will receive heal status records from the running heal
|
|
|
|
// sequence.
|
|
|
|
//
|
|
|
|
// If no client token is provided, and a heal sequence is in progress
|
|
|
|
// an error is returned with information about the running heal
|
|
|
|
// sequence. However, if the force-start flag is provided, the server
|
|
|
|
// aborts the running heal sequence and starts a new one.
|
|
|
|
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
2018-07-21 03:46:32 +02:00
|
|
|
ctx := newContext(r, w, "Heal")
|
2018-03-14 20:01:47 +01:00
|
|
|
|
2017-01-17 19:02:58 +01:00
|
|
|
// Get object layer instance.
|
|
|
|
objLayer := newObjectLayerFn()
|
|
|
|
if objLayer == nil {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
2017-01-17 19:02:58 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate request signature.
|
2018-06-09 02:54:57 +02:00
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2017-01-17 19:02:58 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2017-01-17 19:02:58 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-11 19:21:41 +01:00
|
|
|
// Check if this setup has an erasure coded backend.
|
|
|
|
if !globalIsXL {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrHealNotImplemented, r.URL)
|
2017-01-24 17:11:05 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
bucket, objPrefix, hs, clientToken, forceStart, apiErr := extractHealInitParams(r)
|
|
|
|
if apiErr != ErrNone {
|
|
|
|
writeErrorResponseJSON(w, apiErr, r.URL)
|
2017-01-23 09:32:55 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
type healResp struct {
|
|
|
|
respBytes []byte
|
|
|
|
errCode APIErrorCode
|
|
|
|
errBody string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Define a closure to start sending whitespace to client
|
|
|
|
// after 10s unless a response item comes in
|
|
|
|
keepConnLive := func(w http.ResponseWriter, respCh chan healResp) {
|
|
|
|
ticker := time.NewTicker(time.Second * 10)
|
2018-05-04 19:43:20 +02:00
|
|
|
defer ticker.Stop()
|
2018-01-22 23:54:55 +01:00
|
|
|
started := false
|
|
|
|
forLoop:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
|
|
|
if !started {
|
|
|
|
// Start writing response to client
|
|
|
|
started = true
|
|
|
|
setCommonHeaders(w)
|
|
|
|
w.Header().Set("Content-Type", string(mimeJSON))
|
|
|
|
// Set 200 OK status
|
|
|
|
w.WriteHeader(200)
|
|
|
|
}
|
|
|
|
// Send whitespace and keep connection open
|
|
|
|
w.Write([]byte("\n\r"))
|
|
|
|
w.(http.Flusher).Flush()
|
|
|
|
case hr := <-respCh:
|
|
|
|
switch {
|
|
|
|
case hr.errCode == ErrNone:
|
|
|
|
writeSuccessResponseJSON(w, hr.respBytes)
|
|
|
|
case hr.errBody == "":
|
|
|
|
writeErrorResponseJSON(w, hr.errCode, r.URL)
|
|
|
|
default:
|
|
|
|
writeCustomErrorResponseJSON(w, hr.errCode, hr.errBody, r.URL)
|
|
|
|
}
|
|
|
|
break forLoop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// find number of disks in the setup
|
2018-03-14 20:01:47 +01:00
|
|
|
info := objLayer.StorageInfo(ctx)
|
2018-01-22 23:54:55 +01:00
|
|
|
numDisks := info.Backend.OfflineDisks + info.Backend.OnlineDisks
|
|
|
|
|
|
|
|
if clientToken == "" {
|
|
|
|
// Not a status request
|
2018-03-03 00:23:04 +01:00
|
|
|
nh := newHealSequence(bucket, objPrefix, handlers.GetSourceIP(r),
|
2018-01-22 23:54:55 +01:00
|
|
|
numDisks, hs, forceStart)
|
|
|
|
|
|
|
|
respCh := make(chan healResp)
|
|
|
|
go func() {
|
|
|
|
respBytes, errCode, errMsg := globalAllHealState.LaunchNewHealSequence(nh)
|
|
|
|
hr := healResp{respBytes, errCode, errMsg}
|
|
|
|
respCh <- hr
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Due to the force-starting functionality, the Launch
|
|
|
|
// call above can take a long time - to keep the
|
|
|
|
// connection alive, we start sending whitespace
|
|
|
|
keepConnLive(w, respCh)
|
|
|
|
} else {
|
|
|
|
// Since clientToken is given, fetch heal status from running
|
|
|
|
// heal sequence.
|
|
|
|
path := bucket + "/" + objPrefix
|
|
|
|
respBytes, errCode := globalAllHealState.PopHealStatusJSON(
|
|
|
|
path, clientToken)
|
|
|
|
if errCode != ErrNone {
|
|
|
|
writeErrorResponseJSON(w, errCode, r.URL)
|
|
|
|
} else {
|
|
|
|
writeSuccessResponseJSON(w, respBytes)
|
|
|
|
}
|
2017-01-23 09:32:55 +01:00
|
|
|
}
|
|
|
|
}
|
2017-02-20 21:58:50 +01:00
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// GetConfigHandler - GET /minio/admin/v1/config
|
2017-02-20 21:58:50 +01:00
|
|
|
// Get config.json of this minio setup.
|
2018-01-22 23:54:55 +01:00
|
|
|
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
2017-02-20 21:58:50 +01:00
|
|
|
// Validate request signature.
|
2018-06-07 19:37:31 +02:00
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2017-02-20 21:58:50 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2017-02-20 21:58:50 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-23 03:09:12 +01:00
|
|
|
// Take a read lock on minio/config.json. NB minio is a
|
|
|
|
// reserved bucket name and wouldn't conflict with normal
|
|
|
|
// object operations.
|
|
|
|
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
|
|
|
if configLock.GetRLock(globalObjectTimeout) != nil {
|
|
|
|
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer configLock.RUnlock()
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// Get config.json - in distributed mode, the configuration
|
|
|
|
// occurring on a quorum of the servers is returned.
|
2018-08-15 06:41:47 +02:00
|
|
|
configData, err := getPeerConfig(globalAdminPeers)
|
2017-02-20 21:58:50 +01:00
|
|
|
if err != nil {
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), err)
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
2017-02-20 21:58:50 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-15 06:41:47 +02:00
|
|
|
password := globalServerConfig.GetCredential().SecretKey
|
|
|
|
econfigData, err := madmin.EncryptServerConfigData(password, configData)
|
|
|
|
if err != nil {
|
|
|
|
logger.LogIf(context.Background(), err)
|
|
|
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
writeSuccessResponseJSON(w, econfigData)
|
2017-02-20 21:58:50 +01:00
|
|
|
}
|
2017-02-27 20:40:27 +01:00
|
|
|
|
|
|
|
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
|
|
|
|
// specific error.
|
|
|
|
func toAdminAPIErrCode(err error) APIErrorCode {
|
|
|
|
switch err {
|
|
|
|
case errXLWriteQuorum:
|
|
|
|
return ErrAdminConfigNoQuorum
|
2018-08-06 19:26:40 +02:00
|
|
|
default:
|
|
|
|
return toAPIErrorCode(err)
|
2017-02-27 20:40:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetConfigResult - represents detailed results of a set-config
|
|
|
|
// operation.
|
|
|
|
type nodeSummary struct {
|
2017-03-03 11:53:48 +01:00
|
|
|
Name string `json:"name"`
|
|
|
|
ErrSet bool `json:"errSet"`
|
|
|
|
ErrMsg string `json:"errMsg"`
|
2017-02-27 20:40:27 +01:00
|
|
|
}
|
2017-03-03 11:53:48 +01:00
|
|
|
|
2017-02-27 20:40:27 +01:00
|
|
|
type setConfigResult struct {
|
|
|
|
NodeResults []nodeSummary `json:"nodeResults"`
|
|
|
|
Status bool `json:"status"`
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// writeSetConfigResponse - writes setConfigResult value as json
|
|
|
|
// depending on the status.
|
|
|
|
func writeSetConfigResponse(w http.ResponseWriter, peers adminPeers,
|
|
|
|
errs []error, status bool, reqURL *url.URL) {
|
|
|
|
|
2017-02-27 20:40:27 +01:00
|
|
|
var nodeResults []nodeSummary
|
|
|
|
// Build nodeResults based on error values received during
|
|
|
|
// set-config operation.
|
|
|
|
for i := range errs {
|
|
|
|
nodeResults = append(nodeResults, nodeSummary{
|
2017-03-03 11:53:48 +01:00
|
|
|
Name: peers[i].addr,
|
|
|
|
ErrSet: errs[i] != nil,
|
|
|
|
ErrMsg: fmt.Sprintf("%v", errs[i]),
|
2017-02-27 20:40:27 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
result := setConfigResult{
|
|
|
|
Status: status,
|
|
|
|
NodeResults: nodeResults,
|
|
|
|
}
|
|
|
|
|
|
|
|
// The following elaborate json encoding is to avoid escaping
|
|
|
|
// '<', '>' in <nil>. Note: json.Encoder.Encode() adds a
|
|
|
|
// gratuitous "\n".
|
|
|
|
var resultBuf bytes.Buffer
|
|
|
|
enc := json.NewEncoder(&resultBuf)
|
|
|
|
enc.SetEscapeHTML(false)
|
|
|
|
jsonErr := enc.Encode(result)
|
|
|
|
if jsonErr != nil {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, toAPIErrorCode(jsonErr), reqURL)
|
2017-02-27 20:40:27 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
writeSuccessResponseJSON(w, resultBuf.Bytes())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// SetConfigHandler - PUT /minio/admin/v1/config
|
|
|
|
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
2018-04-06 00:04:40 +02:00
|
|
|
ctx := context.Background()
|
2017-02-27 20:40:27 +01:00
|
|
|
// Get current object layer instance.
|
|
|
|
objectAPI := newObjectLayerFn()
|
|
|
|
if objectAPI == nil {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
2017-02-27 20:40:27 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-06-07 03:10:51 +02:00
|
|
|
// Deny if WORM is enabled
|
|
|
|
if globalWORMEnabled {
|
|
|
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-02-27 20:40:27 +01:00
|
|
|
// Validate request signature.
|
2018-06-07 19:37:31 +02:00
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2017-02-27 20:40:27 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2017-02-27 20:40:27 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read configuration bytes from request body.
|
2018-01-11 08:06:36 +01:00
|
|
|
configBuf := make([]byte, maxConfigJSONSize+1)
|
|
|
|
n, err := io.ReadFull(r.Body, configBuf)
|
|
|
|
if err == nil {
|
|
|
|
// More than maxConfigSize bytes were available
|
2018-06-07 03:10:51 +02:00
|
|
|
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
2018-01-11 08:06:36 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != io.ErrUnexpectedEOF {
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(ctx, err)
|
2018-06-07 03:10:51 +02:00
|
|
|
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
2017-02-27 20:40:27 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-15 06:41:47 +02:00
|
|
|
password := globalServerConfig.GetCredential().SecretKey
|
|
|
|
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
|
|
|
if err != nil {
|
|
|
|
logger.LogIf(ctx, err)
|
|
|
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
|
|
|
return
|
|
|
|
}
|
2018-01-11 08:06:36 +01:00
|
|
|
|
|
|
|
// Validate JSON provided in the request body: check the
|
|
|
|
// client has not sent JSON objects with duplicate keys.
|
2018-04-14 00:14:19 +02:00
|
|
|
if err = quick.CheckDuplicateKeys(string(configBytes)); err != nil {
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(ctx, err)
|
2018-06-07 03:10:51 +02:00
|
|
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
2018-01-11 08:06:36 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-11-29 22:12:47 +01:00
|
|
|
var config serverConfig
|
2017-09-07 20:16:13 +02:00
|
|
|
err = json.Unmarshal(configBytes, &config)
|
|
|
|
if err != nil {
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(ctx, err)
|
2018-07-18 20:22:29 +02:00
|
|
|
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
2017-09-07 20:16:13 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-11 08:06:36 +01:00
|
|
|
// If credentials for the server are provided via environment,
|
|
|
|
// then credentials in the provided configuration must match.
|
2017-09-07 20:16:13 +02:00
|
|
|
if globalIsEnvCreds {
|
2017-11-29 22:12:47 +01:00
|
|
|
creds := globalServerConfig.GetCredential()
|
2017-09-07 20:16:13 +02:00
|
|
|
if config.Credential.AccessKey != creds.AccessKey ||
|
|
|
|
config.Credential.SecretKey != creds.SecretKey {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrAdminCredentialsMismatch, r.URL)
|
2017-09-07 20:16:13 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-18 20:22:29 +02:00
|
|
|
if err := config.Validate(); err != nil {
|
|
|
|
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-02-27 20:40:27 +01:00
|
|
|
// Write config received from request onto a temporary file on
|
|
|
|
// all nodes.
|
|
|
|
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
|
|
|
|
errs := writeTmpConfigPeers(globalAdminPeers, tmpFileName, configBytes)
|
|
|
|
|
|
|
|
// Check if the operation succeeded in quorum or more nodes.
|
2018-04-06 00:04:40 +02:00
|
|
|
rErr := reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
|
2017-02-27 20:40:27 +01:00
|
|
|
if rErr != nil {
|
|
|
|
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Take a lock on minio/config.json. NB minio is a reserved
|
|
|
|
// bucket name and wouldn't conflict with normal object
|
|
|
|
// operations.
|
2017-03-09 04:20:01 +01:00
|
|
|
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
2017-08-31 20:29:22 +02:00
|
|
|
if configLock.GetLock(globalObjectTimeout) != nil {
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
2017-08-31 20:29:22 +02:00
|
|
|
return
|
|
|
|
}
|
2017-02-27 20:40:27 +01:00
|
|
|
defer configLock.Unlock()
|
|
|
|
|
|
|
|
// Rename the temporary config file to config.json
|
|
|
|
errs = commitConfigPeers(globalAdminPeers, tmpFileName)
|
2018-04-06 00:04:40 +02:00
|
|
|
rErr = reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
|
2017-02-27 20:40:27 +01:00
|
|
|
if rErr != nil {
|
|
|
|
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// serverMux (cmd/server-mux.go) implements graceful shutdown,
|
|
|
|
// where all listeners are closed and process restart/shutdown
|
|
|
|
// happens after 5s or completion of all ongoing http
|
|
|
|
// requests, whichever is earlier.
|
|
|
|
writeSetConfigResponse(w, globalAdminPeers, errs, true, r.URL)
|
|
|
|
|
|
|
|
// Restart all node for the modified config to take effect.
|
|
|
|
sendServiceCmd(globalAdminPeers, serviceRestart)
|
|
|
|
}
|
2018-01-22 23:54:55 +01:00
|
|
|
|
|
|
|
// ConfigCredsHandler - POST /minio/admin/v1/config/credential
|
|
|
|
// ----------
|
|
|
|
// Update credentials in a minio server. In a distributed setup,
|
|
|
|
// update all the servers in the cluster.
|
|
|
|
func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|
|
|
r *http.Request) {
|
|
|
|
|
|
|
|
// Authenticate request
|
2018-06-07 19:37:31 +02:00
|
|
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
2018-01-22 23:54:55 +01:00
|
|
|
if adminAPIErr != ErrNone {
|
2018-06-07 03:10:51 +02:00
|
|
|
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
2018-01-22 23:54:55 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Avoid setting new credentials when they are already passed
|
2018-06-07 03:10:51 +02:00
|
|
|
// by the environment. Deny if WORM is enabled.
|
|
|
|
if globalIsEnvCreds || globalWORMEnabled {
|
|
|
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
2018-01-22 23:54:55 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode request body
|
|
|
|
var req madmin.SetCredsReq
|
2018-08-15 06:41:47 +02:00
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
2018-04-06 00:04:40 +02:00
|
|
|
logger.LogIf(context.Background(), err)
|
2018-01-22 23:54:55 +01:00
|
|
|
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
creds, err := auth.CreateCredentials(req.AccessKey, req.SecretKey)
|
|
|
|
if err != nil {
|
2018-06-07 03:10:51 +02:00
|
|
|
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
2018-01-22 23:54:55 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-23 03:09:12 +01:00
|
|
|
// Take a lock on minio/config.json. Prevents concurrent
|
2018-01-31 17:15:54 +01:00
|
|
|
// config file updates.
|
2018-01-23 03:09:12 +01:00
|
|
|
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
|
|
|
if configLock.GetLock(globalObjectTimeout) != nil {
|
|
|
|
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer configLock.Unlock()
|
|
|
|
|
2018-01-31 17:15:54 +01:00
|
|
|
// Acquire lock before updating global configuration.
|
|
|
|
globalServerConfigMu.Lock()
|
|
|
|
defer globalServerConfigMu.Unlock()
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// Update local credentials in memory.
|
|
|
|
globalServerConfig.SetCredential(creds)
|
2018-08-15 06:41:47 +02:00
|
|
|
|
|
|
|
// Construct path to config.json for the given bucket.
|
|
|
|
configFile := path.Join(bucketConfigPrefix, minioConfigFile)
|
|
|
|
transactionConfigFile := configFile + ".transaction"
|
|
|
|
|
|
|
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
|
|
|
// and configFile, take a transaction lock to avoid race.
|
|
|
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
|
|
|
if err = objLock.GetLock(globalOperationTimeout); err != nil {
|
|
|
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = saveServerConfig(newObjectLayerFn(), globalServerConfig); err != nil {
|
|
|
|
objLock.Unlock()
|
|
|
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
2018-01-22 23:54:55 +01:00
|
|
|
return
|
|
|
|
}
|
2018-08-15 06:41:47 +02:00
|
|
|
objLock.Unlock()
|
2018-01-22 23:54:55 +01:00
|
|
|
|
2018-06-06 10:51:56 +02:00
|
|
|
// Notify all other Minio peers to update credentials
|
2018-08-15 06:41:47 +02:00
|
|
|
for host, err := range globalNotificationSys.LoadCredentials() {
|
2018-06-06 10:51:56 +02:00
|
|
|
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", host.String())
|
|
|
|
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
|
|
logger.LogIf(ctx, err)
|
|
|
|
}
|
|
|
|
|
2018-01-22 23:54:55 +01:00
|
|
|
// At this stage, the operation is successful, return 200 OK
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
}
|