| @@ -0,0 +1,86 @@ | ||
| package health | ||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "time" | ||
| . "github.com/onsi/ginkgo" | ||
| . "github.com/onsi/gomega" | ||
| "github.com/onsi/gomega/ghttp" | ||
| "k8s.io/api/core/v1" | ||
| v12 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "github.com/onsi/ginkgo/extensions/table" | ||
| "k8s.io/apimachinery/pkg/util/clock" | ||
| v13 "kubevirt.io/kubevirt/pkg/api/v1" | ||
| "kubevirt.io/kubevirt/pkg/kubecli" | ||
| "kubevirt.io/kubevirt/pkg/log" | ||
| "kubevirt.io/kubevirt/pkg/virt-handler/devices" | ||
| "kubevirt.io/kubevirt/pkg/virt-handler/isolation" | ||
| ) | ||
| var _ = Describe("Health", func() { | ||
| var server *ghttp.Server | ||
| var client kubecli.KubevirtClient | ||
| log.Log.SetIOWriter(GinkgoWriter) | ||
| BeforeEach(func() { | ||
| var err error | ||
| server = ghttp.NewServer() | ||
| client, err = kubecli.GetKubevirtClientFromFlags(server.URL(), "") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| }) | ||
| table.DescribeTable("should mark the nodes", func(schedulable bool, device devices.Device) { | ||
| now := v12.Now() | ||
| t, err := json.Marshal(now) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| patch := fmt.Sprintf(`{"metadata": { "labels": {"kubevirt.io/schedulable": "%t"}, "annotations": {"kubevirt.io/heartbeat": %s}}}`, schedulable, string(t)) | ||
| server.AppendHandlers(ghttp.CombineHandlers( | ||
| ghttp.VerifyRequest(http.MethodPatch, "/api/v1/nodes/testhost"), | ||
| ghttp.VerifyBody([]byte(patch)), | ||
| ghttp.RespondWithJSONEncoded(http.StatusOK, &v1.Node{}), | ||
| )) | ||
| stop := make(chan struct{}) | ||
| defer close(stop) | ||
| go func() { | ||
| GinkgoRecover() | ||
| checker := &ReadinessChecker{ | ||
| clientset: client, | ||
| host: "testhost", | ||
| plugins: map[string]devices.Device{"test": device}, | ||
| clock: clock.NewFakeClock(now.Time), | ||
| } | ||
| checker.HeartBeat(1*time.Second, 100, stop) | ||
| }() | ||
| time.Sleep(500 * time.Millisecond) | ||
| Expect(server.ReceivedRequests()).To(HaveLen(1)) | ||
| }, | ||
| table.Entry("should mark the node as non-schedulable because of a failing device check", false, &testDevice{fail: true}), | ||
| table.Entry("should mark the node as schedulable", true, &testDevice{fail: false}), | ||
| ) | ||
| AfterEach(func() { | ||
| server.Close() | ||
| }) | ||
| }) | ||
| type testDevice struct { | ||
| fail bool | ||
| } | ||
| func (*testDevice) Setup(vmi *v13.VirtualMachineInstance, hostNamespaces *isolation.IsolationResult, podNamespaces *isolation.IsolationResult) error { | ||
| return nil | ||
| } | ||
| func (t *testDevice) Available() error { | ||
| if t.fail { | ||
| return fmt.Errorf("failing") | ||
| } | ||
| return nil | ||
| } |
| @@ -0,0 +1,52 @@ | ||
| // Automatically generated by MockGen. DO NOT EDIT! | ||
| // Source: isolation.go | ||
| package isolation | ||
| import ( | ||
| gomock "github.com/golang/mock/gomock" | ||
| v1 "kubevirt.io/kubevirt/pkg/api/v1" | ||
| ) | ||
| // Mock of PodIsolationDetector interface | ||
| type MockPodIsolationDetector struct { | ||
| ctrl *gomock.Controller | ||
| recorder *_MockPodIsolationDetectorRecorder | ||
| } | ||
| // Recorder for MockPodIsolationDetector (not exported) | ||
| type _MockPodIsolationDetectorRecorder struct { | ||
| mock *MockPodIsolationDetector | ||
| } | ||
| func NewMockPodIsolationDetector(ctrl *gomock.Controller) *MockPodIsolationDetector { | ||
| mock := &MockPodIsolationDetector{ctrl: ctrl} | ||
| mock.recorder = &_MockPodIsolationDetectorRecorder{mock} | ||
| return mock | ||
| } | ||
| func (_m *MockPodIsolationDetector) EXPECT() *_MockPodIsolationDetectorRecorder { | ||
| return _m.recorder | ||
| } | ||
| func (_m *MockPodIsolationDetector) Detect(vm *v1.VirtualMachineInstance) (*IsolationResult, error) { | ||
| ret := _m.ctrl.Call(_m, "Detect", vm) | ||
| ret0, _ := ret[0].(*IsolationResult) | ||
| ret1, _ := ret[1].(error) | ||
| return ret0, ret1 | ||
| } | ||
| func (_mr *_MockPodIsolationDetectorRecorder) Detect(arg0 interface{}) *gomock.Call { | ||
| return _mr.mock.ctrl.RecordCall(_mr.mock, "Detect", arg0) | ||
| } | ||
| func (_m *MockPodIsolationDetector) Whitelist(controller []string) PodIsolationDetector { | ||
| ret := _m.ctrl.Call(_m, "Whitelist", controller) | ||
| ret0, _ := ret[0].(PodIsolationDetector) | ||
| return ret0 | ||
| } | ||
| func (_mr *_MockPodIsolationDetectorRecorder) Whitelist(arg0 interface{}) *gomock.Call { | ||
| return _mr.mock.ctrl.RecordCall(_mr.mock, "Whitelist", arg0) | ||
| } |
| @@ -0,0 +1,206 @@ | ||
| /* | ||
| * This file is part of the kubevirt project | ||
| * | ||
| * 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. | ||
| * | ||
| * Copyright 2017 Red Hat, Inc. | ||
| * | ||
| */ | ||
| package isolation | ||
| //go:generate mockgen -source $GOFILE -package=$GOPACKAGE -destination=generated_mock_$GOFILE | ||
| /* | ||
| ATTENTION: Rerun code generators when interface signatures are modified. | ||
| */ | ||
| import ( | ||
| "bufio" | ||
| "fmt" | ||
| "net" | ||
| "os" | ||
| "strings" | ||
| "syscall" | ||
| "kubevirt.io/kubevirt/pkg/api/v1" | ||
| "kubevirt.io/kubevirt/pkg/log" | ||
| "kubevirt.io/kubevirt/pkg/virt-handler/cmd-client" | ||
| ) | ||
| // PodIsolationDetector helps detecting cgroups, namespaces and PIDs of Pods from outside of them. | ||
| // Different strategies may be applied to do that. | ||
| type PodIsolationDetector interface { | ||
| // Detect takes a vm, looks up a socket based the VM and detects pid, cgroups and namespaces of the owner of that socket. | ||
| // It returns an IsolationResult containing all isolation information | ||
| Detect(vm *v1.VirtualMachineInstance) (*IsolationResult, error) | ||
| // Whitelist allows specifying cgroup controller which should be considered to detect the cgroup slice | ||
| // It returns a PodIsolationDetector to allow configuring the PodIsolationDetector via the builder pattern. | ||
| Whitelist(controller []string) PodIsolationDetector | ||
| } | ||
| type socketBasedIsolationDetector struct { | ||
| socketDir string | ||
| controller []string | ||
| } | ||
| // NewSocketBasedIsolationDetector takes socketDir and creates a socket based IsolationDetector | ||
| // It returns a PodIsolationDetector which detects pid, cgroups and namespaces of the socket owner. | ||
| func NewSocketBasedIsolationDetector(socketDir string) PodIsolationDetector { | ||
| return &socketBasedIsolationDetector{socketDir: socketDir, controller: []string{"devices"}} | ||
| } | ||
| func (s *socketBasedIsolationDetector) Whitelist(controller []string) PodIsolationDetector { | ||
| s.controller = controller | ||
| return s | ||
| } | ||
| func (s *socketBasedIsolationDetector) Detect(vm *v1.VirtualMachineInstance) (*IsolationResult, error) { | ||
| var pid int | ||
| var slice string | ||
| var err error | ||
| var controller []string | ||
| // Look up the socket of the virt-launcher Pod which was created for that VM, and extract the PID from it | ||
| socket := cmdclient.SocketFromUID(s.socketDir, string(vm.UID)) | ||
| if pid, err = s.getPid(socket); err != nil { | ||
| log.Log.Object(vm).Reason(err).Errorf("Could not get owner Pid of socket %s", socket) | ||
| return nil, err | ||
| } | ||
| // Look up the cgroup slice based on the whitelisted controller | ||
| if controller, slice, err = s.getSlice(pid); err != nil { | ||
| log.Log.Object(vm).Reason(err).Errorf("Could not get cgroup slice for Pid %d", pid) | ||
| return nil, err | ||
| } | ||
| return NewIsolationResult(pid, slice, controller), nil | ||
| } | ||
| func NewIsolationResult(pid int, slice string, controller []string) *IsolationResult { | ||
| return &IsolationResult{pid: pid, slice: slice, controller: controller} | ||
| } | ||
| type IsolationResult struct { | ||
| pid int | ||
| slice string | ||
| controller []string | ||
| } | ||
| func (r *IsolationResult) Slice() string { | ||
| return r.slice | ||
| } | ||
| func (r *IsolationResult) PIDNamespace() string { | ||
| return fmt.Sprintf("/proc/%d/ns/pid", r.pid) | ||
| } | ||
| func (r *IsolationResult) NetNamespace() string { | ||
| return fmt.Sprintf("/proc/%d/ns/net", r.pid) | ||
| } | ||
| func (r *IsolationResult) MountRoot() string { | ||
| return fmt.Sprintf("/proc/%d/root", r.pid) | ||
| } | ||
| func (r *IsolationResult) Pid() int { | ||
| return r.pid | ||
| } | ||
| func (r *IsolationResult) Controller() []string { | ||
| return r.controller | ||
| } | ||
| func (s *socketBasedIsolationDetector) getPid(socket string) (int, error) { | ||
| sock, err := net.Dial("unix", socket) | ||
| if err != nil { | ||
| return -1, err | ||
| } | ||
| defer sock.Close() | ||
| ufile, err := sock.(*net.UnixConn).File() | ||
| if err != nil { | ||
| return -1, err | ||
| } | ||
| // This is the tricky part, which will give us the PID of the owning socket | ||
| ucreds, err := syscall.GetsockoptUcred(int(ufile.Fd()), syscall.SOL_SOCKET, syscall.SO_PEERCRED) | ||
| if err != nil { | ||
| return -1, err | ||
| } | ||
| if int(ucreds.Pid) == 0 { | ||
| return -1, fmt.Errorf("The detected PID is 0. Is the isolation detector running in the host PID namespace?") | ||
| } | ||
| return int(ucreds.Pid), nil | ||
| } | ||
| func (s *socketBasedIsolationDetector) getSlice(pid int) (controller []string, slice string, err error) { | ||
| cgroups, err := os.Open(fmt.Sprintf("/proc/%d/cgroup", pid)) | ||
| if err != nil { | ||
| return | ||
| } | ||
| defer cgroups.Close() | ||
| scanner := bufio.NewScanner(cgroups) | ||
| for scanner.Scan() { | ||
| cgEntry := strings.Split(scanner.Text(), ":") | ||
| // Check if we have a sane cgroup line | ||
| if len(cgEntry) != 3 { | ||
| err = fmt.Errorf("Could not extract slice from cgroup line: %s", scanner.Text()) | ||
| return | ||
| } | ||
| // Skip not supported cgroup controller | ||
| if !sliceContains(s.controller, cgEntry[1]) { | ||
| continue | ||
| } | ||
| // Set and check cgroup slice | ||
| if slice == "" { | ||
| slice = cgEntry[2] | ||
| } else if slice != cgEntry[2] { | ||
| err = fmt.Errorf("Process is part of more than one slice. Expected %s, found %s", slice, cgEntry[2]) | ||
| return | ||
| } | ||
| // Add controller | ||
| controller = append(controller, cgEntry[1]) | ||
| } | ||
| // Check if we encountered a read error | ||
| if scanner.Err() != nil { | ||
| err = scanner.Err() | ||
| return | ||
| } | ||
| if slice == "" { | ||
| err = fmt.Errorf("Could not detect slice of whitelisted controller: %v", s.controller) | ||
| return | ||
| } | ||
| return | ||
| } | ||
| func sliceContains(controllers []string, value string) bool { | ||
| for _, c := range controllers { | ||
| if c == value { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| func NodeIsolationResult() *IsolationResult { | ||
| return &IsolationResult{ | ||
| pid: 1, | ||
| } | ||
| } |
| @@ -0,0 +1,16 @@ | ||
| package isolation | ||
| import ( | ||
| . "github.com/onsi/ginkgo" | ||
| . "github.com/onsi/gomega" | ||
| "testing" | ||
| "kubevirt.io/kubevirt/pkg/log" | ||
| ) | ||
| func TestIsolation(t *testing.T) { | ||
| RegisterFailHandler(Fail) | ||
| log.Log.SetIOWriter(GinkgoWriter) | ||
| RunSpecs(t, "Isolation Suite") | ||
| } |
| @@ -0,0 +1,75 @@ | ||
| package isolation | ||
| import ( | ||
| "fmt" | ||
| "io/ioutil" | ||
| "net" | ||
| "os" | ||
| . "github.com/onsi/ginkgo" | ||
| . "github.com/onsi/gomega" | ||
| "kubevirt.io/kubevirt/pkg/api/v1" | ||
| "kubevirt.io/kubevirt/pkg/virt-handler/cmd-client" | ||
| ) | ||
| var _ = Describe("Isolation", func() { | ||
| Context("With an existing socket", func() { | ||
| var socket net.Listener | ||
| var tmpDir string | ||
| vm := v1.NewMinimalVMIWithNS("default", "testvm") | ||
| BeforeEach(func() { | ||
| var err error | ||
| tmpDir, err = ioutil.TempDir("", "kubevirt") | ||
| os.MkdirAll(tmpDir+"/sockets/", os.ModePerm) | ||
| socket, err = net.Listen("unix", cmdclient.SocketFromUID( | ||
| tmpDir, | ||
| string(vm.UID)), | ||
| ) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| }) | ||
| It("Should detect the PID of the test suite", func() { | ||
| result, err := NewSocketBasedIsolationDetector(tmpDir).Whitelist([]string{"devices"}).Detect(vm) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(result.Pid()).To(Equal(os.Getpid())) | ||
| }) | ||
| It("Should not detect any slice if there is no matching controller", func() { | ||
| _, err := NewSocketBasedIsolationDetector(tmpDir).Whitelist([]string{"not_existing_slice"}).Detect(vm) | ||
| Expect(err).To(HaveOccurred()) | ||
| }) | ||
| It("Should detect the 'devices' controller slice of the test suite", func() { | ||
| result, err := NewSocketBasedIsolationDetector(tmpDir).Whitelist([]string{"devices"}).Detect(vm) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(result.Slice()).To(HavePrefix("/")) | ||
| }) | ||
| It("Should detect the PID namespace of the test suite", func() { | ||
| result, err := NewSocketBasedIsolationDetector(tmpDir).Whitelist([]string{"devices"}).Detect(vm) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(result.PIDNamespace()).To(Equal(fmt.Sprintf("/proc/%d/ns/pid", os.Getpid()))) | ||
| }) | ||
| It("Should detect the Mount root of the test suite", func() { | ||
| result, err := NewSocketBasedIsolationDetector(tmpDir).Whitelist([]string{"devices"}).Detect(vm) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(result.MountRoot()).To(Equal(fmt.Sprintf("/proc/%d/root", os.Getpid()))) | ||
| }) | ||
| It("Should detect the Network namespace of the test suite", func() { | ||
| result, err := NewSocketBasedIsolationDetector(tmpDir).Whitelist([]string{"devices"}).Detect(vm) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| Expect(result.NetNamespace()).To(Equal(fmt.Sprintf("/proc/%d/ns/net", os.Getpid()))) | ||
| }) | ||
| AfterEach(func() { | ||
| socket.Close() | ||
| os.RemoveAll(tmpDir) | ||
| }) | ||
| }) | ||
| }) |
| @@ -0,0 +1,224 @@ | ||
| // Copyright 2015 CNI authors | ||
| // Copyright 2017 rmohr@redhat.com | ||
| // Copyright 2018 KubeVirt 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. | ||
| // GO version 1.10 or greater is required. Before that, switching namespaces in | ||
| // long running processes in go did not work in a reliable way. | ||
| // +build go1.10 | ||
| package ns | ||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "regexp" | ||
| "runtime" | ||
| "sync" | ||
| "syscall" | ||
| "golang.org/x/sys/unix" | ||
| ) | ||
| type Namespace interface { | ||
| // Executes the passed closure in this object's namespace, | ||
| // attempting to restore the original namespace before returning. | ||
| // No code called from Do() should create go-routines, or the risk | ||
| // of executing code in an incorrect namespace exists. | ||
| Do(toRun func(Namespace) error) error | ||
| // Sets the current namespace to this object's namespace. | ||
| Set() error | ||
| // Returns the filesystem path representing this object's namespace | ||
| Path() string | ||
| // Returns a file descriptor representing this object's namespace | ||
| Fd() uintptr | ||
| // Cleans up this instance of the namespace; if this instance | ||
| // is the last user the namespace will be destroyed | ||
| Close() error | ||
| } | ||
| type namespace struct { | ||
| file *os.File | ||
| closed bool | ||
| nsType int | ||
| nsTypeStr string | ||
| } | ||
| // Returns an object representing the namespace referred to by @path | ||
| func GetNS(nspath string) (Namespace, error) { | ||
| err := IsNSorErr(nspath) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| //cgroup ipc mnt net pid user uts | ||
| res := regexp.MustCompile(`^/proc/.+/ns/([^/]+)`).FindStringSubmatch(nspath) | ||
| if len(res) != 2 { | ||
| return nil, fmt.Errorf("Error detecting namespace type from path: %s", nspath) | ||
| } | ||
| var nsType int | ||
| switch res[1] { | ||
| case "pid": | ||
| nsType = unix.CLONE_NEWPID | ||
| case "net": | ||
| nsType = unix.CLONE_NEWNET | ||
| default: | ||
| return nil, fmt.Errorf("Error unsupported namespace requested: %s", res[1]) | ||
| } | ||
| fd, err := os.Open(nspath) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &namespace{file: fd, nsType: nsType, nsTypeStr: res[1]}, nil | ||
| } | ||
| func (ns *namespace) Set() error { | ||
| if err := ns.errorIfClosed(); err != nil { | ||
| return err | ||
| } | ||
| if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(ns.nsType), 0); err != 0 { | ||
| return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) | ||
| } | ||
| return nil | ||
| } | ||
| func (ns *namespace) Close() error { | ||
| if err := ns.errorIfClosed(); err != nil { | ||
| return err | ||
| } | ||
| if err := ns.file.Close(); err != nil { | ||
| return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err) | ||
| } | ||
| ns.closed = true | ||
| return nil | ||
| } | ||
| func (ns *namespace) Path() string { | ||
| return ns.file.Name() | ||
| } | ||
| func (ns *namespace) Fd() uintptr { | ||
| return ns.file.Fd() | ||
| } | ||
| func (ns *namespace) errorIfClosed() error { | ||
| if ns.closed { | ||
| return fmt.Errorf("%q has already been closed", ns.file.Name()) | ||
| } | ||
| return nil | ||
| } | ||
| func (ns *namespace) Do(toRun func(Namespace) error) error { | ||
| if err := ns.errorIfClosed(); err != nil { | ||
| return err | ||
| } | ||
| containedCall := func(hostNS Namespace) error { | ||
| threadNS, err := GetNS(ns.getCurrentThreadNSPath()) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to open current netns: %v", err) | ||
| } | ||
| defer threadNS.Close() | ||
| // switch to target namespace | ||
| if err = ns.Set(); err != nil { | ||
| return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err) | ||
| } | ||
| defer threadNS.Set() // switch back | ||
| return toRun(hostNS) | ||
| } | ||
| // save a handle to current network namespace | ||
| hostNS, err := GetNS(ns.getCurrentThreadNSPath()) | ||
| if err != nil { | ||
| return fmt.Errorf("Failed to open current namespace: %v", err) | ||
| } | ||
| defer hostNS.Close() | ||
| var wg sync.WaitGroup | ||
| wg.Add(1) | ||
| var innerError error | ||
| go func() { | ||
| defer wg.Done() | ||
| runtime.LockOSThread() | ||
| innerError = containedCall(hostNS) | ||
| }() | ||
| wg.Wait() | ||
| return innerError | ||
| } | ||
| func (ns *namespace) getCurrentThreadNSPath() string { | ||
| // /proc/self/ns/net returns the namespace of the main thread, not | ||
| // of whatever thread this goroutine is running on. Make sure we | ||
| // use the thread's net namespace since the thread is switching around | ||
| return fmt.Sprintf("/proc/%d/task/%d/ns/%s", os.Getpid(), unix.Gettid(), ns.nsTypeStr) | ||
| } | ||
| // WithNSPath executes the passed closure under the given | ||
| // namespace, restoring the original namespace afterwards. | ||
| func WithNSPath(nspath string, toRun func(Namespace) error) error { | ||
| ns, err := GetNS(nspath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer ns.Close() | ||
| return ns.Do(toRun) | ||
| } | ||
| const ( | ||
| // https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h | ||
| NSFS_MAGIC = 0x6e736673 | ||
| PROCFS_MAGIC = 0x9fa0 | ||
| ) | ||
| type NSPathNotExistErr struct{ msg string } | ||
| func (e NSPathNotExistErr) Error() string { return e.msg } | ||
| type NSPathNotNSErr struct{ msg string } | ||
| func (e NSPathNotNSErr) Error() string { return e.msg } | ||
| func IsNSorErr(nspath string) error { | ||
| stat := syscall.Statfs_t{} | ||
| if err := syscall.Statfs(nspath, &stat); err != nil { | ||
| if os.IsNotExist(err) { | ||
| err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)} | ||
| } else { | ||
| err = fmt.Errorf("failed to Statfs %q: %v", nspath, err) | ||
| } | ||
| return err | ||
| } | ||
| switch stat.Type { | ||
| case PROCFS_MAGIC, NSFS_MAGIC: | ||
| return nil | ||
| default: | ||
| return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)} | ||
| } | ||
| } |