Implement SetConfig admin API handler. (#3792)

master
Krishnan Parthasarathi 8 years ago committed by Harshavardhana
parent dce0345f8f
commit c9619673fb
  1. 124
      cmd/admin-handlers.go
  2. 253
      cmd/admin-handlers_test.go
  3. 2
      cmd/admin-router.go
  4. 135
      cmd/admin-rpc-client.go
  5. 72
      cmd/admin-rpc-server.go
  6. 61
      cmd/admin-rpc-server_test.go
  7. 6
      cmd/api-errors.go
  8. 2
      cmd/generic-handlers.go
  9. 35
      pkg/madmin/API.md
  10. 78
      pkg/madmin/config-commands.go
  11. 156
      pkg/madmin/examples/set-config.go

@ -17,8 +17,10 @@
package cmd package cmd
import ( import (
"bytes"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
@ -27,7 +29,8 @@ import (
) )
const ( const (
minioAdminOpHeader = "X-Minio-Operation" minioAdminOpHeader = "X-Minio-Operation"
minioConfigTmpFormat = "config-%s.json"
) )
// Type-safe query params. // Type-safe query params.
@ -694,10 +697,127 @@ func (adminAPI adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http
// returns local config.json. // returns local config.json.
configBytes, err := getPeerConfig(globalAdminPeers) configBytes, err := getPeerConfig(globalAdminPeers)
if err != nil { if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
errorIf(err, "Failed to get config from peers") errorIf(err, "Failed to get config from peers")
writeErrorResponse(w, toAdminAPIErrCode(err), r.URL)
return return
} }
writeSuccessResponseJSON(w, configBytes) writeSuccessResponseJSON(w, configBytes)
} }
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
// specific error.
func toAdminAPIErrCode(err error) APIErrorCode {
switch err {
case errXLWriteQuorum:
return ErrAdminConfigNoQuorum
}
return toAPIErrorCode(err)
}
// SetConfigResult - represents detailed results of a set-config
// operation.
type nodeSummary struct {
Name string `json:"name"`
Err string `json:"err"`
}
type setConfigResult struct {
NodeResults []nodeSummary `json:"nodeResults"`
Status bool `json:"status"`
}
// writeSetConfigResponse - writes setConfigResult value as json depending on the status.
func writeSetConfigResponse(w http.ResponseWriter, peers adminPeers, errs []error, status bool, reqURL *url.URL) {
var nodeResults []nodeSummary
// Build nodeResults based on error values received during
// set-config operation.
for i := range errs {
nodeResults = append(nodeResults, nodeSummary{
Name: peers[i].addr,
Err: fmt.Sprintf("%v", errs[i]),
})
}
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 {
writeErrorResponse(w, toAPIErrorCode(jsonErr), reqURL)
return
}
writeSuccessResponseJSON(w, resultBuf.Bytes())
return
}
// SetConfigHandler - PUT /?config
// - x-minio-operation = set
func (adminAPI adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Read configuration bytes from request body.
configBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
errorIf(err, "Failed to read config from request body.")
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// 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.
rErr := reduceWriteQuorumErrs(errs, nil, len(globalAdminPeers)/2+1)
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.
configLock := globalNSMutex.NewNSLock(minioReservedBucket, globalMinioConfigFile)
configLock.Lock()
defer configLock.Unlock()
// Rename the temporary config file to config.json
errs = commitConfigPeers(globalAdminPeers, tmpFileName)
rErr = reduceWriteQuorumErrs(errs, nil, len(globalAdminPeers)/2+1)
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)
}

@ -20,6 +20,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -30,6 +31,102 @@ import (
router "github.com/gorilla/mux" router "github.com/gorilla/mux"
) )
var configJSON = []byte(`{
"version": "13",
"credential": {
"accessKey": "minio",
"secretKey": "minio123"
},
"region": "us-west-1",
"logger": {
"console": {
"enable": true,
"level": "fatal"
},
"file": {
"enable": false,
"fileName": "",
"level": ""
}
},
"notify": {
"amqp": {
"1": {
"enable": false,
"url": "",
"exchange": "",
"routingKey": "",
"exchangeType": "",
"mandatory": false,
"immediate": false,
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
}
},
"nats": {
"1": {
"enable": false,
"address": "",
"subject": "",
"username": "",
"password": "",
"token": "",
"secure": false,
"pingInterval": 0,
"streaming": {
"enable": false,
"clusterID": "",
"clientID": "",
"async": false,
"maxPubAcksInflight": 0
}
}
},
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
},
"postgresql": {
"1": {
"enable": false,
"connectionString": "",
"table": "",
"host": "",
"port": "",
"user": "",
"password": "",
"database": ""
}
},
"kafka": {
"1": {
"enable": false,
"brokers": null,
"topic": ""
}
},
"webhook": {
"1": {
"enable": false,
"endpoint": ""
}
}
}
}`)
// adminXLTestBed - encapsulates subsystems that need to be setup for // adminXLTestBed - encapsulates subsystems that need to be setup for
// admin-handler unit tests. // admin-handler unit tests.
type adminXLTestBed struct { type adminXLTestBed struct {
@ -1033,3 +1130,159 @@ func TestGetConfigHandler(t *testing.T) {
} }
} }
// TestSetConfigHandler - test for SetConfigHandler.
func TestSetConfigHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
eps, err := parseStorageEndpoints([]string{"http://127.0.0.1"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
globalMinioAddr = eps[0].Host
initGlobalAdminPeers(eps)
// SetConfigHandler restarts minio setup - need to start a
// signal receiver to receive on globalServiceSignalCh.
go testServiceSignalReceiver(restartCmd, t)
// Prepare query params for set-config mgmt REST API.
queryVal := url.Values{}
queryVal.Set("config", "")
req, err := newTestRequest("PUT", "/?"+queryVal.Encode(), int64(len(configJSON)), bytes.NewReader(configJSON))
if err != nil {
t.Fatalf("Failed to construct get-config object request - %v", err)
}
// Set x-minio-operation header to set.
req.Header.Set(minioAdminOpHeader, "set")
// Sign the request using signature v4.
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Failed to sign heal object request - %v", err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected to succeed but failed with %d", rec.Code)
}
result := setConfigResult{}
err = json.NewDecoder(rec.Body).Decode(&result)
if err != nil {
t.Fatalf("Failed to decode set config result json %v", err)
}
if result.Status != true {
t.Error("Expected set-config to succeed, but failed")
}
}
// TestToAdminAPIErr - test for toAdminAPIErr helper function.
func TestToAdminAPIErr(t *testing.T) {
testCases := []struct {
err error
expectedAPIErr APIErrorCode
}{
// 1. Server not in quorum.
{
err: errXLWriteQuorum,
expectedAPIErr: ErrAdminConfigNoQuorum,
},
// 2. No error.
{
err: nil,
expectedAPIErr: ErrNone,
},
// 3. Non-admin API specific error.
{
err: errDiskNotFound,
expectedAPIErr: toAPIErrorCode(errDiskNotFound),
},
}
for i, test := range testCases {
actualErr := toAdminAPIErrCode(test.err)
if actualErr != test.expectedAPIErr {
t.Errorf("Test %d: Expected %v but received %v",
i+1, test.expectedAPIErr, actualErr)
}
}
}
func TestWriteSetConfigResponse(t *testing.T) {
testCases := []struct {
status bool
errs []error
}{
// 1. all nodes returned success.
{
status: true,
errs: []error{nil, nil, nil, nil},
},
// 2. some nodes returned errors.
{
status: false,
errs: []error{errDiskNotFound, nil, errDiskAccessDenied, errFaultyDisk},
},
}
testPeers := []adminPeer{
adminPeer{
addr: "localhost:9001",
},
adminPeer{
addr: "localhost:9002",
},
adminPeer{
addr: "localhost:9003",
},
adminPeer{
addr: "localhost:9004",
},
}
testURL, err := url.Parse("dummy.com")
if err != nil {
t.Fatalf("Failed to parse a place-holder url")
}
var actualResult setConfigResult
for i, test := range testCases {
rec := httptest.NewRecorder()
writeSetConfigResponse(rec, testPeers, test.errs, test.status, testURL)
resp := rec.Result()
jsonBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Test %d: Failed to read response %v", i+1, err)
}
err = json.Unmarshal(jsonBytes, &actualResult)
if err != nil {
t.Fatalf("Test %d: Failed to unmarshal json %v", i+1, err)
}
if actualResult.Status != test.status {
t.Errorf("Test %d: Expected status %v but received %v", i+1, test.status, actualResult.Status)
}
for p, res := range actualResult.NodeResults {
if res.Name != testPeers[p].addr {
t.Errorf("Test %d: Expected node name %s but received %s", i+1, testPeers[p].addr, res.Name)
}
expectedErrStr := fmt.Sprintf("%v", test.errs[p])
if res.Err != expectedErrStr {
t.Errorf("Test %d: Expected error %s but received %s", i+1, expectedErrStr, res.Err)
}
}
}
}

@ -67,4 +67,6 @@ func registerAdminRouter(mux *router.Router) {
// Get config // Get config
adminRouter.Methods("GET").Queries("config", "").Headers(minioAdminOpHeader, "get").HandlerFunc(adminAPI.GetConfigHandler) adminRouter.Methods("GET").Queries("config", "").Headers(minioAdminOpHeader, "get").HandlerFunc(adminAPI.GetConfigHandler)
// Set Config
adminRouter.Methods("PUT").Queries("config", "").Headers(minioAdminOpHeader, "set").HandlerFunc(adminAPI.SetConfigHandler)
} }

@ -20,13 +20,26 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"net/url" "net/url"
"os"
"path" "path"
"path/filepath"
"reflect" "reflect"
"sort" "sort"
"sync" "sync"
"time" "time"
) )
const (
// Admin service names
serviceRestartRPC = "Admin.Restart"
listLocksRPC = "Admin.ListLocks"
reInitDisksRPC = "Admin.ReInitDisks"
uptimeRPC = "Admin.Uptime"
getConfigRPC = "Admin.GetConfig"
writeTmpConfigRPC = "Admin.WriteTmpConfig"
commitConfigRPC = "Admin.CommitConfig"
)
// localAdminClient - represents admin operation to be executed locally. // localAdminClient - represents admin operation to be executed locally.
type localAdminClient struct { type localAdminClient struct {
} }
@ -45,6 +58,8 @@ type adminCmdRunner interface {
ReInitDisks() error ReInitDisks() error
Uptime() (time.Duration, error) Uptime() (time.Duration, error)
GetConfig() ([]byte, error) GetConfig() ([]byte, error)
WriteTmpConfig(tmpFileName string, configBytes []byte) error
CommitConfig(tmpFileName string) error
} }
// Restart - Sends a message over channel to the go-routine // Restart - Sends a message over channel to the go-routine
@ -63,7 +78,7 @@ func (lc localAdminClient) ListLocks(bucket, prefix string, duration time.Durati
func (rc remoteAdminClient) Restart() error { func (rc remoteAdminClient) Restart() error {
args := AuthRPCArgs{} args := AuthRPCArgs{}
reply := AuthRPCReply{} reply := AuthRPCReply{}
return rc.Call("Admin.Restart", &args, &reply) return rc.Call(serviceRestartRPC, &args, &reply)
} }
// ListLocks - Sends list locks command to remote server via RPC. // ListLocks - Sends list locks command to remote server via RPC.
@ -74,7 +89,7 @@ func (rc remoteAdminClient) ListLocks(bucket, prefix string, duration time.Durat
duration: duration, duration: duration,
} }
var reply ListLocksReply var reply ListLocksReply
if err := rc.Call("Admin.ListLocks", &listArgs, &reply); err != nil { if err := rc.Call(listLocksRPC, &listArgs, &reply); err != nil {
return nil, err return nil, err
} }
return reply.volLocks, nil return reply.volLocks, nil
@ -91,7 +106,7 @@ func (lc localAdminClient) ReInitDisks() error {
func (rc remoteAdminClient) ReInitDisks() error { func (rc remoteAdminClient) ReInitDisks() error {
args := AuthRPCArgs{} args := AuthRPCArgs{}
reply := AuthRPCReply{} reply := AuthRPCReply{}
return rc.Call("Admin.ReInitDisks", &args, &reply) return rc.Call(reInitDisksRPC, &args, &reply)
} }
// Uptime - Returns the uptime of this server. Timestamp is taken // Uptime - Returns the uptime of this server. Timestamp is taken
@ -108,7 +123,7 @@ func (lc localAdminClient) Uptime() (time.Duration, error) {
func (rc remoteAdminClient) Uptime() (time.Duration, error) { func (rc remoteAdminClient) Uptime() (time.Duration, error) {
args := AuthRPCArgs{} args := AuthRPCArgs{}
reply := UptimeReply{} reply := UptimeReply{}
err := rc.Call("Admin.Uptime", &args, &reply) err := rc.Call(uptimeRPC, &args, &reply)
if err != nil { if err != nil {
return time.Duration(0), err return time.Duration(0), err
} }
@ -129,12 +144,70 @@ func (lc localAdminClient) GetConfig() ([]byte, error) {
func (rc remoteAdminClient) GetConfig() ([]byte, error) { func (rc remoteAdminClient) GetConfig() ([]byte, error) {
args := AuthRPCArgs{} args := AuthRPCArgs{}
reply := ConfigReply{} reply := ConfigReply{}
if err := rc.Call("Admin.GetConfig", &args, &reply); err != nil { if err := rc.Call(getConfigRPC, &args, &reply); err != nil {
return nil, err return nil, err
} }
return reply.Config, nil return reply.Config, nil
} }
// WriteTmpConfig - writes config file content to a temporary file on
// the local server.
func (lc localAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
return writeTmpConfigCommon(tmpFileName, configBytes)
}
// WriteTmpConfig - writes config file content to a temporary file on
// a remote node.
func (rc remoteAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
wArgs := WriteConfigArgs{
TmpFileName: tmpFileName,
Buf: configBytes,
}
err := rc.Call(writeTmpConfigRPC, &wArgs, &WriteConfigReply{})
if err != nil {
errorIf(err, "Failed to write temporary config file.")
return err
}
return nil
}
// CommitConfig - Move the new config in tmpFileName onto config.json
// on a local node.
func (lc localAdminClient) CommitConfig(tmpFileName string) error {
configDir, err := getConfigPath()
if err != nil {
errorIf(err, "Failed to get config directory path.")
return err
}
configFilePath := filepath.Join(configDir, globalMinioConfigFile)
err = os.Rename(filepath.Join(configDir, tmpFileName), configFilePath)
if err != nil {
errorIf(err, "Failed to rename to config.json")
return err
}
return nil
}
// CommitConfig - Move the new config in tmpFileName onto config.json
// on a remote node.
func (rc remoteAdminClient) CommitConfig(tmpFileName string) error {
cArgs := CommitConfigArgs{
FileName: tmpFileName,
}
cReply := CommitConfigReply{}
err := rc.Call(commitConfigRPC, &cArgs, &cReply)
if err != nil {
errorIf(err, "Failed to rename config file.")
return err
}
return nil
}
// adminPeer - represents an entity that implements Restart methods. // adminPeer - represents an entity that implements Restart methods.
type adminPeer struct { type adminPeer struct {
addr string addr string
@ -489,3 +562,55 @@ func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (server
return configJSON, nil return configJSON, nil
} }
// Write config contents into a temporary file on all nodes.
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
return []error{err}
}
errs := make([]error, len(peers))
// Write config into temporary file on all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
}(i, peer)
}
wg.Wait()
// Return bytes written and errors (if any) during writing
// temporary config file.
return errs
}
// Move config contents from the given temporary file onto config.json
// on all nodes.
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
}
errs := make([]error, len(peers))
// Rename temporary config file into configDir/config.json on
// all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
}(i, peer)
}
wg.Wait()
// Return errors (if any) received during rename.
return errs
}

@ -19,7 +19,10 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil"
"net/rpc" "net/rpc"
"os"
"path/filepath"
"time" "time"
router "github.com/gorilla/mux" router "github.com/gorilla/mux"
@ -158,6 +161,75 @@ func (s *adminCmd) GetConfig(args *AuthRPCArgs, reply *ConfigReply) error {
return nil return nil
} }
// WriteConfigArgs - wraps the bytes to be written and temporary file name.
type WriteConfigArgs struct {
AuthRPCArgs
TmpFileName string
Buf []byte
}
// WriteConfigReply - wraps the result of a writing config into a temporary file.
// the remote node.
type WriteConfigReply struct {
AuthRPCReply
}
func writeTmpConfigCommon(tmpFileName string, configBytes []byte) error {
configDir, err := getConfigPath()
if err != nil {
errorIf(err, "Failed to get config path")
return err
}
err = ioutil.WriteFile(filepath.Join(configDir, tmpFileName), configBytes, 0666)
if err != nil {
errorIf(err, "Failed to write to temporary config file.")
return err
}
return err
}
// WriteTmpConfig - writes the supplied config contents onto the
// supplied temporary file.
func (s *adminCmd) WriteTmpConfig(wArgs *WriteConfigArgs, wReply *WriteConfigReply) error {
if err := wArgs.IsAuthenticated(); err != nil {
return err
}
return writeTmpConfigCommon(wArgs.TmpFileName, wArgs.Buf)
}
// CommitConfigArgs - wraps the config file name that needs to be
// committed into config.json on this node.
type CommitConfigArgs struct {
AuthRPCArgs
FileName string
}
// CommitConfigReply - represents response to commit of config file on
// this node.
type CommitConfigReply struct {
AuthRPCReply
}
// CommitConfig - Renames the temporary file into config.json on this node.
func (s *adminCmd) CommitConfig(cArgs *CommitConfigArgs, cReply *CommitConfigReply) error {
configDir, err := getConfigPath()
if err != nil {
errorIf(err, "Failed to get config path.")
return err
}
configFilePath := filepath.Join(configDir, globalMinioConfigFile)
err = os.Rename(filepath.Join(configDir, cArgs.FileName), configFilePath)
if err != nil {
errorIf(err, "Failed to rename config file.")
return err
}
return nil
}
// registerAdminRPCRouter - registers RPC methods for service status, // registerAdminRPCRouter - registers RPC methods for service status,
// stop and restart commands. // stop and restart commands.
func registerAdminRPCRouter(mux *router.Router) error { func registerAdminRPCRouter(mux *router.Router) error {

@ -144,6 +144,7 @@ func TestReInitDisks(t *testing.T) {
} }
} }
// TestGetConfig - Test for GetConfig admin RPC.
func TestGetConfig(t *testing.T) { func TestGetConfig(t *testing.T) {
// Reset global variables to start afresh. // Reset global variables to start afresh.
resetTestGlobals() resetTestGlobals()
@ -185,3 +186,63 @@ func TestGetConfig(t *testing.T) {
t.Errorf("Expected json unmarshal to pass but failed with %v", err) t.Errorf("Expected json unmarshal to pass but failed with %v", err)
} }
} }
// TestWriteAndCommitConfig - test for WriteTmpConfig and CommitConfig
// RPC handler.
func TestWriteAndCommitConfig(t *testing.T) {
// Reset global variables to start afresh.
resetTestGlobals()
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("Unable to initialize server config. %s", err)
}
defer removeAll(rootPath)
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: time.Now().UTC(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
// Write temporary config.
buf := []byte("hello")
tmpFileName := mustGetUUID()
wArgs := WriteConfigArgs{
AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken,
},
TmpFileName: tmpFileName,
Buf: buf,
}
err = adminServer.WriteTmpConfig(&wArgs, &WriteConfigReply{})
if err != nil {
t.Fatalf("Failed to write temporary config %v", err)
}
if err != nil {
t.Errorf("Expected to succeed but failed %v", err)
}
cArgs := CommitConfigArgs{
AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken,
},
FileName: tmpFileName,
}
cReply := CommitConfigReply{}
err = adminServer.CommitConfig(&cArgs, &cReply)
if err != nil {
t.Fatalf("Failed to commit config file %v", err)
}
}

@ -144,6 +144,7 @@ const (
ErrAdminInvalidAccessKey ErrAdminInvalidAccessKey
ErrAdminInvalidSecretKey ErrAdminInvalidSecretKey
ErrAdminConfigNoQuorum
) )
// error code to APIError structure, these fields carry respective // error code to APIError structure, these fields carry respective
@ -593,6 +594,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "The secret key is invalid.", Description: "The secret key is invalid.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrAdminConfigNoQuorum: {
Code: "XMinioAdminConfigNoQuorum",
Description: "Configuration update failed because server quorum was not met",
HTTPStatusCode: http.StatusServiceUnavailable,
},
// Add your error structure here. // Add your error structure here.
} }

@ -351,7 +351,7 @@ func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
// A put method on path "/" doesn't make sense, ignore it. // A put method on path "/" doesn't make sense, ignore it.
if r.Method == httpPUT && r.URL.Path == "/" { if r.Method == httpPUT && r.URL.Path == "/" && r.Header.Get(minioAdminOpHeader) == "" {
writeErrorResponse(w, ErrNotImplemented, r.URL) writeErrorResponse(w, ErrNotImplemented, r.URL)
return return
} }

@ -39,7 +39,7 @@ func main() {
| Service operations|LockInfo operations|Healing operations|Config operations| Misc | | Service operations|LockInfo operations|Healing operations|Config operations| Misc |
|:---|:---|:---|:---|:---| |:---|:---|:---|:---|:---|
|[`ServiceStatus`](#ServiceStatus)| [`ListLocks`](#ListLocks)| [`ListObjectsHeal`](#ListObjectsHeal)|[`GetConfig`](#GetConfig)| [`SetCredentials`](#SetCredentials)| |[`ServiceStatus`](#ServiceStatus)| [`ListLocks`](#ListLocks)| [`ListObjectsHeal`](#ListObjectsHeal)|[`GetConfig`](#GetConfig)| [`SetCredentials`](#SetCredentials)|
|[`ServiceRestart`](#ServiceRestart)| [`ClearLocks`](#ClearLocks)| [`ListBucketsHeal`](#ListBucketsHeal)||| |[`ServiceRestart`](#ServiceRestart)| [`ClearLocks`](#ClearLocks)| [`ListBucketsHeal`](#ListBucketsHeal)|[`SetConfig`](#SetConfig)||
| | |[`HealBucket`](#HealBucket) ||| | | |[`HealBucket`](#HealBucket) |||
| | |[`HealObject`](#HealObject)||| | | |[`HealObject`](#HealObject)|||
| | |[`HealFormat`](#HealFormat)||| | | |[`HealFormat`](#HealFormat)|||
@ -306,7 +306,7 @@ __Example__
<a name="SetCredentials"></a> <a name="SetCredentials"></a>
### SetCredentials() error ### SetCredentials() error
Set new credentials of a Minio setup. Set new credentials of a Minio setup.
__Example__ __Example__
@ -320,4 +320,35 @@ __Example__
``` ```
<a name="SetConfig"></a>
### SetConfig(config io.Reader) (SetConfigResult, error)
Set config.json of a minio setup and restart setup for configuration
change to take effect.
| Param | Type | Description |
|---|---|---|
|`st.Status` | _bool_ | true if set-config succeeded, false otherwise. |
|`st.NodeSummary.Name` | _string_ | Network address of the node. |
|`st.NodeSummary.Err` | _string_ | String representation of the error (if any) on the node.|
__Example__
``` go
config := bytes.NewReader([]byte(`config.json contents go here`))
result, err := madmClnt.SetConfig(config)
if err != nil {
log.Fatalf("failed due to: %v", err)
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", "\t")
err = enc.Encode(result)
if err != nil {
log.Fatalln(err)
}
log.Println("SetConfig: ", string(buf.Bytes()))
```

@ -18,15 +18,36 @@
package madmin package madmin
import ( import (
"bytes"
"encoding/json"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
) )
const (
configQueryParam = "config"
)
// NodeSummary - represents the result of an operation part of
// set-config on a node.
type NodeSummary struct {
Name string `json:"name"`
Err string `json:"err"`
}
// SetConfigResult - represents detailed results of a set-config
// operation.
type SetConfigResult struct {
NodeResults []NodeSummary `json:"nodeResults"`
Status bool `json:"status"`
}
// GetConfig - returns the config.json of a minio setup. // GetConfig - returns the config.json of a minio setup.
func (adm *AdminClient) GetConfig() ([]byte, error) { func (adm *AdminClient) GetConfig() ([]byte, error) {
queryVal := make(url.Values) queryVal := make(url.Values)
queryVal.Set("config", "") queryVal.Set(configQueryParam, "")
hdrs := make(http.Header) hdrs := make(http.Header)
hdrs.Set(minioAdminOpHeader, "get") hdrs.Set(minioAdminOpHeader, "get")
@ -36,7 +57,7 @@ func (adm *AdminClient) GetConfig() ([]byte, error) {
customHeaders: hdrs, customHeaders: hdrs,
} }
// Execute GET on /?lock to list locks. // Execute GET on /?config to get config of a setup.
resp, err := adm.executeMethod("GET", reqData) resp, err := adm.executeMethod("GET", reqData)
defer closeResponse(resp) defer closeResponse(resp)
@ -44,6 +65,59 @@ func (adm *AdminClient) GetConfig() ([]byte, error) {
return nil, err return nil, err
} }
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
// Return the JSON marshalled bytes to user. // Return the JSON marshalled bytes to user.
return ioutil.ReadAll(resp.Body) return ioutil.ReadAll(resp.Body)
} }
// SetConfig - set config supplied as config.json for the setup.
func (adm *AdminClient) SetConfig(config io.Reader) (SetConfigResult, error) {
queryVal := url.Values{}
queryVal.Set(configQueryParam, "")
// Set x-minio-operation to set.
hdrs := make(http.Header)
hdrs.Set(minioAdminOpHeader, "set")
// Read config bytes to calculate MD5, SHA256 and content length.
configBytes, err := ioutil.ReadAll(config)
if err != nil {
return SetConfigResult{}, err
}
reqData := requestData{
queryValues: queryVal,
customHeaders: hdrs,
contentBody: bytes.NewReader(configBytes),
contentMD5Bytes: sumMD5(configBytes),
contentSHA256Bytes: sum256(configBytes),
}
// Execute PUT on /?config to set config.
resp, err := adm.executeMethod("PUT", reqData)
defer closeResponse(resp)
if err != nil {
return SetConfigResult{}, err
}
if resp.StatusCode != http.StatusOK {
return SetConfigResult{}, httpRespToErrorResponse(resp)
}
var result SetConfigResult
jsonBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return SetConfigResult{}, err
}
err = json.Unmarshal(jsonBytes, &result)
if err != nil {
return SetConfigResult{}, err
}
return result, nil
}

@ -0,0 +1,156 @@
// +build ignore
/*
* 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 main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"github.com/minio/minio/pkg/madmin"
)
var configJSON = []byte(`{
"version": "13",
"credential": {
"accessKey": "minio",
"secretKey": "minio123"
},
"region": "us-west-1",
"logger": {
"console": {
"enable": true,
"level": "fatal"
},
"file": {
"enable": false,
"fileName": "",
"level": ""
}
},
"notify": {
"amqp": {
"1": {
"enable": false,
"url": "",
"exchange": "",
"routingKey": "",
"exchangeType": "",
"mandatory": false,
"immediate": false,
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
}
},
"nats": {
"1": {
"enable": false,
"address": "",
"subject": "",
"username": "",
"password": "",
"token": "",
"secure": false,
"pingInterval": 0,
"streaming": {
"enable": false,
"clusterID": "",
"clientID": "",
"async": false,
"maxPubAcksInflight": 0
}
}
},
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
},
"postgresql": {
"1": {
"enable": false,
"connectionString": "",
"table": "",
"host": "",
"port": "",
"user": "",
"password": "",
"database": ""
}
},
"kafka": {
"1": {
"enable": false,
"brokers": null,
"topic": ""
}
},
"webhook": {
"1": {
"enable": false,
"endpoint": ""
}
}
}
}`)
func main() {
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
// dummy values, please replace them with original values.
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
// dummy values, please replace them with original values.
// API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise.
// New returns an Minio Admin client object.
madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true)
if err != nil {
log.Fatalln(err)
}
result, err := madmClnt.SetConfig(bytes.NewReader(configJSON))
if err != nil {
log.Fatalln(err)
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", "\t")
err = enc.Encode(result)
if err != nil {
log.Fatalln(err)
}
fmt.Println("SetConfig: ", string(buf.Bytes()))
}
Loading…
Cancel
Save