Files
my-rook-config/pkg/daemon/ceph/osd/agent_test.go
T

560 lines
21 KiB
Go

/*
Copyright 2016 The Rook Authors. All rights reserved.
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 osd
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
rookalpha "github.com/rook/rook/pkg/apis/rook.io/v1alpha1"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/mon"
"github.com/rook/rook/pkg/daemon/ceph/test"
"github.com/rook/rook/pkg/operator/k8sutil"
testop "github.com/rook/rook/pkg/operator/test"
exectest "github.com/rook/rook/pkg/util/exec/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestOSDAgentWithDevicesFilestore(t *testing.T) {
testOSDAgentWithDevicesHelper(t, rookalpha.StoreConfig{StoreType: Filestore})
}
func TestOSDAgentWithDevicesBluestore(t *testing.T) {
testOSDAgentWithDevicesHelper(t, rookalpha.StoreConfig{StoreType: Bluestore})
}
func testOSDAgentWithDevicesHelper(t *testing.T, storeConfig rookalpha.StoreConfig) {
// set up a temporary config directory that will be cleaned up after test
configDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("failed to create temp config dir: %+v", err)
}
defer os.RemoveAll(configDir)
agent, executor := createTestAgent(t, "sdx,sdy", configDir, &storeConfig)
startCount := 0
executor.MockStartExecuteCommand = func(debug bool, name string, command string, args ...string) (*exec.Cmd, error) {
logger.Infof("START %d for %s. %s %+v", startCount, name, command, args)
cmd := &exec.Cmd{Args: append([]string{command}, args...)}
switch {
case startCount < 2:
assert.Equal(t, "ceph-osd", command)
default:
assert.Fail(t, fmt.Sprintf("unexpected case %d", startCount))
}
startCount++
return cmd, nil
}
execCount := 0
executor.MockExecuteCommand = func(debug bool, name string, command string, args ...string) error {
logger.Infof("RUN %d: %s %+v", execCount, command, args)
parts := strings.Split(name, " ")
nameSuffix := parts[0]
if len(parts) > 1 {
nameSuffix = parts[1]
}
if storeConfig.StoreType == Bluestore {
switch {
case execCount == 0: // first exec is the osd mkfs for sdx
assert.Equal(t, "--mkfs", args[0])
createTestKeyring(t, configDir, args)
case execCount == 1: // all remaining execs are for partitioning sdy then mkfs sdy
assert.Equal(t, "sgdisk", command)
assert.Equal(t, "--zap-all", args[0])
assert.Equal(t, "/dev/"+nameSuffix, args[1])
case execCount == 2:
assert.Equal(t, "sgdisk", command)
assert.Equal(t, "--clear", args[0])
assert.Equal(t, "/dev/"+nameSuffix, args[2])
case execCount == 3:
// the partitioning for sdy.
assert.Equal(t, "sgdisk", command)
assert.Equal(t, "/dev/"+nameSuffix, args[10])
case execCount == 4:
// the osd mkfs for sdy bluestore
assert.Equal(t, "--mkfs", args[0])
createTestKeyring(t, configDir, args)
default:
assert.Fail(t, fmt.Sprintf("unexpected case %d", execCount))
}
} else if storeConfig.StoreType == Filestore {
switch {
case execCount == 0:
// first exec is the remounting of sdx because its partitions were created previously, we just need to remount it
// note this only happens for filestore (not bluestore)
assert.Equal(t, "mount", command)
case execCount == 1:
// the osd mkfs for sdx
assert.Equal(t, "--mkfs", args[0])
createTestKeyring(t, configDir, args)
case execCount == 2: // all remaining execs are for partitioning sdy then mkfs sdy
assert.Equal(t, "sgdisk", command)
assert.Equal(t, "--zap-all", args[0])
assert.Equal(t, "/dev/"+nameSuffix, args[1])
case execCount == 3:
assert.Equal(t, "sgdisk", command)
assert.Equal(t, "--clear", args[0])
assert.Equal(t, "/dev/"+nameSuffix, args[2])
case execCount == 4:
// the partitioning for sdy.
assert.Equal(t, "sgdisk", command)
assert.Equal(t, "/dev/"+nameSuffix, args[4])
case execCount == 5:
// mkfs.ext4 for sdy filestore
assert.Equal(t, "mkfs.ext4", command)
case execCount == 6:
// the mount for sdy filestore
assert.Equal(t, "mount", command)
case execCount == 7:
// the osd mkfs for sdy filestore
assert.Equal(t, "--mkfs", args[0])
createTestKeyring(t, configDir, args)
default:
assert.Fail(t, fmt.Sprintf("unexpected case %d", execCount))
}
}
execCount++
return nil
}
outputExecCount := 0
executor.MockExecuteCommandWithOutputFile = func(debug bool, actionName string, command string, outFileArg string, args ...string) (string, error) {
logger.Infof("OUTPUT %d for %s. %s %+v", outputExecCount, actionName, command, args)
outputExecCount++
if args[0] == "auth" && args[1] == "get-or-create-key" {
return "{\"key\":\"mysecurekey\"}", nil
}
if args[0] == "osd" && args[1] == "create" {
return "{\"osdid\":3.0}", nil
}
return "", nil
}
executor.MockExecuteCommandWithOutput = func(debug bool, actionName string, command string, args ...string) (string, error) {
logger.Infof("OUTPUT %d for %s. %s %+v", outputExecCount, actionName, command, args)
outputExecCount++
if strings.HasPrefix(actionName, "lsblk /dev/disk/by-partuuid") {
// this is a call to get device properties so we figure out CRUSH weight, which should only be done for Bluestore
// (Filestore uses Statfs since it has a mounted filesystem)
assert.Equal(t, Bluestore, storeConfig.StoreType)
return `SIZE="1234567890" TYPE="part"`, nil
}
return "", nil
}
context := &clusterd.Context{
Executor: executor,
ConfigDir: configDir,
}
// Set sdx as already having an assigned osd id, a UUID and saved to the partition scheme.
// The other device (sdy) will go through id selection, which is mocked in the createTestAgent method to return an id of 3.
_, sdxUUID := mockPartitionSchemeEntry(t, 23, "sdx", &storeConfig, agent.kv, agent.nodeName)
// note only sdx already has a UUID (it's been through partitioning)
context.Devices = []*clusterd.LocalDisk{
{Name: "sdx", Size: 1234567890, UUID: sdxUUID},
{Name: "sdy", Size: 1234567890},
}
devices := &DeviceOsdMapping{Entries: map[string]*DeviceOsdIDEntry{
"sdx": {Data: -1},
"sdy": {Data: -1},
}}
err = agent.configureDevices(context, devices)
assert.Nil(t, err)
assert.Equal(t, int32(0), agent.configCounter)
assert.Equal(t, 2, startCount) // 2 OSD procs should be started
assert.Equal(t, 2, len(agent.osdProc), fmt.Sprintf("procs=%+v", agent.osdProc))
if storeConfig.StoreType == Bluestore {
assert.Equal(t, 11, outputExecCount) // Bluestore has 2 extra output exec calls to get device properties of each device to determine CRUSH weight
assert.Equal(t, 5, execCount) // 1 osd mkfs for sdx, 3 partition steps for sdy, 1 osd mkfs for sdy
} else {
assert.Equal(t, 9, outputExecCount)
assert.Equal(t, 8, execCount) // 1 for remount sdx, 1 osd mkfs for sdx, 3 partition steps for sdy, 1 mkfs for sdy, 1 mount for sdy, 1 osd mkfs for sdy
}
}
func TestOSDAgentNoDevices(t *testing.T) {
// set up a temporary config directory that will be cleaned up after test
configDir, err := ioutil.TempDir("", "TestOSDAgentNoDevices")
require.NoError(t, err)
defer os.RemoveAll(configDir)
os.MkdirAll(filepath.Join(configDir, "osd3"), 0744)
// create a test OSD agent with no devices specified
agent, executor := createTestAgent(t, "", configDir, nil)
startCount := 0
executor.MockStartExecuteCommand = func(debug bool, name string, command string, args ...string) (*exec.Cmd, error) {
logger.Infof("StartExecuteCommand: %s %v", command, args)
startCount++
cmd := &exec.Cmd{Args: append([]string{command}, args...)}
return cmd, nil
}
runCount := 0
executor.MockExecuteCommand = func(debug bool, name string, command string, args ...string) error {
logger.Infof("ExecuteCommand: %s %v", command, args)
runCount++
createTestKeyring(t, configDir, args)
return nil
}
execWithOutputFileCount := 0
executor.MockExecuteCommandWithOutputFile = func(debug bool, actionName string, command string, outFileArg string, args ...string) (string, error) {
logger.Infof("ExecuteCommandWithOutputFile: %s %v", command, args)
execWithOutputFileCount++
return "{\"key\":\"mysecurekey\", \"osdid\":3.0}", nil
}
execWithOutputCount := 0
executor.MockExecuteCommandWithOutput = func(debug bool, actionName string, command string, arg ...string) (string, error) {
logger.Infof("ExecuteCommandWithOutput: %s %v", command, arg)
execWithOutputCount++
return "", nil
}
// set up expected ProcManager commands
context := &clusterd.Context{
Devices: []*clusterd.LocalDisk{},
Executor: executor,
ConfigDir: configDir,
}
dirs := map[string]int{
filepath.Join(configDir, "sdx"): -1,
filepath.Join(configDir, "sdy"): -1,
}
err = agent.configureDirs(context, dirs)
assert.Nil(t, err)
assert.Equal(t, 2, runCount)
assert.Equal(t, 2, startCount)
assert.Equal(t, 6, execWithOutputFileCount)
assert.Equal(t, 2, execWithOutputCount)
assert.Equal(t, 1, len(agent.osdProc))
}
func createTestAgent(t *testing.T, devices, configDir string, storeConfig *rookalpha.StoreConfig) (*OsdAgent, *exectest.MockExecutor) {
location := "root=here"
forceFormat := false
if storeConfig == nil {
storeConfig = &rookalpha.StoreConfig{StoreType: Bluestore}
}
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutputFile: func(debug bool, actionName string, command string, outFileArg string, args ...string) (string, error) {
return "{\"key\":\"mysecurekey\", \"osdid\":3.0}", nil
},
}
cluster := &mon.ClusterInfo{Name: "myclust"}
agent := NewAgent(
&clusterd.Context{Executor: executor, Clientset: testop.New(1)},
devices, false, "", "", forceFormat, location, *storeConfig, cluster, "myhost", mockKVStore())
return agent, executor
}
func createTestKeyring(t *testing.T, configRoot string, args []string) {
var configDir string
if len(args) > 5 && strings.HasPrefix(args[5], "--id") {
configDir = filepath.Join(configRoot, "osd") + args[5][5:]
err := os.MkdirAll(configDir, 0744)
assert.Nil(t, err)
err = ioutil.WriteFile(path.Join(configDir, "keyring"), []byte("mykeyring"), 0644)
assert.Nil(t, err)
}
}
func TestGetPartitionPerfScheme(t *testing.T) {
configDir, _ := ioutil.TempDir("", "")
defer os.RemoveAll(configDir)
context := &clusterd.Context{Devices: []*clusterd.LocalDisk{}, ConfigDir: configDir}
test.CreateConfigDir(configDir)
// 3 disks: 2 for data and 1 for the metadata of both disks (2 WALs and 2 DBs)
a := &OsdAgent{desiredDevices: []string{"sda", "sdb"}, metadataDevice: "sdc", kv: mockKVStore(), nodeName: "a"}
context.Devices = []*clusterd.LocalDisk{
{Name: "sda", Size: 107374182400}, // 100 GB
{Name: "sdb", Size: 107374182400}, // 100 GB
{Name: "sdc", Size: 44158681088}, // 1 MB (starting offset) + 2 * (576 MB + 20 GB) = 41.125 GB
}
clusterInfo := &mon.ClusterInfo{Name: "myclust"}
a.cluster = clusterInfo
// mock monitor command to return an osd ID when the client registers/creates an osd
currOsdID := 10
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutputFile: func(debug bool, actionName string, command string, outFileArg string, args ...string) (string, error) {
switch {
case args[0] == "osd" && args[1] == "create":
currOsdID++
return fmt.Sprintf(`{"osdid": %d}`, currOsdID), nil
}
return "", fmt.Errorf("unexpected command '%v'", args)
},
MockExecuteCommandWithOutput: func(debug bool, actionName string, command string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if command == "lsblk" {
if args[0] == "/dev/sda" {
return `NAME="sda" SIZE="107374182400" TYPE="disk" PKNAME=""`, nil
}
if args[0] == "/dev/sdb" {
return `NAME="sdb" SIZE="107374182400" TYPE="disk" PKNAME=""`, nil
}
if args[0] == "/dev/sdc" {
return `NAME="sdc" SIZE="44158681088" TYPE="disk" PKNAME=""`, nil
}
}
if command == "blkid" {
return "", nil
}
if command == "df" {
return "", nil
}
return "", fmt.Errorf("unexpected command %s %v", command, args)
},
}
context.Executor = executor
devices, err := getAvailableDevices(context, "sda,sdb", "sdc", false)
assert.Nil(t, err)
scheme, err := a.getPartitionPerfScheme(context, devices)
assert.Nil(t, err)
assert.Equal(t, 2, len(scheme.Entries))
// verify the metadata entries, they should be on sdc and there should be 2 of them (2 per OSD)
require.NotNil(t, scheme.Metadata)
assert.Equal(t, "sdc", scheme.Metadata.Device)
assert.Equal(t, 4, len(scheme.Metadata.Partitions))
// verify the first entry in the performance partition scheme. note that the block device will either be sda or
// sdb because ordering of map traversal in golang isn't guaranteed. Ensure that the first is either sda or sdb
// and that the second is the other one.
entry := scheme.Entries[0]
assert.Equal(t, 11, entry.ID)
firstBlockDevice := entry.Partitions[BlockPartitionType].Device
assert.True(t, firstBlockDevice == "sda" || firstBlockDevice == "sdb", firstBlockDevice)
verifyPartitionEntry(t, entry.Partitions[BlockPartitionType], firstBlockDevice, -1, 1)
verifyPartitionEntry(t, entry.Partitions[WalPartitionType], "sdc", WalDefaultSizeMB, 1)
verifyPartitionEntry(t, entry.Partitions[DatabasePartitionType], "sdc", DBDefaultSizeMB, 577)
// verify the second entry in the scheme. Note the comment above about sda vs. sdb ordering.
entry = scheme.Entries[1]
assert.Equal(t, 12, entry.ID)
var secondBlockDevice string
if firstBlockDevice == "sda" {
secondBlockDevice = "sdb"
} else {
secondBlockDevice = "sda"
}
verifyPartitionEntry(t, entry.Partitions[BlockPartitionType], secondBlockDevice, -1, 1)
verifyPartitionEntry(t, entry.Partitions[WalPartitionType], "sdc", WalDefaultSizeMB, 21057)
verifyPartitionEntry(t, entry.Partitions[DatabasePartitionType], "sdc", DBDefaultSizeMB, 21633)
}
func TestGetPartitionSchemeDiskInUse(t *testing.T) {
configDir, err := ioutil.TempDir("", "TestGetPartitionPerfSchemeDiskInUse")
if err != nil {
t.Fatalf("failed to create temp config dir: %+v", err)
}
defer os.RemoveAll(configDir)
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(debug bool, actionName string, command string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if command == "lsblk" {
if args[0] == "/dev/sda" {
return `NAME="sda" SIZE="20971520000" TYPE="disk" PKNAME=""
NAME="sda1" SIZE="19921895424" TYPE="part" PKNAME="sda"
NAME="sda2" SIZE="1048576000" TYPE="part" PKNAME="sda"`, nil
}
}
if command == "blkid" {
return "", nil
}
if command == "df" {
return "", nil
}
return "", fmt.Errorf("unexpected command %s %v", command, args)
},
}
context := &clusterd.Context{
Devices: []*clusterd.LocalDisk{},
ConfigDir: configDir,
Executor: executor,
}
a := &OsdAgent{desiredDevices: []string{"sda"}, kv: mockKVStore()}
_, sdaUUID := mockPartitionSchemeEntry(t, 1, "sda", nil, a.kv, a.nodeName)
context.Devices = []*clusterd.LocalDisk{
{Name: "sda", Size: 107374182400, UUID: sdaUUID}, // 100 GB
}
// get the partition scheme based on the available devices. Since sda is already in use, the partition
// scheme returned should reflect that.
devices, err := getAvailableDevices(context, "sda", "", false)
scheme, err := a.getPartitionPerfScheme(context, devices)
assert.Nil(t, err)
// the partition scheme should have a single entry for osd 1 on sda and it should have collocated data and metadata
assert.NotNil(t, scheme)
assert.Equal(t, 1, len(scheme.Entries))
assert.Equal(t, 1, scheme.Entries[0].ID)
assert.Equal(t, 3, len(scheme.Entries[0].Partitions))
for _, p := range scheme.Entries[0].Partitions {
assert.Equal(t, "sda", p.Device)
assert.Equal(t, sdaUUID, p.DiskUUID)
}
// there should be no dedicated metadata partitioning because sda has osd 1 collocated on it
assert.Nil(t, scheme.Metadata)
}
func TestGetPartitionSchemeDiskNameChanged(t *testing.T) {
configDir, err := ioutil.TempDir("", "TestGetPartitionPerfSchemeDiskNameChanged")
if err != nil {
t.Fatalf("failed to create temp config dir: %+v", err)
}
defer os.RemoveAll(configDir)
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(debug bool, actionName string, command string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if command == "lsblk" {
if args[0] == "/dev/sda-changed" {
return `NAME="sda" SIZE="20971520000" TYPE="disk" PKNAME=""
NAME="sda1" SIZE="19921895424" TYPE="part" PKNAME="sda"
NAME="sda2" SIZE="1048576000" TYPE="part" PKNAME="sda"`, nil
}
if args[0] == "/dev/nvme01-changed" {
return `NAME="nvme01-changed" SIZE="20971520000" TYPE="disk" PKNAME=""
NAME="nvme01-changed1" SIZE="19921895424" TYPE="part" PKNAME="nvme01-changed"
NAME="nvme01-changed2" SIZE="1048576000" TYPE="part" PKNAME="nvme01-changed"`, nil
}
}
if command == "blkid" {
return "", nil
}
if command == "df" {
return "", nil
}
return "", fmt.Errorf("unexpected command %s %v", command, args)
},
}
context := &clusterd.Context{
Devices: []*clusterd.LocalDisk{},
ConfigDir: configDir,
Executor: executor,
}
// mock the currently discovered hardware, note the device names have changed (e.g., across reboots) but their UUIDs are always static
a := &OsdAgent{desiredDevices: []string{"sda-changed"}, kv: mockKVStore()}
// setup an existing partition schme with metadata on nvme01 and data on sda
_, metadataUUID, sdaUUID := mockDistributedPartitionScheme(t, 1, "nvme01", "sda", a.kv, a.nodeName)
context.Devices = []*clusterd.LocalDisk{
{Name: "nvme01-changed", Size: 107374182400, UUID: metadataUUID},
{Name: "sda-changed", Size: 107374182400, UUID: sdaUUID},
}
// get the current partition scheme. This should notice that the device names changed and update the
// partition scheme to have the latest device names
devices, err := getAvailableDevices(context, "sda-changed", "nvme01", false)
scheme, err := a.getPartitionPerfScheme(context, devices)
assert.Nil(t, err)
require.NotNil(t, scheme)
assert.Equal(t, "sda-changed", scheme.Entries[0].Partitions[BlockPartitionType].Device)
assert.Equal(t, "nvme01", scheme.Metadata.Device)
assert.Equal(t, "nvme01", scheme.Entries[0].Partitions[WalPartitionType].Device)
assert.Equal(t, "nvme01", scheme.Entries[0].Partitions[DatabasePartitionType].Device)
}
func verifyPartitionEntry(t *testing.T, actual *PerfSchemePartitionDetails, expectedDevice string,
expectedSize int, expectedOffset int) {
assert.Equal(t, expectedDevice, actual.Device)
assert.Equal(t, expectedSize, actual.SizeMB)
assert.Equal(t, expectedOffset, actual.OffsetMB)
}
func mockPartitionSchemeEntry(t *testing.T, osdID int, device string, storeConfig *rookalpha.StoreConfig,
kv *k8sutil.ConfigMapKVStore, nodeName string) (entry *PerfSchemeEntry, diskUUID string) {
if storeConfig == nil {
storeConfig = &rookalpha.StoreConfig{StoreType: Bluestore}
}
entry = NewPerfSchemeEntry(storeConfig.StoreType)
entry.ID = osdID
entry.OsdUUID = uuid.Must(uuid.NewRandom())
PopulateCollocatedPerfSchemeEntry(entry, device, *storeConfig)
scheme := NewPerfScheme()
scheme.Entries = append(scheme.Entries, entry)
err := scheme.SaveScheme(kv, getConfigStoreName(nodeName))
assert.Nil(t, err)
// figure out what random UUID got assigned to the device
for _, p := range entry.Partitions {
diskUUID = p.DiskUUID
break
}
assert.NotEqual(t, "", diskUUID)
return entry, diskUUID
}
func mockDistributedPartitionScheme(t *testing.T, osdID int, metadataDevice, device string,
kv *k8sutil.ConfigMapKVStore, nodeName string) (*PerfScheme, string, string) {
scheme := NewPerfScheme()
scheme.Metadata = NewMetadataDeviceInfo(metadataDevice)
entry := NewPerfSchemeEntry(Bluestore)
entry.ID = osdID
entry.OsdUUID = uuid.Must(uuid.NewRandom())
PopulateDistributedPerfSchemeEntry(entry, device, scheme.Metadata, rookalpha.StoreConfig{})
scheme.Entries = append(scheme.Entries, entry)
err := scheme.SaveScheme(kv, getConfigStoreName(nodeName))
assert.Nil(t, err)
// return the full partition scheme, the metadata device UUID and the data device UUID
return scheme, scheme.Metadata.DiskUUID, entry.Partitions[BlockPartitionType].DiskUUID
}
func mockKVStore() *k8sutil.ConfigMapKVStore {
clientset := testop.New(1)
return k8sutil.NewConfigMapKVStore("myns", clientset, metav1.OwnerReference{})
}