Files
my-rook-config/pkg/operator/ceph/cluster/osd/osd.go
T
2018-09-04 14:15:18 -05:00

593 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 for the Ceph OSDs.
package osd
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/coreos/pkg/capnslog"
rookalpha "github.com/rook/rook/pkg/apis/rook.io/v1alpha2"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client"
osdconfig "github.com/rook/rook/pkg/operator/ceph/cluster/osd/config"
"github.com/rook/rook/pkg/operator/discover"
"github.com/rook/rook/pkg/operator/k8sutil"
"github.com/rook/rook/pkg/util/display"
batch "k8s.io/api/batch/v1"
"k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/kubelet/apis"
)
var logger = capnslog.NewPackageLogger("github.com/rook/rook", "op-osd")
const (
appName = "rook-ceph-osd"
prepareAppName = "rook-ceph-osd-prepare"
prepareAppNameFmt = "rook-ceph-osd-prepare-%s"
legacyAppNameFmt = "rook-ceph-osd-id-%d"
osdAppNameFmt = "rook-ceph-osd-%d"
osdLabelKey = "ceph-osd-id"
clusterAvailableSpaceReserve = 0.05
defaultServiceAccountName = "rook-ceph-cluster"
unknownID = -1
)
// Cluster keeps track of the OSDs
type Cluster struct {
context *clusterd.Context
Namespace string
placement rookalpha.Placement
Keyring string
Version string
Storage rookalpha.StorageScopeSpec
dataDirHostPath string
HostNetwork bool
resources v1.ResourceRequirements
ownerRef metav1.OwnerReference
serviceAccount string
kv *k8sutil.ConfigMapKVStore
}
// New creates an instance of the OSD manager
func New(
context *clusterd.Context,
namespace,
version,
serviceAccount string,
storageSpec rookalpha.StorageScopeSpec,
dataDirHostPath string,
placement rookalpha.Placement,
hostNetwork bool,
resources v1.ResourceRequirements,
ownerRef metav1.OwnerReference,
) *Cluster {
if serviceAccount == "" {
// if the service account was not set, make a best effort with the example service account name since the default is unlikely to be sufficient.
serviceAccount = defaultServiceAccountName
logger.Infof("setting the osd pods to use the service account name: %s", serviceAccount)
}
return &Cluster{
context: context,
Namespace: namespace,
serviceAccount: serviceAccount,
placement: placement,
Version: version,
Storage: storageSpec,
dataDirHostPath: dataDirHostPath,
HostNetwork: hostNetwork,
resources: resources,
ownerRef: ownerRef,
kv: k8sutil.NewConfigMapKVStore(namespace, context.Clientset, ownerRef),
}
}
type OSDInfo struct {
ID int `json:"id"`
DataPath string `json:"data-path"`
Config string `json:"conf"`
Cluster string `json:"cluster"`
KeyringPath string `json:"keyring-path"`
UUID string `json:"uuid"`
Journal string `json:"journal"`
IsFileStore bool `json:"is-file-store"`
IsDirectory bool `json:"is-directory"`
DevicePartUUID string `json:"device-part-uuid"`
}
type OrchestrationStatus struct {
OSDs []OSDInfo `json:"osds"`
Status string `json:"status"`
Message string `json:"message"`
}
// Start the osd management
func (c *Cluster) Start() error {
logger.Infof("start running osds in namespace %s", c.Namespace)
if c.Storage.UseAllNodes == false && len(c.Storage.Nodes) == 0 {
logger.Warningf("useAllNodes is set to false and no nodes are specified, no OSD pods are going to be created")
}
// disable scrubbing during orchestration and ensure it gets enabled again afterwards
if o, err := client.DisableScrubbing(c.context, c.Namespace); err != nil {
logger.Warningf("failed to disable scrubbing: %+v. %s", err, o)
}
defer func() {
if o, err := client.EnableScrubbing(c.context, c.Namespace); err != nil {
logger.Warningf("failed to enable scrubbing: %+v. %s", err, o)
}
}()
if c.Storage.UseAllNodes {
// resolve all storage nodes
c.Storage.Nodes = nil
rookSystemNS := os.Getenv(k8sutil.PodNamespaceEnvVar)
allNodeDevices, err := discover.ListDevices(c.context, rookSystemNS, "" /* all nodes */)
if err != nil {
logger.Warningf("failed to get storage nodes from namespace %s: %v", rookSystemNS, err)
return err
}
for nodeName := range allNodeDevices {
storageNode := rookalpha.Node{
Name: nodeName,
}
c.Storage.Nodes = append(c.Storage.Nodes, storageNode)
}
logger.Debugf("storage nodes: %+v", c.Storage.Nodes)
}
validNodes := k8sutil.GetValidNodes(c.Storage.Nodes, c.context.Clientset, c.placement)
// no valid node is ready to run an osd
if len(validNodes) == 0 {
logger.Warningf("no valid node available to run an osd in namespace %s", c.Namespace)
return nil
}
logger.Infof("%d of the %d storage nodes are valid", len(validNodes), len(c.Storage.Nodes))
c.Storage.Nodes = validNodes
// orchestrate individual nodes, starting with any that are still ongoing (in the case that we
// are resuming a previous orchestration attempt)
config := newProvisionConfig()
logger.Infof("checking if orchestration is still in progress")
c.completeProvisionSkipOSDStart(config)
// start the jobs to provision the OSD devices and directories
logger.Infof("start provisioning the osds on nodes, if needed")
c.startProvisioning(config)
// start the OSD pods, waiting for the provisioning to be completed
logger.Infof("start osds after provisioning is completed, if needed")
c.completeProvision(config)
// handle the removed nodes and rebalance the PGs
logger.Infof("checking if any nodes were removed")
c.handleRemovedNodes(config)
if len(config.errorMessages) > 0 {
return fmt.Errorf("%d failures encountered while running osds in namespace %s: %+v",
len(config.errorMessages), c.Namespace, strings.Join(config.errorMessages, "\n"))
}
logger.Infof("completed running osds in namespace %s", c.Namespace)
return nil
}
func (c *Cluster) startProvisioning(config *provisionConfig) {
config.devicesToUse = make(map[string][]rookalpha.Device, len(c.Storage.Nodes))
// start with nodes currently in the storage spec
for _, node := range c.Storage.Nodes {
// fully resolve the storage config and resources for this node
n := c.resolveNode(node.Name)
if n == nil {
logger.Warningf("node %s did not resolve", node.Name)
continue
}
if n.Name == "" {
logger.Warningf("skipping node with a blank name! %+v", n)
continue
}
// update the orchestration status of this node to the starting state
status := OrchestrationStatus{Status: OrchestrationStatusStarting}
if err := c.updateNodeStatus(n.Name, status); err != nil {
config.addError("failed to set orchestration starting status for node %s: %+v", n.Name, err)
continue
}
config.devicesToUse[n.Name] = n.Devices
availDev, deviceErr := discover.GetAvailableDevices(c.context, n.Name, c.Namespace, n.Devices, n.Selection.DeviceFilter, n.Selection.GetUseAllDevices())
if deviceErr != nil {
logger.Warningf("failed to get devices for node %s cluster %s: %v", n.Name, c.Namespace, deviceErr)
} else {
config.devicesToUse[n.Name] = availDev
logger.Infof("avail devices for node %s: %+v", n.Name, availDev)
}
if len(availDev) == 0 && len(c.dataDirHostPath) == 0 {
config.addError("empty volumes for node %s", n.Name)
continue
}
// create the job that prepares osds on the node
storeConfig := osdconfig.ToStoreConfig(n.Config)
metadataDevice := osdconfig.MetadataDevice(n.Config)
job, err := c.makeJob(n.Name, config.devicesToUse[n.Name], n.Selection, n.Resources, storeConfig, metadataDevice, n.Location)
if err != nil {
message := fmt.Sprintf("failed to create prepare job node %s: %v", n.Name, err)
config.addError(message)
status := OrchestrationStatus{Status: OrchestrationStatusCompleted, Message: message}
if err := c.updateNodeStatus(n.Name, status); err != nil {
config.addError("failed to update node %s status. %+v", n.Name, err)
continue
}
}
if !c.updateJob(job, n.Name, config, "provision") {
if err = discover.FreeDevices(c.context, n.Name, c.Namespace); err != nil {
logger.Warningf("failed to free devices: %s", err)
}
}
}
}
func (c *Cluster) updateJob(job *batch.Job, nodeName string, config *provisionConfig, action string) bool {
// check if the job was already created and what its status is
existingJob, err := c.context.Clientset.Batch().Jobs(c.Namespace).Get(job.Name, metav1.GetOptions{})
if err != nil && !errors.IsNotFound(err) {
config.addError("failed to detect %s job for node %s. %+v", action, nodeName, err)
} else if err == nil {
// delete the job that already exists from a previous run
if existingJob.Status.Active > 0 {
logger.Infof("Found previous %s job for node %s. Status=%+v", action, nodeName, existingJob.Status)
return true
}
logger.Infof("Removing previous %s job for node %s to start a new one", action, nodeName)
err := c.deleteBatchJob(existingJob.Name)
if err != nil {
logger.Warningf("failed to remove job %s. %+v", nodeName, err)
}
}
_, err = c.context.Clientset.Batch().Jobs(c.Namespace).Create(job)
if err != nil {
if !errors.IsAlreadyExists(err) {
// we failed to create job, update the orchestration status for this node
message := fmt.Sprintf("failed to create %s job for node %s. %+v", action, nodeName, err)
c.handleOrchestrationFailure(config, nodeName, message)
return false
}
// the job is already in progress so we will let it run to completion
}
logger.Infof("osd %s job started for node %s", action, nodeName)
return true
}
func (c *Cluster) deleteBatchJob(name string) error {
propagation := metav1.DeletePropagationForeground
gracePeriod := int64(0)
options := &metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod, PropagationPolicy: &propagation}
if err := c.context.Clientset.Batch().Jobs(c.Namespace).Delete(name, options); err != nil {
return fmt.Errorf("failed to remove previous provisioning job for node %s. %+v", name, err)
}
retries := 20
sleepInterval := 2 * time.Second
for i := 0; i < retries; i++ {
_, err := c.context.Clientset.Batch().Jobs(c.Namespace).Get(name, metav1.GetOptions{})
if err != nil && errors.IsNotFound(err) {
logger.Infof("batch job %s deleted", name)
return nil
}
logger.Infof("batch job %s still exists", name)
time.Sleep(sleepInterval)
}
logger.Warningf("gave up waiting for batch job %s to be deleted", name)
return nil
}
func (c *Cluster) startOSDDaemonsOnNode(nodeName string, config *provisionConfig, configMap *v1.ConfigMap, status *OrchestrationStatus) {
osds := status.OSDs
logger.Infof("starting %d osd daemons on node %s", len(osds), nodeName)
// fully resolve the storage config and resources for this node
n := c.resolveNode(nodeName)
if n == nil {
config.addError("node %s did not resolve to start osds", nodeName)
return
}
storeConfig := osdconfig.ToStoreConfig(n.Config)
metadataDevice := osdconfig.MetadataDevice(n.Config)
// start osds
for _, osd := range osds {
logger.Debugf("start osd %v", osd)
dp, err := c.makeDeployment(n.Name, config.devicesToUse[n.Name], n.Selection, n.Resources, storeConfig, metadataDevice, n.Location, osd)
if err != nil {
errMsg := fmt.Sprintf("nil deployment for node %s: %v", n.Name, err)
config.addError(errMsg)
err = discover.FreeDevices(c.context, n.Name, c.Namespace)
if err != nil {
logger.Warningf("failed to free devices: %s", err)
}
continue
}
if err = c.deleteDeploymentWithLegacyName(osd.ID); err != nil {
logger.Warningf("failed to delete legacy osd deployment. %+v", err)
}
_, err = c.context.Clientset.Extensions().Deployments(c.Namespace).Create(dp)
if err != nil {
if !errors.IsAlreadyExists(err) {
// we failed to create job, update the orchestration status for this node
logger.Warningf("failed to create osd deployment for node %s, osd %v: %+v", n.Name, osd, err)
err = discover.FreeDevices(c.context, n.Name, c.Namespace)
if err != nil {
logger.Warningf("failed to free devices: %s", err)
}
continue
}
logger.Infof("deployment for osd %d already exists. updating if needed", osd.ID)
if err = k8sutil.UpdateDeploymentAndWait(c.context, dp, c.Namespace); err != nil {
config.addError(fmt.Sprintf("failed to update osd deployment %d. %+v", osd.ID, err))
}
}
logger.Infof("started deployment for osd %d (dir=%t, type=%s)", osd.ID, osd.IsDirectory, storeConfig.StoreType)
}
}
func (c *Cluster) deleteDeploymentWithLegacyName(osdID int) error {
legacyName := fmt.Sprintf(legacyAppNameFmt, osdID)
return k8sutil.DeleteDeployment(c.context.Clientset, c.Namespace, legacyName)
}
func (c *Cluster) handleRemovedNodes(config *provisionConfig) {
// find all removed nodes (if any) and start orchestration to remove them from the cluster
removedNodes, err := c.findRemovedNodes()
if err != nil {
config.addError("failed to find removed nodes: %+v", err)
}
logger.Infof("processing %d removed nodes", len(removedNodes))
for removedNode, osdDeployments := range removedNodes {
logger.Infof("processing removed node %s", removedNode)
if err := c.isSafeToRemoveNode(removedNode, osdDeployments); err != nil {
logger.Warningf("skipping the removal of node %s because it is not safe to do so: %+v", removedNode, err)
continue
}
logger.Infof("removing node %s from the cluster with %d OSDs", removedNode, len(osdDeployments))
var nodeCrushName string
errorOnCurrentNode := false
for _, dp := range osdDeployments {
logger.Infof("processing removed osd %s", dp.Name)
id := getIDFromDeployment(dp)
if id == unknownID {
config.addError("cannot remove unknown osd %s", dp.Name)
continue
}
// on the first osd, get the crush name of the host
if nodeCrushName == "" {
nodeCrushName, err = client.GetCrushHostName(c.context, c.Namespace, id)
if err != nil {
config.addError("failed to get crush host name for osd.%d: %+v", id, err)
}
}
if err := removeOSD(c.context, c.Namespace, dp.Name, id); err != nil {
config.addError("failed to remove osd %d. %+v", id, err)
errorOnCurrentNode = true
continue
}
}
if err := c.updateNodeStatus(removedNode, OrchestrationStatus{Status: OrchestrationStatusCompleted}); err != nil {
config.addError("failed to set orchestration starting status for removed node %s: %+v", removedNode, err)
}
if errorOnCurrentNode {
logger.Warningf("done processing %d osd removals on node %s with an error removing the osds. skipping node cleanup", len(osdDeployments), removedNode)
} else {
logger.Infof("succeeded processing %d osd removals on node %s. starting cleanup job on the node.", len(osdDeployments), removedNode)
c.cleanupRemovedNode(config, removedNode, nodeCrushName)
}
}
logger.Infof("done processing removed nodes")
}
func (c *Cluster) cleanupRemovedNode(config *provisionConfig, nodeName, crushName string) {
// update the orchestration status of this removed node to the starting state
if err := c.updateNodeStatus(nodeName, OrchestrationStatus{Status: OrchestrationStatusStarting}); err != nil {
config.addError("failed to set orchestration starting status for removed node %s: %+v", nodeName, err)
return
}
// trigger orchestration on the removed node by telling it not to use any storage at all. note that the directories are still passed in
// so that the pod will be able to mount them and migrate data from them.
job, err := c.makeJob(nodeName, []rookalpha.Device{}, rookalpha.Selection{DeviceFilter: "none"},
v1.ResourceRequirements{}, osdconfig.StoreConfig{}, "", "")
if err != nil {
message := fmt.Sprintf("failed to create prepare job node %s: %v", nodeName, err)
config.addError(message)
status := OrchestrationStatus{Status: OrchestrationStatusCompleted, Message: message}
if err := c.updateNodeStatus(nodeName, status); err != nil {
config.addError("failed to update node %s status. %+v", nodeName, err)
}
return
}
if !c.updateJob(job, nodeName, config, "remove") {
status := OrchestrationStatus{Status: OrchestrationStatusCompleted, Message: fmt.Sprintf("failed to cleanup osd config on node %s", nodeName)}
if err := c.updateNodeStatus(nodeName, status); err != nil {
config.addError("failed to update node %s status. %+v", nodeName, err)
}
return
}
logger.Infof("waiting for removal cleanup on node %s", nodeName)
c.completeProvisionSkipOSDStart(config)
logger.Infof("done waiting for removal cleanup on node %s", nodeName)
// after the batch job is finished, clean up all the resources related to the node
if crushName != "" {
if err := c.cleanUpNodeResources(nodeName, crushName); err != nil {
config.addError("failed to cleanup node resources for %s", crushName)
}
}
}
func (c *Cluster) discoverStorageNodes() (map[string][]*extensions.Deployment, error) {
listOpts := metav1.ListOptions{LabelSelector: fmt.Sprintf("app=%s", appName)}
osdDeployments, err := c.context.Clientset.Extensions().Deployments(c.Namespace).List(listOpts)
if err != nil {
return nil, fmt.Errorf("failed to list osd deployment: %+v", err)
}
discoveredNodes := map[string][]*extensions.Deployment{}
for _, osdDeployment := range osdDeployments.Items {
osdPodSpec := osdDeployment.Spec.Template.Spec
// get the node name from the node selector
nodeName, ok := osdPodSpec.NodeSelector[apis.LabelHostname]
if !ok || nodeName == "" {
return nil, fmt.Errorf("osd deployment %s doesn't have a node name on its node selector: %+v", osdDeployment.Name, osdPodSpec.NodeSelector)
}
if _, ok := discoveredNodes[nodeName]; !ok {
discoveredNodes[nodeName] = []*extensions.Deployment{}
}
logger.Debugf("adding osd %s to node %s", osdDeployment.Name, nodeName)
osdCopy := osdDeployment
discoveredNodes[nodeName] = append(discoveredNodes[nodeName], &osdCopy)
}
return discoveredNodes, nil
}
func (c *Cluster) isSafeToRemoveNode(nodeName string, osdDeployments []*extensions.Deployment) error {
if err := client.IsClusterClean(c.context, c.Namespace); err != nil {
// the cluster isn't clean, it's not safe to remove this node
return err
}
// get the current used space on all OSDs in the cluster
currUsage, err := client.GetOSDUsage(c.context, c.Namespace)
if err != nil {
return err
}
// sum up the total OSD used space for the node by summing the used space of each OSD on the node
nodeUsage := int64(0)
for _, osdDeployment := range osdDeployments {
id := getIDFromDeployment(osdDeployment)
if id == unknownID {
continue
}
osdUsage := currUsage.ByID(id)
if osdUsage != nil {
osdKB, err := osdUsage.UsedKB.Int64()
if err != nil {
logger.Warningf("osd.%d has invalid usage %+v: %+v", id, osdUsage.UsedKB, err)
continue
}
nodeUsage += osdKB * 1024
}
}
// check to see if there is sufficient space left in the cluster to absorb all the migrated data from the node to be removed
clusterUsage, err := client.Usage(c.context, c.Namespace)
if err != nil {
return err
}
clusterAvailableBytes, err := clusterUsage.Stats.TotalAvailBytes.Int64()
if err != nil {
return err
}
clusterTotalBytes, err := clusterUsage.Stats.TotalBytes.Int64()
if err != nil {
return err
}
if (clusterAvailableBytes - nodeUsage) < int64((float64(clusterTotalBytes) * clusterAvailableSpaceReserve)) {
// the remaining available space in the cluster after the space that this node is using gets moved elsewhere
// would be less than the cluster available space reserve, it's not safe to remove this node
return fmt.Errorf("insufficient available space in the cluster to remove node %s. node usage: %s, cluster available: %s",
nodeName, display.BytesToString(uint64(nodeUsage)), display.BytesToString(uint64(clusterAvailableBytes)))
}
// looks safe to remove the node
return nil
}
func getIDFromDeployment(deployment *extensions.Deployment) int {
if idstr, ok := deployment.Labels[osdLabelKey]; ok {
id, err := strconv.Atoi(idstr)
if err != nil {
logger.Errorf("unknown osd id from label %s", idstr)
return unknownID
}
return id
}
logger.Errorf("unknown osd id for deployment %s", deployment.Name)
return unknownID
}
func (c *Cluster) resolveNode(nodeName string) *rookalpha.Node {
// fully resolve the storage config and resources for this node
rookNode := c.Storage.ResolveNode(nodeName)
if rookNode == nil {
return nil
}
rookNode.Resources = k8sutil.MergeResourceRequirements(rookNode.Resources, c.resources)
// ensure no invalid dirs are specified
var validDirs []rookalpha.Directory
for _, dir := range rookNode.Directories {
if dir.Path == k8sutil.DataDir || dir.Path == c.dataDirHostPath {
logger.Warningf("skipping directory %s that would conflict with the dataDirHostPath", dir.Path)
continue
}
validDirs = append(validDirs, dir)
}
rookNode.Directories = validDirs
return rookNode
}