diff --git a/commands/cluster_command_launcher.go b/commands/cluster_command_launcher.go index 6912556..69a72c5 100644 --- a/commands/cluster_command_launcher.go +++ b/commands/cluster_command_launcher.go @@ -563,6 +563,8 @@ func constructCmds() []*cobra.Command { makeCmdCreateConnection(), // hidden cmds (for internal testing only) makeCmdPromoteSandbox(), + + makeCmdCheckVClusterServerPid(), } } diff --git a/commands/cmd_base.go b/commands/cmd_base.go index d883de6..6d1b409 100644 --- a/commands/cmd_base.go +++ b/commands/cmd_base.go @@ -168,7 +168,7 @@ func (c *CmdBase) setConfigFlags(cmd *cobra.Command, flags []string) { configFlag, "c", "", - "The path to the config file. If a configuration file is present in the default location (automatically generated by `create_db`),\n"+ + "The path to the config file. If a configuration file is present in the default location (automatically generated by create_db),\n"+ "you do not need to specify this option.\n"+ "Default: /opt/vertica/config/vertica_cluster.yaml") markFlagsFileName(cmd, map[string][]string{configFlag: {"yaml"}}) diff --git a/commands/cmd_check_vcluster_server_pid.go b/commands/cmd_check_vcluster_server_pid.go new file mode 100644 index 0000000..88f934e --- /dev/null +++ b/commands/cmd_check_vcluster_server_pid.go @@ -0,0 +1,86 @@ +/* + (c) Copyright [2023-2024] Open Text. + 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 commands + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/vertica/vcluster/vclusterops" + "github.com/vertica/vcluster/vclusterops/vlog" +) + +/* CmdCheckVClusterServerPid + * + * Implements ClusterCommand interface + */ +type CmdCheckVClusterServerPid struct { + checkPidOptions *vclusterops.VCheckVClusterServerPidOptions + + CmdBase +} + +func makeCmdCheckVClusterServerPid() *cobra.Command { + newCmd := &CmdCheckVClusterServerPid{} + opt := vclusterops.VCheckVClusterServerPidOptionsFactory() + newCmd.checkPidOptions = &opt + + cmd := makeBasicCobraCmd( + newCmd, + "check_pid", + "Check VCluster server PID files", + `Check VCluster server PID files in nodes to make sure that +only one host is running the VCluster server`, + []string{hostsFlag}, + ) + + return cmd +} + +func (c *CmdCheckVClusterServerPid) Parse(inputArgv []string, logger vlog.Printer) error { + c.argv = inputArgv + logger.LogArgParse(&c.argv) + + return c.validateParse(logger) +} + +func (c *CmdCheckVClusterServerPid) validateParse(logger vlog.Printer) error { + logger.Info("Called validateParse()") + if !c.usePassword() { + err := c.getCertFilesFromCertPaths(&c.checkPidOptions.DatabaseOptions) + if err != nil { + return err + } + } + return c.ValidateParseBaseOptions(&c.checkPidOptions.DatabaseOptions) +} + +func (c *CmdCheckVClusterServerPid) Run(vcc vclusterops.ClusterCommands) error { + vcc.V(1).Info("Called method Run()") + + hostsWithVclusterServerPid, err := vcc.VCheckVClusterServerPid(c.checkPidOptions) + if err != nil { + vcc.LogError(err, "failed to drop the database") + return err + } + fmt.Printf("Hosts with VCluster server PID files: %+v\n", hostsWithVclusterServerPid) + + return nil +} + +func (c *CmdCheckVClusterServerPid) SetDatabaseOptions(opt *vclusterops.DatabaseOptions) { + c.checkPidOptions.DatabaseOptions = *opt +} diff --git a/commands/vcluster_config.go b/commands/vcluster_config.go index fbeb894..3ddddbb 100644 --- a/commands/vcluster_config.go +++ b/commands/vcluster_config.go @@ -312,6 +312,11 @@ func (c *DatabaseConfig) write(configFilePath string, forceOverwrite bool) error return nil } +// Exposing the write function for external packages +func (c *DatabaseConfig) Write(configFilePath string, forceOverwrite bool) error { + return c.write(configFilePath, forceOverwrite) +} + // getHosts returns host addresses of all nodes in database func (c *DatabaseConfig) getHosts() []string { var hostList []string diff --git a/rfc7807/rfc7807_test.go b/rfc7807/rfc7807_test.go index ed4bb51..769eeb0 100644 --- a/rfc7807/rfc7807_test.go +++ b/rfc7807/rfc7807_test.go @@ -51,7 +51,7 @@ func TestHttpResponse(t *testing.T) { p := New(CommunalAccessError). WithDetail("communal endpoint is down"). WithHost("pod-2") - handler := func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, _ *http.Request) { p.SendError(w) } @@ -70,7 +70,7 @@ func TestProblemExtraction(t *testing.T) { origProblem := New(CommunalRWAccessError). WithDetail("could not read from communal storage"). WithHost("pod-3") - handler := func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, _ *http.Request) { origProblem.SendError(w) } @@ -94,7 +94,7 @@ func TestProblemExtraction(t *testing.T) { } func TestJSONExtractFailure(t *testing.T) { - handler := func(w http.ResponseWriter, r *http.Request) { + handler := func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, "not json") } req := httptest.NewRequest("GET", "http://vertica.com/bootstrapEndpoint", http.NoBody) diff --git a/vclusterops/check_vcluster_server_pid.go b/vclusterops/check_vcluster_server_pid.go new file mode 100644 index 0000000..50c5a01 --- /dev/null +++ b/vclusterops/check_vcluster_server_pid.go @@ -0,0 +1,100 @@ +/* + (c) Copyright [2023-2024] Open Text. + 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 vclusterops + +import ( + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" + "github.com/vertica/vcluster/vclusterops/vlog" + "golang.org/x/exp/slices" +) + +type VCheckVClusterServerPidOptions struct { + DatabaseOptions +} + +func VCheckVClusterServerPidOptionsFactory() VCheckVClusterServerPidOptions { + options := VCheckVClusterServerPidOptions{} + // set default values to the params + options.setDefaultValues() + + return options +} + +func (options *VCheckVClusterServerPidOptions) setDefaultValues() { + options.DatabaseOptions.setDefaultValues() +} + +func (options *VCheckVClusterServerPidOptions) analyzeOptions() (err error) { + // resolve RawHosts to be IP addresses + if len(options.RawHosts) > 0 { + options.Hosts, err = util.ResolveRawHostsToAddresses(options.RawHosts, options.IPv6) + if err != nil { + return err + } + } + + return nil +} + +func (options *VCheckVClusterServerPidOptions) validateAnalyzeOptions(_ vlog.Printer) error { + err := options.analyzeOptions() + if err != nil { + return err + } + return nil +} + +func (vcc VClusterCommands) VCheckVClusterServerPid( + options *VCheckVClusterServerPidOptions) (hostsWithVclusterServerPid []string, err error) { + // validate and analyze options + err = options.validateAnalyzeOptions(vcc.Log) + if err != nil { + return hostsWithVclusterServerPid, err + } + + // produce instructions of checking VCluster server PID files + instructions, err := vcc.produceCheckVClusterServerPidInstructions(options) + if err != nil { + return hostsWithVclusterServerPid, fmt.Errorf("fail to produce instructions, %w", err) + } + + // create a VClusterOpEngine, and add certs to the engine + clusterOpEngine := makeClusterOpEngine(instructions, options) + + // give the instructions to the VClusterOpEngine to run + runError := clusterOpEngine.run(vcc.Log) + if runError != nil { + return hostsWithVclusterServerPid, fmt.Errorf("fail to check VCluster server PID files: %w", runError) + } + + hostsWithVclusterServerPid = clusterOpEngine.execContext.HostsWithVclusterServerPid + slices.Sort(hostsWithVclusterServerPid) + + return hostsWithVclusterServerPid, nil +} + +func (vcc VClusterCommands) produceCheckVClusterServerPidInstructions(options *VCheckVClusterServerPidOptions) ([]clusterOp, error) { + var instructions []clusterOp + + nmaHealthOp := makeNMAHealthOpSkipUnreachable(options.Hosts) + + nmaCheckVClusterServerPidOp := makeNMACheckVClusterServerPidOp(options.Hosts) + + instructions = append(instructions, &nmaHealthOp, &nmaCheckVClusterServerPidOp) + return instructions, nil +} diff --git a/vclusterops/cluster_op.go b/vclusterops/cluster_op.go index 16efbf0..a725d67 100644 --- a/vclusterops/cluster_op.go +++ b/vclusterops/cluster_op.go @@ -565,6 +565,8 @@ type ClusterCommands interface { VPromoteSandboxToMain(options *VPromoteSandboxToMainOptions) error VRenameSubcluster(options *VRenameSubclusterOptions) error VFetchNodesDetails(options *VFetchNodesDetailsOptions) (NodesDetails, error) + + VCheckVClusterServerPid(options *VCheckVClusterServerPidOptions) ([]string, error) } type VClusterCommandsLogger struct { diff --git a/vclusterops/cluster_op_engine_context.go b/vclusterops/cluster_op_engine_context.go index 3c41025..0ad856b 100644 --- a/vclusterops/cluster_op_engine_context.go +++ b/vclusterops/cluster_op_engine_context.go @@ -41,6 +41,9 @@ type opEngineExecContext struct { // hosts that is not reachable through NMA unreachableHosts []string + + // hosts that have the VCluster server PID file + HostsWithVclusterServerPid []string } func makeOpEngineExecContext(logger vlog.Printer) opEngineExecContext { diff --git a/vclusterops/http_adapter.go b/vclusterops/http_adapter.go index a592e26..32dc199 100644 --- a/vclusterops/http_adapter.go +++ b/vclusterops/http_adapter.go @@ -390,7 +390,7 @@ func (adapter *httpAdapter) setupHTTPClient( // Note that hosts at this point are IP addresses, so verify-full may be impractical // or impossible due to the complications of issuing certificates valid for IPs. // Hence the custom validator skipping hostname validation. - config.VerifyPeerCertificate = adapter.generateTLSVerifyFunc(caCertPool) + config.VerifyPeerCertificate = util.GenerateTLSVerifyFunc(caCertPool) } } } @@ -399,47 +399,6 @@ func (adapter *httpAdapter) setupHTTPClient( return client, nil } -// generateTLSVerifyFunc returns a callback function suitable for use as the VerifyPeerCertificate -// field of a tls.Config struct. It is a slightly less performant but logically equivalent version of -// the validation logic which gets run when InsecureSkipVerify == false in go v1.20.11. The difference -// is that hostname validation is elided, which is not possible without custom verification. -// -// See crypto/x509/verify.go for hostname validation behavior and crypto/tls/handshake_client.go for -// the reference implementation of this function. -func (*httpAdapter) generateTLSVerifyFunc(rootCAs *x509.CertPool) func([][]byte, [][]*x509.Certificate) error { - return func(certificates [][]byte, _ [][]*x509.Certificate) error { - // Reparse certs. The crypto/tls package version does some extra checks, but they're already - // done by this point, so no need to repeat them. It also uses a cache to reduce parsing, which - // isn't included here, but could be if there is a perf issue. - certs := make([]*x509.Certificate, len(certificates)) - for i, asn1Data := range certificates { - cert, err := x509.ParseCertificate(asn1Data) - if err != nil { - return err - } - certs[i] = cert - } - - // construct verification options like reference implementation, minus hostname - opts := x509.VerifyOptions{ - Roots: rootCAs, - CurrentTime: time.Now(), - DNSName: "", - Intermediates: x509.NewCertPool(), - } - - for _, cert := range certs[1:] { - opts.Intermediates.AddCert(cert) - } - _, err := certs[0].Verify(opts) - if err != nil { - return &tls.CertificateVerificationError{UnverifiedCertificates: certs, Err: err} - } - - return nil - } -} - func buildQueryParamString(queryParams map[string]string) string { var queryParamString string if len(queryParams) == 0 { diff --git a/vclusterops/nma_check_vcluster_server_pid_op.go b/vclusterops/nma_check_vcluster_server_pid_op.go new file mode 100644 index 0000000..dad9aff --- /dev/null +++ b/vclusterops/nma_check_vcluster_server_pid_op.go @@ -0,0 +1,92 @@ +/* + (c) Copyright [2023-2024] Open Text. + 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 vclusterops + +import ( + "errors" +) + +// we limit the check timeout to 30 seconds +// we believe that this is enough to test the NMA connection +const nmaCheckVClusterServerPidTimeout = 30 + +type nmaCheckVClusterServerPidOp struct { + opBase +} + +func makeNMACheckVClusterServerPidOp(hosts []string) nmaCheckVClusterServerPidOp { + op := nmaCheckVClusterServerPidOp{} + op.name = "NMACheckVClusterServerPidOp" + op.description = "Check whether the VCluster server PID file exists" + op.hosts = hosts + return op +} + +// setupClusterHTTPRequest works as the module setup in Admintools +func (op *nmaCheckVClusterServerPidOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = GetMethod + httpRequest.buildNMAEndpoint("health/vcluster-server") + httpRequest.Timeout = nmaCheckVClusterServerPidTimeout + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *nmaCheckVClusterServerPidOp) prepare(execContext *opEngineExecContext) error { + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *nmaCheckVClusterServerPidOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +func (op *nmaCheckVClusterServerPidOp) finalize(_ *opEngineExecContext) error { + return nil +} + +func (op *nmaCheckVClusterServerPidOp) processResult(execContext *opEngineExecContext) error { + var allErrs error + + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + + if result.isPassing() { + resultMap, err := op.parseAndCheckMapResponse(host, result.content) + if err != nil { + return errors.Join(allErrs, err) + } + exist, ok := resultMap["vcluster_server_pid_file_exists"] + if !ok { + e := errors.New(`the key "vcluster_server_pid_file_exists" does not exist in the response`) + allErrs = errors.Join(allErrs, e) + } + if exist == "true" { + execContext.HostsWithVclusterServerPid = append(execContext.HostsWithVclusterServerPid, host) + } + } + } + + return allErrs +} diff --git a/vclusterops/util/tls.go b/vclusterops/util/tls.go new file mode 100644 index 0000000..d15472b --- /dev/null +++ b/vclusterops/util/tls.go @@ -0,0 +1,48 @@ +package util + +import ( + "crypto/tls" + "crypto/x509" + "time" +) + +// generateTLSVerifyFunc returns a callback function suitable for use as the VerifyPeerCertificate +// field of a tls.Config struct. It is a slightly less performant but logically equivalent version of +// the validation logic which gets run when InsecureSkipVerify == false in go v1.20.11. The difference +// is that hostname validation is elided, which is not possible without custom verification. +// +// See crypto/x509/verify.go for hostname validation behavior and crypto/tls/handshake_client.go for +// the reference implementation of this function. +func GenerateTLSVerifyFunc(rootCAs *x509.CertPool) func([][]byte, [][]*x509.Certificate) error { + return func(certificates [][]byte, _ [][]*x509.Certificate) error { + // Reparse certs. The crypto/tls package version does some extra checks, but they're already + // done by this point, so no need to repeat them. It also uses a cache to reduce parsing, which + // isn't included here, but could be if there is a perf issue. + certs := make([]*x509.Certificate, len(certificates)) + for i, asn1Data := range certificates { + cert, err := x509.ParseCertificate(asn1Data) + if err != nil { + return err + } + certs[i] = cert + } + + // construct verification options like reference implementation, minus hostname + opts := x509.VerifyOptions{ + Roots: rootCAs, + CurrentTime: time.Now(), + DNSName: "", + Intermediates: x509.NewCertPool(), + } + + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + _, err := certs[0].Verify(opts) + if err != nil { + return &tls.CertificateVerificationError{UnverifiedCertificates: certs, Err: err} + } + + return nil + } +}