blob: 7354151b53aeacfa4a9cb2043513fe8a43a51682 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package wifi
import (
"context"
"fmt"
"io/ioutil"
"net"
"path/filepath"
"sort"
"strings"
"time"
"chromiumos/tast/common/testexec"
upstartcommon "chromiumos/tast/common/upstart"
"chromiumos/tast/errors"
"chromiumos/tast/local/bundles/cros/wifi/stringset"
"chromiumos/tast/local/shill"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
func init() {
testing.AddTest(&testing.Test{
Func: UdevRename,
Desc: "Verifies that network interfaces remain intact after udev restart and WiFi driver rebind",
Contacts: []string{
"chromeos-wifi-champs@google.com", // WiFi oncall rotation; or http://b/new?component=893827
},
Attr: []string{"group:mainline"},
// TODO(b/149247291): remove the elm/hana 3.18 dependency once elm/hana upreved kernel to 4.19 or above.
SoftwareDeps: []string{"wifi", "shill-wifi", "no_elm_hana_3_18"},
// TODO(b/183992356): remove informational variant once Asurada unbind/bind issues are fixed.
Params: []testing.Param{
{
ExtraHardwareDeps: hwdep.D(hwdep.SkipOnPlatform("asurada")),
},
{
Name: "informational",
ExtraAttr: []string{"informational"},
ExtraHardwareDeps: hwdep.D(hwdep.Platform("asurada")),
},
},
})
}
func restartWifiInterface(ctx context.Context) error {
manager, err := shill.NewManager(ctx)
if err != nil {
return errors.Wrap(err, "failed creating shill manager proxy")
}
iface, err := shill.WifiInterface(ctx, manager, 5*time.Second)
if err != nil {
return errors.Wrap(err, "could not find interface")
}
devicePath := fmt.Sprintf("/sys/class/net/%s/device", iface)
deviceRealPath, err := filepath.EvalSymlinks(devicePath)
if err != nil {
return errors.Wrapf(err, "could not evaluate symlink on payload %s", devicePath)
}
// The driver path is the directory where we can bind and release the device.
driverPath := filepath.Join(devicePath, "driver")
driverRealPath, err := filepath.EvalSymlinks(driverPath)
if err != nil {
return errors.Wrapf(err, "could not evaluate symlink on path %s", driverPath)
}
// Function to find device paths for the brcmfmac (Broadcom FullMAC) driver.
// In general, one device is associated with a driver. However, for brcmfmac driver,
// it associates with two devices. We have to unbind/bind both.
brcmfmacDevicePaths := func(driverPath string) ([]string, error) {
paths, err := filepath.Glob(filepath.Join(driverPath, "*"))
if err != nil {
return nil, err
}
if len(paths) <= 1 {
return nil, errors.Errorf("found %d brcmfmac driver devices, expected at least 2", len(paths))
}
var ret []string
for _, p := range paths {
// Only consider links to devices, and not paths like '/sys/bus/.../unbind'.
if rp, err := filepath.EvalSymlinks(p); err == nil && strings.HasPrefix(rp, "/sys/devices") {
ret = append(ret, rp)
}
}
return ret, nil
}
devPaths := []string{deviceRealPath}
// Special case for brcmfmac (Broadcom FullMAC) driver.
// Note that in older kernels, e.g. 3.14, the driver name of Broadcom FullMAC is "brcmfmac_sdio";
// however, in recent kernels, e.g. 4.19, it is named as "brcmfmac". So we use prefix match here.
if strings.HasPrefix(filepath.Base(driverRealPath), "brcmfmac") {
devPaths, err = brcmfmacDevicePaths(driverPath)
if err != nil {
errors.Wrap(err, "brcmfmac device paths error")
}
testing.ContextLog(ctx, "Devices associated with brcmfmac driver: ", devPaths)
}
for _, devPath := range devPaths {
testing.ContextLogf(ctx, "Rebind device %s to driver %s", devPath, driverRealPath)
devName := filepath.Base(devPath)
if err := ioutil.WriteFile(filepath.Join(driverRealPath, "unbind"), []byte(devName), 0200); err != nil {
return errors.Wrapf(err, "could not unbind %s driver", iface)
}
if err := ioutil.WriteFile(filepath.Join(driverRealPath, "bind"), []byte(devName), 0200); err != nil {
return errors.Wrapf(err, "could not bind %s driver", iface)
}
}
return nil
}
func restartUdev(ctx context.Context) error {
const service = "udev"
if _, state, _, err := upstart.JobStatus(ctx, service); err != nil {
return errors.Wrapf(err, "could not query status of service %s", service)
} else if state != upstartcommon.RunningState {
return errors.Errorf("%s not running", service)
}
if err := upstart.RestartJob(ctx, service); err != nil {
return errors.Errorf("%s failed to restart", service)
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := testexec.CommandContext(ctx, "udevadm", "control", "--ping").Run(); err != nil {
return errors.Wrap(err, "udev is not yet running")
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return err
}
return nil
}
// deviceRestarter is a function type that defines a first class function that would restart
// a device or series of devices. restartUdev() and restartWifiInterface() match the
// function prototype.
type deviceRestarter func(ctx context.Context) error
func interfaceNames() ([]string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
// Sanitize iface names to exclude ARC and Crostini related interfaces.
// Context: b/191789332
var names []string
for _, iface := range ifaces {
name := iface.Name
if !(strings.HasPrefix(name, "arc") || strings.HasPrefix(name, "vmtap")) {
names = append(names, name)
}
}
sort.Strings(names)
return names, nil
}
// expectIface expects actual interfaces is the same as expected.
func expectIface(expect, actual []string) error {
es := stringset.New(expect)
as := stringset.New(actual)
if es.Equal(as) {
return nil
}
var errs []string
// wanted: interfaces in expect not in actual.
if wanted := es.Diff(as); len(wanted) > 0 {
errs = append(errs, fmt.Sprintf("wanted:%v", wanted.Elements()))
}
// unexpected: interfaces in actual not in expect.
if unexpected := as.Diff(es); len(unexpected) > 0 {
errs = append(errs, fmt.Sprintf("unexpected:%v", unexpected.Elements()))
}
// matched: interfaces in both actual and expect.
if matched := es.Intersect(as); len(matched) > 0 {
errs = append(errs, fmt.Sprintf("matched:%v", matched.Elements()))
}
return errors.New("failed expecting network interfaces: " + strings.Join(errs, ", "))
}
func testUdevDeviceList(ctx context.Context, fn deviceRestarter) error {
iflistPre, err := interfaceNames()
if err != nil {
return err
}
if err := fn(ctx); err != nil {
return err
}
// Wait for event processing.
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(5*time.Second))
defer cancel()
if err := testexec.CommandContext(timeoutCtx, "udevadm", "settle").Run(testexec.DumpLogOnError); err != nil {
return errors.Wrap(err, "device could not settle in time after restart")
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
iflistPost, err := interfaceNames()
if err != nil {
return err
}
if err := expectIface(iflistPre, iflistPost); err != nil {
return err
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return err
}
return nil
}
func UdevRename(ctx context.Context, s *testing.State) {
if err := testUdevDeviceList(ctx, restartUdev); err != nil {
s.Error("Restarting udev: ", err)
}
if err := testUdevDeviceList(ctx, restartWifiInterface); err != nil {
s.Error("Restarting wireless interface: ", err)
}
}