Add bootstrap REST handler for verifying server config (#8550)
parent
890b493a2e
commit
c3771df641
@ -0,0 +1,257 @@ |
|||||||
|
/* |
||||||
|
* 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" |
||||||
|
"crypto/tls" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"runtime" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gorilla/mux" |
||||||
|
"github.com/minio/minio-go/pkg/set" |
||||||
|
xhttp "github.com/minio/minio/cmd/http" |
||||||
|
"github.com/minio/minio/cmd/logger" |
||||||
|
"github.com/minio/minio/cmd/rest" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
bootstrapRESTVersion = "v1" |
||||||
|
bootstrapRESTVersionPrefix = SlashSeparator + bootstrapRESTVersion |
||||||
|
bootstrapRESTPrefix = minioReservedBucketPath + "/bootstrap" |
||||||
|
bootstrapRESTPath = bootstrapRESTPrefix + bootstrapRESTVersionPrefix |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
bootstrapRESTMethodVerify = "/verify" |
||||||
|
) |
||||||
|
|
||||||
|
// To abstract a node over network.
|
||||||
|
type bootstrapRESTServer struct{} |
||||||
|
|
||||||
|
// ServerSystemConfig - captures information about server configuration.
|
||||||
|
type ServerSystemConfig struct { |
||||||
|
MinioPlatform string |
||||||
|
MinioRuntime string |
||||||
|
MinioEndpoints EndpointZones |
||||||
|
} |
||||||
|
|
||||||
|
// Diff - returns error on first difference found in two configs.
|
||||||
|
func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error { |
||||||
|
if s1.MinioPlatform != s2.MinioPlatform { |
||||||
|
return fmt.Errorf("Expected platform '%s', found to be running '%s'", |
||||||
|
s1.MinioPlatform, s2.MinioPlatform) |
||||||
|
} |
||||||
|
if s1.MinioEndpoints.Nodes() != s2.MinioEndpoints.Nodes() { |
||||||
|
return fmt.Errorf("Expected number of endpoints %d, seen %d", s1.MinioEndpoints.Nodes(), |
||||||
|
s2.MinioEndpoints.Nodes()) |
||||||
|
} |
||||||
|
|
||||||
|
for i, ep := range s1.MinioEndpoints { |
||||||
|
if ep.SetCount != s2.MinioEndpoints[i].SetCount { |
||||||
|
return fmt.Errorf("Expected set count %d, seen %d", ep.SetCount, |
||||||
|
s2.MinioEndpoints[i].SetCount) |
||||||
|
} |
||||||
|
if ep.DrivesPerSet != s2.MinioEndpoints[i].DrivesPerSet { |
||||||
|
return fmt.Errorf("Expected drives pet set %d, seen %d", ep.DrivesPerSet, |
||||||
|
s2.MinioEndpoints[i].DrivesPerSet) |
||||||
|
} |
||||||
|
for j, endpoint := range ep.Endpoints { |
||||||
|
if endpoint.String() != s2.MinioEndpoints[i].Endpoints[j].String() { |
||||||
|
return fmt.Errorf("Expected endpoint %s, seen %s", endpoint, |
||||||
|
s2.MinioEndpoints[i].Endpoints[j]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func getServerSystemCfg() ServerSystemConfig { |
||||||
|
return ServerSystemConfig{ |
||||||
|
MinioPlatform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH), |
||||||
|
MinioEndpoints: globalEndpoints, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (b *bootstrapRESTServer) VerifyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
ctx := newContext(r, w, "VerifyHandler") |
||||||
|
cfg := getServerSystemCfg() |
||||||
|
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg)) |
||||||
|
w.(http.Flusher).Flush() |
||||||
|
} |
||||||
|
|
||||||
|
// registerBootstrapRESTHandlers - register bootstrap rest router.
|
||||||
|
func registerBootstrapRESTHandlers(router *mux.Router) { |
||||||
|
server := &bootstrapRESTServer{} |
||||||
|
subrouter := router.PathPrefix(bootstrapRESTPrefix).Subrouter() |
||||||
|
|
||||||
|
subrouter.Methods(http.MethodPost).Path(bootstrapRESTVersionPrefix + bootstrapRESTMethodVerify).HandlerFunc( |
||||||
|
httpTraceHdrs(server.VerifyHandler)) |
||||||
|
} |
||||||
|
|
||||||
|
// client to talk to bootstrap Nodes.
|
||||||
|
type bootstrapRESTClient struct { |
||||||
|
endpoint Endpoint |
||||||
|
restClient *rest.Client |
||||||
|
connected int32 |
||||||
|
} |
||||||
|
|
||||||
|
// Reconnect to a bootstrap rest server.k
|
||||||
|
func (client *bootstrapRESTClient) reConnect() { |
||||||
|
atomic.StoreInt32(&client.connected, 1) |
||||||
|
} |
||||||
|
|
||||||
|
// Wrapper to restClient.Call to handle network errors, in case of network error the connection is marked disconnected
|
||||||
|
// permanently. The only way to restore the connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
||||||
|
// after verifying format.json
|
||||||
|
func (client *bootstrapRESTClient) call(method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) { |
||||||
|
return client.callWithContext(context.Background(), method, values, body, length) |
||||||
|
} |
||||||
|
|
||||||
|
// Wrapper to restClient.Call to handle network errors, in case of network error the connection is marked disconnected
|
||||||
|
// permanently. The only way to restore the connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
||||||
|
// after verifying format.json
|
||||||
|
func (client *bootstrapRESTClient) callWithContext(ctx context.Context, method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) { |
||||||
|
if !client.IsOnline() { |
||||||
|
client.reConnect() |
||||||
|
} |
||||||
|
|
||||||
|
if values == nil { |
||||||
|
values = make(url.Values) |
||||||
|
} |
||||||
|
|
||||||
|
respBody, err = client.restClient.CallWithContext(ctx, method, values, body, length) |
||||||
|
if err == nil { |
||||||
|
return respBody, nil |
||||||
|
} |
||||||
|
|
||||||
|
if isNetworkError(err) { |
||||||
|
atomic.StoreInt32(&client.connected, 0) |
||||||
|
} |
||||||
|
|
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Stringer provides a canonicalized representation of node.
|
||||||
|
func (client *bootstrapRESTClient) String() string { |
||||||
|
return client.endpoint.String() |
||||||
|
} |
||||||
|
|
||||||
|
// IsOnline - returns whether RPC client failed to connect or not.
|
||||||
|
func (client *bootstrapRESTClient) IsOnline() bool { |
||||||
|
return atomic.LoadInt32(&client.connected) == 1 |
||||||
|
} |
||||||
|
|
||||||
|
// Close - marks the client as closed.
|
||||||
|
func (client *bootstrapRESTClient) Close() error { |
||||||
|
atomic.StoreInt32(&client.connected, 0) |
||||||
|
client.restClient.Close() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Verify - fetches system server config.
|
||||||
|
func (client *bootstrapRESTClient) Verify(srcCfg ServerSystemConfig) (err error) { |
||||||
|
if newObjectLayerFn() != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
respBody, err := client.call(bootstrapRESTMethodVerify, nil, nil, -1) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
defer xhttp.DrainBody(respBody) |
||||||
|
recvCfg := ServerSystemConfig{} |
||||||
|
if err = json.NewDecoder(respBody).Decode(&recvCfg); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return srcCfg.Diff(recvCfg) |
||||||
|
} |
||||||
|
|
||||||
|
func verifyServerSystemConfig(endpointZones EndpointZones) error { |
||||||
|
srcCfg := getServerSystemCfg() |
||||||
|
clnts := newBootstrapRESTClients(endpointZones) |
||||||
|
var onlineServers int |
||||||
|
for onlineServers < len(clnts)/2 { |
||||||
|
for _, clnt := range clnts { |
||||||
|
if err := clnt.Verify(srcCfg); err != nil { |
||||||
|
if isNetworkError(err) { |
||||||
|
continue |
||||||
|
} |
||||||
|
return fmt.Errorf("%s as has incorrect configuration: %w", clnt.String(), err) |
||||||
|
} |
||||||
|
onlineServers++ |
||||||
|
} |
||||||
|
// Sleep for a while - so that we don't go into
|
||||||
|
// 100% CPU when half the endpoints are offline.
|
||||||
|
time.Sleep(500 * time.Millisecond) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func newBootstrapRESTClients(endpointZones EndpointZones) []*bootstrapRESTClient { |
||||||
|
seenHosts := set.NewStringSet() |
||||||
|
var clnts []*bootstrapRESTClient |
||||||
|
for _, ep := range endpointZones { |
||||||
|
for _, endpoint := range ep.Endpoints { |
||||||
|
if seenHosts.Contains(endpoint.Host) { |
||||||
|
continue |
||||||
|
} |
||||||
|
seenHosts.Add(endpoint.Host) |
||||||
|
|
||||||
|
// Only proceed for remote endpoints.
|
||||||
|
if !endpoint.IsLocal { |
||||||
|
clnt, err := newBootstrapRESTClient(endpoint) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
clnts = append(clnts, clnt) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return clnts |
||||||
|
} |
||||||
|
|
||||||
|
// Returns a new bootstrap client.
|
||||||
|
func newBootstrapRESTClient(endpoint Endpoint) (*bootstrapRESTClient, error) { |
||||||
|
serverURL := &url.URL{ |
||||||
|
Scheme: endpoint.Scheme, |
||||||
|
Host: endpoint.Host, |
||||||
|
Path: bootstrapRESTPath, |
||||||
|
} |
||||||
|
|
||||||
|
var tlsConfig *tls.Config |
||||||
|
if globalIsSSL { |
||||||
|
tlsConfig = &tls.Config{ |
||||||
|
ServerName: endpoint.Hostname(), |
||||||
|
RootCAs: globalRootCAs, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
trFn := newCustomHTTPTransport(tlsConfig, rest.DefaultRESTTimeout, rest.DefaultRESTTimeout) |
||||||
|
restClient, err := rest.NewClient(serverURL, trFn, newAuthToken) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &bootstrapRESTClient{endpoint: endpoint, restClient: restClient, connected: 1}, nil |
||||||
|
} |
Loading…
Reference in new issue