diff --git a/cmd/kubeadm/app/checks/checks.go b/cmd/kubeadm/app/checks/checks.go new file mode 100644 index 00000000000..1b5a39e16db --- /dev/null +++ b/cmd/kubeadm/app/checks/checks.go @@ -0,0 +1,191 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 checks + +import ( + "fmt" + "io" + "net" + "os" + "os/exec" +) + +// PreFlightCheck validates the state of the system to ensure kubeadm will be +// successful as often as possilble. +type PreFlightCheck interface { + Check() (warnings []string, errors []string) +} + +// ServiceCheck verifies that the given service is enabled and active. If we do not +// detect a supported init system however, all checks are skipped and a warning is +// returned. +type ServiceCheck struct { + service string +} + +func (sc ServiceCheck) Check() (warnings []string, errors []string) { + + initSystem := getInitSystem() + if initSystem == nil { + return []string{"no supported init system detected, skipping service checks"}, nil + } + + warnings = []string{} + + if !initSystem.ServiceExists(sc.service) { + warnings = append(warnings, fmt.Sprintf("%s service does not exist", sc.service)) + return warnings, nil + } + + if !initSystem.ServiceIsEnabled(sc.service) { + warnings = append(warnings, + fmt.Sprintf("%s service is not enabled, please run 'systemctl enable %s.service'", + sc.service, sc.service)) + } + + if !initSystem.ServiceIsActive(sc.service) { + errors = append(errors, + fmt.Sprintf("%s service is not active, please run 'systemctl start %s.service'", + sc.service, sc.service)) + } + + return warnings, nil +} + +// PortOpenCheck ensures the given port is available for use. +type PortOpenCheck struct { + port int +} + +func (poc PortOpenCheck) Check() (warnings []string, errors []string) { + errors = []string{} + // TODO: Get IP from KubeadmConfig + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", poc.port)) + if err != nil { + errors = append(errors, fmt.Sprintf("Port %d is in use.", poc.port)) + } + if ln != nil { + ln.Close() + } + + return nil, errors +} + +// DirAvailableCheck checks if the given directory either does not exist, or +// is empty. +type DirAvailableCheck struct { + path string +} + +func (dac DirAvailableCheck) Check() (warnings []string, errors []string) { + errors = []string{} + // If it doesn't exist we are good: + if _, err := os.Stat(dac.path); os.IsNotExist(err) { + return nil, nil + } + + f, err := os.Open(dac.path) + if err != nil { + errors = append(errors, fmt.Sprintf("Unable to check if %s is empty: %s", dac.path, err)) + return nil, errors + } + defer f.Close() + + _, err = f.Readdirnames(1) + if err != io.EOF { + errors = append(errors, fmt.Sprintf("%s is not empty", dac.path)) + } + + return nil, errors +} + +// InPathChecks checks if the given executable is present in the path. +type InPathCheck struct { + executable string + mandatory bool +} + +func (ipc InPathCheck) Check() (warnings []string, errors []string) { + _, err := exec.LookPath(ipc.executable) + if err != nil { + if ipc.mandatory { + // Return as an error: + return nil, []string{fmt.Sprintf("%s not found in system path.", ipc.executable)} + } + // Return as a warning: + return []string{fmt.Sprintf("%s not found in system path.", ipc.executable)}, nil + } + return nil, nil +} + +func RunInitMasterChecks() { + // TODO: Some of these ports should come from kubeadm config eventually: + checks := []PreFlightCheck{ + ServiceCheck{service: "kubelet"}, + ServiceCheck{service: "docker"}, + PortOpenCheck{port: 443}, + PortOpenCheck{port: 2379}, + PortOpenCheck{port: 8080}, + PortOpenCheck{port: 10250}, + PortOpenCheck{port: 10251}, + PortOpenCheck{port: 10252}, + DirAvailableCheck{path: "/etc/kubernetes"}, + DirAvailableCheck{path: "/var/lib/etcd"}, + DirAvailableCheck{path: "/var/lib/kubelet"}, + InPathCheck{executable: "socat", mandatory: true}, + InPathCheck{executable: "ethtool", mandatory: true}, + } + + runChecks(checks) +} + +func RunJoinNodeChecks() { + // TODO: Some of these ports should come from kubeadm config eventually: + checks := []PreFlightCheck{ + ServiceCheck{service: "kubelet"}, + ServiceCheck{service: "docker"}, + PortOpenCheck{port: 8080}, + PortOpenCheck{port: 10250}, + PortOpenCheck{port: 10251}, + PortOpenCheck{port: 10252}, + DirAvailableCheck{path: "/etc/kubernetes"}, + DirAvailableCheck{path: "/var/lib/kubelet"}, + InPathCheck{executable: "socat", mandatory: true}, + InPathCheck{executable: "ethtool", mandatory: true}, + } + + runChecks(checks) +} + +// runChecks runs each check, displays it's warnings/errors, and once all +// are processed will exit if any errors occurred. +func runChecks(checks []PreFlightCheck) { + foundErrors := false + for _, check := range checks { + warnings, errors := check.Check() + for _, warnMsg := range warnings { + fmt.Printf("WARNING: %s\n", warnMsg) + } + for _, errMsg := range errors { + foundErrors = true + fmt.Printf("ERROR: %s\n", errMsg) + } + } + if foundErrors { + os.Exit(1) + } +} diff --git a/cmd/kubeadm/app/checks/initsystem.go b/cmd/kubeadm/app/checks/initsystem.go new file mode 100644 index 00000000000..06dd49ad4a2 --- /dev/null +++ b/cmd/kubeadm/app/checks/initsystem.go @@ -0,0 +1,83 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 checks + +import ( + "fmt" + "os/exec" + "strings" +) + +type InitSystem interface { + + // ServiceExists ensures the service is defined for this init system. + ServiceExists(service string) bool + + // ServiceIsEnabled ensures the service is enabled to start on each boot. + ServiceIsEnabled(service string) bool + + // ServiceIsActive ensures the service is running, or attempting to run. (crash looping in the case of kubelet) + ServiceIsActive(service string) bool +} + +type SystemdInitSystem struct{} + +func (sysd SystemdInitSystem) ServiceExists(service string) bool { + args := []string{"status", service} + outBytes, _ := exec.Command("systemctl", args...).Output() + output := string(outBytes) + if strings.Contains(output, "Loaded: not-found") { + return false + } + return true +} + +func (sysd SystemdInitSystem) ServiceIsEnabled(service string) bool { + args := []string{"is-enabled", service} + _, err := exec.Command("systemctl", args...).Output() + if err != nil { + fmt.Println(err) + return false + } + return true +} + +// ServiceIsActive will check is the service is "active". In the case of +// crash looping services (kubelet in our case) status will return as +// "activating", so we will consider this active as well. +func (sysd SystemdInitSystem) ServiceIsActive(service string) bool { + args := []string{"is-active", service} + // Ignoring error here, command returns non-0 if in "activating" status: + outBytes, _ := exec.Command("systemctl", args...).Output() + output := strings.TrimSpace(string(outBytes)) + if output == "active" || output == "activating" { + return true + } + return false +} + +// getInitSystem returns an InitSystem for the current system, or nil +// if we cannot detect a supported init system for pre-flight checks. +// This indicates we will skip init system checks, not an error. +func getInitSystem() InitSystem { + // Assume existence of systemctl in path implies this is a systemd system: + _, err := exec.LookPath("systemctl") + if err == nil { + return &SystemdInitSystem{} + } + return nil +} diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index bd369b20d00..15946a6a145 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/cobra" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubechecks "k8s.io/kubernetes/cmd/kubeadm/app/checks" kubemaster "k8s.io/kubernetes/cmd/kubeadm/app/master" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/pkg/api" @@ -49,6 +50,7 @@ var ( func NewCmdInit(out io.Writer) *cobra.Command { cfg := &kubeadmapi.MasterConfiguration{} var cfgPath string + var skipChecks bool cmd := &cobra.Command{ Use: "init", Short: "Run this in order to set up the Kubernetes master.", @@ -58,7 +60,7 @@ func NewCmdInit(out io.Writer) *cobra.Command { cmdutil.CheckErr(fmt.Errorf(" %v", err)) } } - i, err := NewInit(cfgPath, cfg) + i, err := NewInit(cfgPath, cfg, skipChecks) check(err) check(i.Run(out)) }, @@ -124,6 +126,10 @@ func NewCmdInit(out io.Writer) *cobra.Command { "etcd client key file. Note: The path must be in /etc/ssl/certs", ) cmd.PersistentFlags().MarkDeprecated("external-etcd-keyfile", "this flag will be removed when componentconfig exists") + cmd.PersistentFlags().BoolVar( + &skipChecks, "skip-checks", false, + "skip checks normally run before modifying the system", + ) return cmd } @@ -132,7 +138,7 @@ type Init struct { cfg *kubeadmapi.MasterConfiguration } -func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration) (*Init, error) { +func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipChecks bool) (*Init, error) { if cfgPath != "" { b, err := ioutil.ReadFile(cfgPath) if err != nil { @@ -142,6 +148,14 @@ func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration) (*Init, error) return nil, fmt.Errorf("unable to decode config from %q [%v]", cfgPath, err) } } + + if !skipChecks { + fmt.Println("Running pre-flight checks") + kubechecks.RunInitMasterChecks() + } else { + fmt.Println("Skipping pre-flight checks") + } + // Auto-detect the IP if len(cfg.API.AdvertiseAddresses) == 0 { // TODO(phase1+) perhaps we could actually grab eth0 and eth1 diff --git a/cmd/kubeadm/app/cmd/join.go b/cmd/kubeadm/app/cmd/join.go index 99ff5a0c97a..a2b6508f40e 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubechecks "k8s.io/kubernetes/cmd/kubeadm/app/checks" kubenode "k8s.io/kubernetes/cmd/kubeadm/app/node" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" @@ -43,11 +44,12 @@ var ( // NewCmdJoin returns "kubeadm join" command. func NewCmdJoin(out io.Writer) *cobra.Command { cfg := &kubeadmapi.NodeConfiguration{} + var skipChecks bool cmd := &cobra.Command{ Use: "join", Short: "Run this on any machine you wish to join an existing cluster.", Run: func(cmd *cobra.Command, args []string) { - err := RunJoin(out, cmd, args, cfg) + err := RunJoin(out, cmd, args, cfg, skipChecks) cmdutil.CheckErr(err) }, } @@ -57,11 +59,24 @@ func NewCmdJoin(out io.Writer) *cobra.Command { "(required) Shared secret used to secure bootstrap. Must match the output of 'kubeadm init'", ) + cmd.PersistentFlags().BoolVar( + &skipChecks, "skip-checks", false, + "skip checks normally run before modifying the system", + ) + return cmd } // RunJoin executes worked node provisioning and tries to join an existing cluster. -func RunJoin(out io.Writer, cmd *cobra.Command, args []string, s *kubeadmapi.NodeConfiguration) error { +func RunJoin(out io.Writer, cmd *cobra.Command, args []string, s *kubeadmapi.NodeConfiguration, skipChecks bool) error { + + if !skipChecks { + fmt.Println("Running pre-flight checks") + kubechecks.RunJoinNodeChecks() + } else { + fmt.Println("Skipping pre-flight checks") + } + // TODO(phase1+) this we are missing args from the help text, there should be a way to tell cobra about it if len(args) == 0 { return fmt.Errorf(" must specify master IP address (see --help)")