package cluster
import (
"errors"
"fmt"
"net"
"net/url"
"strings"
kapi "k8s.io/kubernetes/pkg/api"
kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
authorizationapi "github.com/openshift/origin/pkg/authorization/api"
osclient "github.com/openshift/origin/pkg/client"
configapilatest "github.com/openshift/origin/pkg/cmd/server/api/latest"
"github.com/openshift/origin/pkg/diagnostics/types"
sdnapi "github.com/openshift/origin/pkg/sdn/api"
)
const masterNotRunningAsANode = `Unable to find a node matching the cluster server IP.
This may indicate the master is not also running a node, and is unable
to proxy to pods over the Open vSwitch SDN.
`
// MasterNode is a Diagnostic for checking that the OpenShift master is also running as node.
// This is currently required to have the master on the Open vSwitch SDN and able to communicate
// with other nodes.
type MasterNode struct {
KubeClient *kclientset.Clientset
OsClient *osclient.Client
ServerUrl string
MasterConfigFile string // may often be empty if not being run on the host
}
const MasterNodeName = "MasterNode"
func (d *MasterNode) Name() string {
return MasterNodeName
}
func (d *MasterNode) Description() string {
return "Check if master is also running node (for Open vSwitch)"
}
func (d *MasterNode) CanRun() (bool, error) {
if d.KubeClient == nil || d.OsClient == nil {
return false, errors.New("must have kube and os client")
}
if d.ServerUrl == "" {
return false, errors.New("must have a server URL")
}
// If there is a master config file available, we'll perform an additional
// check to see if an OVS network plugin is in use. If no master config,
// we assume this is the case for now and let the check run anyhow.
if len(d.MasterConfigFile) > 0 {
// Parse the master config and check the network plugin name:
masterCfg, masterErr := configapilatest.ReadAndResolveMasterConfig(d.MasterConfigFile)
if masterErr != nil {
return false, types.DiagnosticError{ID: "DClu3008",
LogMessage: fmt.Sprintf("Master config provided but unable to parse: %s", masterErr), Cause: masterErr}
}
if !sdnapi.IsOpenShiftNetworkPlugin(masterCfg.NetworkConfig.NetworkPluginName) {
return false, errors.New(fmt.Sprintf("Network plugin does not require master to also run node: %s", masterCfg.NetworkConfig.NetworkPluginName))
}
}
can, err := userCan(d.OsClient, authorizationapi.Action{
Verb: "list",
Group: kapi.GroupName,
Resource: "nodes",
})
if err != nil {
return false, types.DiagnosticError{ID: "DClu3000", LogMessage: fmt.Sprintf(clientErrorGettingNodes, err), Cause: err}
} else if !can {
return false, types.DiagnosticError{ID: "DClu3001", LogMessage: "Client does not have access to see node status", Cause: err}
}
return true, nil
}
func (d *MasterNode) Check() types.DiagnosticResult {
r := types.NewDiagnosticResult(MasterNodeName)
nodes, err := d.KubeClient.Core().Nodes().List(kapi.ListOptions{})
if err != nil {
r.Error("DClu3002", err, fmt.Sprintf(clientErrorGettingNodes, err))
return r
}
// Provide the actual net.LookupHost as the DNS resolver:
serverIps, err := resolveServerIP(d.ServerUrl, net.LookupHost)
if err != nil {
r.Error("DClu3007", err, "Error resolving servers IP")
return r
}
return searchNodesForIP(nodes.Items, serverIps)
}
// Define a resolve callback function type, use to swap in a dummy implementation
// in tests and avoid actual DNS calls.
type dnsResolver func(string) ([]string, error)
// resolveServerIP extracts the hostname portion of the API server URL passed in,
// and attempts dns resolution. It also attempts to catch server URL's that already
// contain both IPv4 and IPv6 addresses.
func resolveServerIP(serverUrl string, fn dnsResolver) ([]string, error) {
// Extract the hostname from the API server URL:
u, err := url.Parse(serverUrl)
if err != nil || u.Host == "" {
return nil, errors.New(fmt.Sprintf("Unable to parse hostname from URL: %s", serverUrl))
}
// Trim the port, if one exists, and watchout for IPv6 URLs.
if strings.Count(u.Host, ":") > 1 {
// Check if this is an IPv6 address as is to avoid problems with splitting
// off the port:
ipv6 := net.ParseIP(u.Host)
if ipv6 != nil {
return []string{ipv6.String()}, nil
}
}
hostname, _, err := net.SplitHostPort(u.Host)
if err != nil && hostname == "" {
// Likely didn't have a port, carry on:
hostname = u.Host
}
// Check if the hostname already looks like an IPv4 or IPv6 address:
goIp := net.ParseIP(hostname)
if goIp != nil {
return []string{goIp.String()}, nil
}
// If not, attempt a DNS lookup. We may get multiple addresses for the hostname,
// we'll return them all and search for any match in Kube nodes:
ips, err := fn(hostname)
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to perform DNS lookup for: %s", hostname))
}
return ips, nil
}
func searchNodesForIP(nodes []kapi.Node, ips []string) types.DiagnosticResult {
r := types.NewDiagnosticResult(MasterNodeName)
r.Debug("DClu3005", fmt.Sprintf("Seaching for a node with master IP: %s", ips))
// Loops = # of nodes * number of IPs per node (2 commonly) * # of IPs the
// server hostname resolves to. (should usually be 1)
for _, node := range nodes {
for _, address := range node.Status.Addresses {
for _, ipAddress := range ips {
r.Debug("DClu3006", fmt.Sprintf("Checking node %s address %s",
node.ObjectMeta.Name, address.Address))
if address.Address == ipAddress {
r.Info("DClu3003", fmt.Sprintf("Found a node with same IP as master: %s",
node.ObjectMeta.Name))
return r
}
}
}
}
r.Warn("DClu3004", nil, masterNotRunningAsANode)
return r
}