diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index e573adabbde..a22591f64e6 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -26,11 +26,12 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubemaster "k8s.io/kubernetes/cmd/kubeadm/app/master" + "k8s.io/kubernetes/cmd/kubeadm/app/preflight" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/util" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/cloudprovider" _ "k8s.io/kubernetes/pkg/cloudprovider/providers" - cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/runtime" netutil "k8s.io/kubernetes/pkg/util/net" ) @@ -49,6 +50,7 @@ var ( func NewCmdInit(out io.Writer) *cobra.Command { cfg := &kubeadmapi.MasterConfiguration{} var cfgPath string + var skipPreFlight 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, skipPreFlight) 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( + &skipPreFlight, "skip-preflight-checks", false, + "skip preflight 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, skipPreFlight bool) (*Init, error) { if cfgPath != "" { b, err := ioutil.ReadFile(cfgPath) if err != nil { @@ -142,6 +148,17 @@ func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration) (*Init, error) return nil, fmt.Errorf("unable to decode config from %q [%v]", cfgPath, err) } } + + if !skipPreFlight { + fmt.Println(" Running pre-flight checks") + err := preflight.RunInitMasterChecks() + if err != nil { + return nil, err + } + } 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..adca8ed82d0 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -25,8 +25,8 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubenode "k8s.io/kubernetes/cmd/kubeadm/app/node" + "k8s.io/kubernetes/cmd/kubeadm/app/preflight" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" - cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) var ( @@ -43,12 +43,13 @@ var ( // NewCmdJoin returns "kubeadm join" command. func NewCmdJoin(out io.Writer) *cobra.Command { cfg := &kubeadmapi.NodeConfiguration{} + var skipPreFlight 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) - cmdutil.CheckErr(err) + err := RunJoin(out, cmd, args, cfg, skipPreFlight) + kubeadmutil.CheckErr(err) }, } @@ -57,12 +58,27 @@ func NewCmdJoin(out io.Writer) *cobra.Command { "(required) Shared secret used to secure bootstrap. Must match the output of 'kubeadm init'", ) + cmd.PersistentFlags().BoolVar( + &skipPreFlight, "skip-preflight-checks", false, + "skip preflight 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, skipPreFlight bool) error { // TODO(phase1+) this we are missing args from the help text, there should be a way to tell cobra about it + if !skipPreFlight { + fmt.Println("Running pre-flight checks") + err := preflight.RunJoinNodeChecks() + if err != nil { + return err + } + } else { + fmt.Println("Skipping pre-flight checks") + } + if len(args) == 0 { return fmt.Errorf(" must specify master IP address (see --help)") } diff --git a/cmd/kubeadm/app/kubeadm.go b/cmd/kubeadm/app/kubeadm.go index e90bb775e11..f17ac226895 100644 --- a/cmd/kubeadm/app/kubeadm.go +++ b/cmd/kubeadm/app/kubeadm.go @@ -19,7 +19,6 @@ package app import ( "os" - "github.com/renstrom/dedent" "github.com/spf13/pflag" "k8s.io/kubernetes/cmd/kubeadm/app/cmd" @@ -27,12 +26,6 @@ import ( "k8s.io/kubernetes/pkg/util/logs" ) -var AlphaWarningOnExit = dedent.Dedent(` - kubeadm: I am an alpha version, my authors welcome your feedback and bug reports - kubeadm: please create an issue using https://github.com/kubernetes/kubernetes/issues/new - kubeadm: and make sure to mention @kubernetes/sig-cluster-lifecycle. Thank you! -`) - func Run() error { logs.InitLogs() defer logs.FlushLogs() diff --git a/cmd/kubeadm/app/preflight/checks.go b/cmd/kubeadm/app/preflight/checks.go new file mode 100644 index 00000000000..748d8ecfb85 --- /dev/null +++ b/cmd/kubeadm/app/preflight/checks.go @@ -0,0 +1,238 @@ +/* +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 preflight + +import ( + "fmt" + "io" + "net" + "os" + "os/exec" + + "k8s.io/kubernetes/pkg/util/initsystem" +) + +type PreFlightError struct { + Msg string + Count int +} + +func (e *PreFlightError) Error() string { + return fmt.Sprintf("preflight check error\n count: %d \n msg: %s", e.Count, e.Msg) +} + +// PreFlightCheck validates the state of the system to ensure kubeadm will be +// successful as often as possilble. +type PreFlightCheck interface { + Check() (warnings, errors []error) +} + +// 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, errors []error) { + initSystem := initsystem.GetInitSystem() + if initSystem == nil { + return []error{fmt.Errorf("no supported init system detected, skipping service checks for %s", sc.service)}, nil + } + + warnings = []error{} + + if !initSystem.ServiceExists(sc.service) { + warnings = append(warnings, fmt.Errorf("%s service does not exist", sc.service)) + return warnings, nil + } + + if !initSystem.ServiceIsEnabled(sc.service) { + warnings = append(warnings, + fmt.Errorf("%s service is not enabled, please run 'systemctl enable %s.service'", + sc.service, sc.service)) + } + + if !initSystem.ServiceIsActive(sc.service) { + errors = append(errors, + fmt.Errorf("%s service is not active, please run 'systemctl start %s.service'", + sc.service, sc.service)) + } + + return warnings, errors +} + +// PortOpenCheck ensures the given port is available for use. +type PortOpenCheck struct { + port int +} + +func (poc PortOpenCheck) Check() (warnings, errors []error) { + errors = []error{} + // TODO: Get IP from KubeadmConfig + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", poc.port)) + if err != nil { + errors = append(errors, fmt.Errorf("Port %d is in use", poc.port)) + } + if ln != nil { + ln.Close() + } + + return nil, errors +} + +// IsRootCheck verifies user is root +type IsRootCheck struct { + root bool +} + +func (irc IsRootCheck) Check() (warnings, errors []error) { + errors = []error{} + if os.Getuid() != 0 { + errors = append(errors, fmt.Errorf("user is not running as root")) + } + + 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, errors []error) { + errors = []error{} + // 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.Errorf("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.Errorf("%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, errors []error) { + _, err := exec.LookPath(ipc.executable) + if err != nil { + if ipc.mandatory { + // Return as an error: + return nil, []error{fmt.Errorf("%s not found in system path", ipc.executable)} + } + // Return as a warning: + return []error{fmt.Errorf("%s not found in system path", ipc.executable)}, nil + } + return nil, nil +} + +func RunInitMasterChecks() error { + // TODO: Some of these ports should come from kubeadm config eventually: + checks := []PreFlightCheck{ + IsRootCheck{root: true}, + 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: "ebtables", mandatory: true}, + InPathCheck{executable: "ethtool", mandatory: true}, + InPathCheck{executable: "ip", mandatory: true}, + InPathCheck{executable: "iptables", mandatory: true}, + InPathCheck{executable: "mount", mandatory: true}, + InPathCheck{executable: "nsenter", mandatory: true}, + InPathCheck{executable: "socat", mandatory: true}, + InPathCheck{executable: "tc", mandatory: false}, + InPathCheck{executable: "touch", mandatory: false}, + } + + return runChecks(checks) +} + +func RunJoinNodeChecks() error { + // TODO: Some of these ports should come from kubeadm config eventually: + checks := []PreFlightCheck{ + IsRootCheck{root: true}, + ServiceCheck{service: "docker"}, + ServiceCheck{service: "kubelet"}, + PortOpenCheck{port: 8080}, + PortOpenCheck{port: 10250}, + PortOpenCheck{port: 10251}, + PortOpenCheck{port: 10252}, + DirAvailableCheck{path: "/etc/kubernetes"}, + DirAvailableCheck{path: "/var/lib/kubelet"}, + InPathCheck{executable: "ebtables", mandatory: true}, + InPathCheck{executable: "ethtool", mandatory: true}, + InPathCheck{executable: "ip", mandatory: true}, + InPathCheck{executable: "iptables", mandatory: true}, + InPathCheck{executable: "mount", mandatory: true}, + InPathCheck{executable: "nsenter", mandatory: true}, + InPathCheck{executable: "socat", mandatory: true}, + InPathCheck{executable: "tc", mandatory: false}, + InPathCheck{executable: "touch", mandatory: false}, + } + + return 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) error { + found := []error{} + for _, c := range checks { + warnings, errors := c.Check() + for _, w := range warnings { + fmt.Printf(" WARNING: %s\n", w) + } + for _, e := range errors { + found = append(found, e) + } + } + if len(found) > 0 { + errors := "\n" + for _, i := range found { + errors += "\t" + i.Error() + "\n" + } + return &PreFlightError{ + Msg: errors, + Count: len(found), + } + } + return nil +} diff --git a/cmd/kubeadm/app/util/error.go b/cmd/kubeadm/app/util/error.go new file mode 100644 index 00000000000..d8f7c6ec95b --- /dev/null +++ b/cmd/kubeadm/app/util/error.go @@ -0,0 +1,92 @@ +/* +Copyright 2014 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 util + +import ( + "fmt" + "os" + "strings" + + "k8s.io/kubernetes/cmd/kubeadm/app/preflight" + + "github.com/golang/glog" + "github.com/renstrom/dedent" +) + +const ( + DefaultErrorExitCode = 1 + PreFlight = 2 +) + +var AlphaWarningOnExit = dedent.Dedent(` + kubeadm: I am an alpha version, my authors welcome your feedback and bug reports + kubeadm: please create issue an using https://github.com/kubernetes/kubernetes/issues/new + kubeadm: and make sure to mention @kubernetes/sig-cluster-lifecycle. Thank you! +`) + +type debugError interface { + DebugError() (msg string, args []interface{}) +} + +var fatalErrHandler = fatal + +// BehaviorOnFatal allows you to override the default behavior when a fatal +// error occurs, which is to call os.Exit(code). You can pass 'panic' as a function +// here if you prefer the panic() over os.Exit(1). +func BehaviorOnFatal(f func(string, int)) { + fatalErrHandler = f +} + +// fatal prints the message if set and then exits. If V(2) or greater, glog.Fatal +// is invoked for extended information. +func fatal(msg string, code int) { + if len(msg) > 0 { + // add newline if needed + if !strings.HasSuffix(msg, "\n") { + msg += "\n" + } + + if glog.V(2) { + glog.FatalDepth(2, msg) + } + fmt.Fprint(os.Stderr, msg) + } + os.Exit(code) +} + +// CheckErr prints a user friendly error to STDERR and exits with a non-zero +// exit code. Unrecognized errors will be printed with an "error: " prefix. +// +// This method is generic to the command in use and may be used by non-Kubectl +// commands. +func CheckErr(err error) { + checkErr("", err, fatalErrHandler) +} + +// checkErr formats a given error as a string and calls the passed handleErr +// func with that string and an kubectl exit code. +func checkErr(prefix string, err error, handleErr func(string, int)) { + switch err.(type) { + case nil: + return + case *preflight.PreFlightError: + handleErr(err.Error(), PreFlight) + default: + fmt.Printf(AlphaWarningOnExit) + handleErr(err.Error(), DefaultErrorExitCode) + } +} diff --git a/cmd/kubeadm/kubeadm.go b/cmd/kubeadm/kubeadm.go index 902e46127da..6c743cd7a21 100644 --- a/cmd/kubeadm/kubeadm.go +++ b/cmd/kubeadm/kubeadm.go @@ -21,12 +21,12 @@ import ( "os" "k8s.io/kubernetes/cmd/kubeadm/app" + "k8s.io/kubernetes/cmd/kubeadm/app/util" ) -// TODO(phase1+): check for root func main() { if err := app.Run(); err != nil { - fmt.Printf(app.AlphaWarningOnExit) + fmt.Printf(util.AlphaWarningOnExit) os.Exit(1) } os.Exit(0) diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index e4d7c4c4d60..54fdefb3f65 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -513,6 +513,7 @@ since-seconds since-time skip-generated-rewrite skip-munges +skip-preflight-checks sort-by source-file ssh-env diff --git a/pkg/util/initsystem/initsystem.go b/pkg/util/initsystem/initsystem.go new file mode 100644 index 00000000000..43ea818eb9d --- /dev/null +++ b/pkg/util/initsystem/initsystem.go @@ -0,0 +1,80 @@ +/* +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 initsystem + +import ( + "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 { + 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 +}