Skip to content

Commit

Permalink
Merge branch 'master' into readable-version-line
Browse files Browse the repository at this point in the history
  • Loading branch information
teran authored Jun 26, 2024
2 parents a6dddbf + a5bc393 commit bbde782
Show file tree
Hide file tree
Showing 38 changed files with 919 additions and 102 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ to adjust `ceph` binary path to access ceph in container and/or remote machine.
* [X] FreeBSD support in builds
* [X] Remote Ceph cluster access via SSH
* [ ] v0.2.0
* [ ] Apply/Dump declarative configuration for `ceph osd set-*` stuff
* [X] Apply/Dump declarative configuration for `ceph osd set-*` stuff
* [ ] v0.3.0
* [ ] Apply/Dump declarative configuration for Ceph Object Gateway (rgw)
* [ ] v0.4.0
Expand Down Expand Up @@ -137,6 +137,22 @@ drop-in replacement for official ceph image to use for `cephadm shell` command.

Container image is available at [GitHub Packages](https://github.com/runityru/cephctl/pkgs/container/cephctl%2Fceph)

To replace official Ceph image with the one containing cephctl in cephadm
clusters just do:

```shell
# Set container image as a global parameter to all components
ceph config set global container_image ghcr.io/runityru/cephctl/ceph:v18.2.2

# Run upgrade procedure
ceph orch upgrade start --ceph_version=18.2.2
```

Please note Ceph orch will automatically replace `container_image` parameter
for each component with specific sha256 image ID instead of tag we defined
manually. It's OK and it's a guarantee the image won't be changed in your
cluster.

### Build from source

It's possible to build cephctl from source by simply running the following
Expand Down
42 changes: 35 additions & 7 deletions ceph/ceph.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"

"github.com/pkg/errors"
Expand All @@ -16,6 +15,7 @@ import (

type Ceph interface {
ApplyCephConfigOption(ctx context.Context, section, key, value string) error
ApplyCephOSDConfigOption(ctx context.Context, key, value string) error
ClusterReport(ctx context.Context) (models.ClusterReport, error)
ClusterStatus(ctx context.Context) (models.ClusterStatus, error)
DumpConfig(ctx context.Context) (models.CephConfig, error)
Expand All @@ -37,20 +37,48 @@ func (c *ceph) ApplyCephConfigOption(ctx context.Context, section, key, value st
bin, args := mkCommand(c.binaryPath, []string{"config", "set", section, key, value})

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stderr = os.Stderr
cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel)
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "error applying configuration")
}
return nil
}

func (c *ceph) ApplyCephOSDConfigOption(ctx context.Context, key, value string) error {
keyArgs := []string{}
switch key {
case "AllowCrimson":
keyArgs = []string{"osd", "set-allow-crimson", "--yes-i-really-mean-it"}
case "BackfillfullRatio":
keyArgs = []string{"osd", "set-backfillfull-ratio", value}
case "FullRatio":
keyArgs = []string{"osd", "set-full-ratio", value}
case "NearfullRatio":
keyArgs = []string{"osd", "set-nearfull-ratio", value}
case "RequireMinCompatClient":
keyArgs = []string{"osd", "set-require-min-compat-client", value}
default:
return errors.Errorf("unexpected key: `%s`", key)
}

bin, args := mkCommand(c.binaryPath, keyArgs)

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel)
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "error applying OSD configuration")
}

return nil
}

func (c *ceph) ClusterReport(ctx context.Context) (models.ClusterReport, error) {
buf := &bytes.Buffer{}
bin, args := mkCommand(c.binaryPath, []string{"report", "--format=json"})

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stdout = buf
cmd.Stderr = os.Stderr
cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel)
if err := cmd.Run(); err != nil {
return models.ClusterReport{}, errors.Wrap(err, "error retrieving report")
}
Expand All @@ -71,7 +99,7 @@ func (c ceph) ClusterStatus(ctx context.Context) (models.ClusterStatus, error) {

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stdout = buf
cmd.Stderr = os.Stderr
cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel)
if err := cmd.Run(); err != nil {
return models.ClusterStatus{}, errors.Wrap(err, "error retrieving cluster status")
}
Expand All @@ -93,7 +121,7 @@ func (c *ceph) DumpConfig(ctx context.Context) (models.CephConfig, error) {

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stdout = buf
cmd.Stderr = os.Stderr
cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel)
if err := cmd.Run(); err != nil {
return nil, errors.Wrap(err, "error running command")
}
Expand Down Expand Up @@ -122,7 +150,7 @@ func (c *ceph) ListDevices(ctx context.Context) ([]models.Device, error) {

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stdout = buf
cmd.Stderr = os.Stderr
cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel)
if err := cmd.Run(); err != nil {
return nil, errors.Wrap(err, "error listing devices")
}
Expand All @@ -147,7 +175,7 @@ func (c *ceph) RemoveCephConfigOption(ctx context.Context, section, key string)
bin, args := mkCommand(c.binaryPath, []string{"config", "rm", section, key})

cmd := exec.CommandContext(ctx, bin, args...)
cmd.Stderr = os.Stderr
cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel)
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "error applying configuration")
}
Expand Down
22 changes: 22 additions & 0 deletions ceph/ceph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ func TestApplyCephConfigOption(t *testing.T) {
r.NoError(err)
}

func TestApplyCephOSDConfigOption(t *testing.T) {
r := require.New(t)

c := New("testdata/ceph_mock_ApplyCephOSDConfigOption")
err := c.ApplyCephOSDConfigOption(context.Background(), "AllowCrimson", "true")
r.NoError(err)
}

func TestApplyCephOSDConfigOptionInvalidKey(t *testing.T) {
r := require.New(t)

c := New("testdata/ceph_mock_ApplyCephOSDConfigOption")
err := c.ApplyCephOSDConfigOption(context.Background(), "key", "value")
r.Error(err)
r.Equal("unexpected key: `key`", err.Error())
}

func TestClusterReport(t *testing.T) {
r := require.New(t)

Expand Down Expand Up @@ -56,6 +73,11 @@ func TestClusterReport(t *testing.T) {
"active": 234,
"clean": 234,
},
AllowCrimson: false,
NearfullRatio: 0.85,
BackfillfullRatio: 0.9,
FullRatio: 0.95,
RequireMinCompatClient: "luminous",
OSDDaemons: []models.OSDDaemon{
{
ID: 0,
Expand Down
22 changes: 22 additions & 0 deletions ceph/config/spec/cephosdconfig/spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cephosdconfig

import (
"github.com/creasty/defaults"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"

"github.com/runityru/cephctl/models"
)

func New(in []byte) (models.CephOSDConfig, error) {
spec := models.CephOSDConfig{}
if err := defaults.Set(&spec); err != nil {
return models.CephOSDConfig{}, errors.Wrap(err, "error setting default values")
}

if err := yaml.Unmarshal(in, &spec); err != nil {
return models.CephOSDConfig{}, errors.Wrap(err, "error decoding spec file")
}

return spec, nil
}
44 changes: 44 additions & 0 deletions ceph/config/spec/cephosdconfig/spec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cephosdconfig

import (
"os"
"testing"

"github.com/stretchr/testify/require"

"github.com/runityru/cephctl/models"
)

func TestNewValidConfig(t *testing.T) {
r := require.New(t)

data, err := os.ReadFile("testdata/full.json")
r.NoError(err)

cfg, err := New(data)
r.NoError(err)
r.Equal(models.CephOSDConfig{
AllowCrimson: true,
NearfullRatio: 0.75,
BackfillfullRatio: 0.8,
FullRatio: 0.85,
RequireMinCompatClient: "squid",
}, cfg)
}

func TestNewEmptyConfig(t *testing.T) {
r := require.New(t)

data, err := os.ReadFile("testdata/empty.json")
r.NoError(err)

cfg, err := New(data)
r.NoError(err)
r.Equal(models.CephOSDConfig{
AllowCrimson: false,
NearfullRatio: 0.85,
BackfillfullRatio: 0.9,
FullRatio: 0.95,
RequireMinCompatClient: "reef",
}, cfg)
}
1 change: 1 addition & 0 deletions ceph/config/spec/cephosdconfig/testdata/empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
7 changes: 7 additions & 0 deletions ceph/config/spec/cephosdconfig/testdata/full.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"allow_crimson": true,
"nearfull_ratio": 0.75,
"backfillfull_ratio": 0.8,
"full_ratio": 0.85,
"require_min_compat_client": "squid"
}
53 changes: 27 additions & 26 deletions ceph/config/spec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,51 @@ package spec

import (
"encoding/json"
"io"
"os"
"path/filepath"
"strings"

"github.com/pkg/errors"
yaml "gopkg.in/yaml.v3"
)

type description struct {
type Description struct {
Kind string `json:"kind"`
Spec json.RawMessage `json:"spec"`
}

func NewFromDescription(filename string) (string, json.RawMessage, error) {
desc := description{}
type yamlIntermediate struct {
Kind string `yaml:"kind"`
Spec any `yaml:"spec"`
}

data, err := os.ReadFile(filename)
func NewFromDescription(filename string) ([]Description, error) {
fp, err := os.Open(filename)
if err != nil {
return "", nil, errors.Wrap(err, "error reading configuration file")
return nil, errors.Wrap(err, "error opening spec file")
}
defer fp.Close()

switch strings.ToLower(filepath.Ext(filename)) {
case ".yml", ".yaml":
type intermediate struct {
Kind string `yaml:"kind"`
Spec any `yaml:"spec"`
}

d := intermediate{}
err := yaml.Unmarshal(data, &d)
docs := []Description{}
dec := yaml.NewDecoder(fp)
for {
v := yamlIntermediate{}
err := dec.Decode(&v)
if err != nil {
return "", nil, errors.Wrap(err, "error unmarshaling intermediate configuration")
if errors.Is(err, io.EOF) {
break
}
return nil, errors.Wrap(err, "error unmarshaling document")
}

spec, err := json.Marshal(d.Spec)
spec, err := json.Marshal(v.Spec)
if err != nil {
return "", nil, errors.Wrap(err, "error marshaling intermediate configuration")
return nil, errors.Wrap(err, "error marshaling intermediate data structure")
}
return d.Kind, json.RawMessage(spec), nil
case ".json":
// skip since supported natively
default:
return "", nil, errors.Errorf("unexpected file format: `%s`", filepath.Ext(filename))

docs = append(docs, Description{
Kind: v.Kind,
Spec: json.RawMessage(spec),
})
}

return desc.Kind, desc.Spec, json.Unmarshal(data, &desc)
return docs, nil
}
22 changes: 14 additions & 8 deletions ceph/config/spec/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ import (
"github.com/stretchr/testify/require"
)

func TestNewFromDescriptionYAML(t *testing.T) {
func TestNewFromDescriptionSingle(t *testing.T) {
r := require.New(t)

kind, spec, err := NewFromDescription("testdata/sample_NewFromDescriptionYAML.yaml")
descs, err := NewFromDescription("testdata/sample_NewFromDescriptionSingle.yaml")
r.NoError(err)
r.Equal("CephConfig", kind)
r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(spec))
r.Len(descs, 1)
r.Equal("CephConfig", descs[0].Kind)
r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(descs[0].Spec))
}

func TestNewFromDescriptionJSON(t *testing.T) {
func TestNewFromDescriptionMulti(t *testing.T) {
r := require.New(t)

kind, spec, err := NewFromDescription("testdata/sample_NewFromDescriptionJSON.json")
descs, err := NewFromDescription("testdata/sample_NewFromDescriptionMulti.yaml")
r.NoError(err)
r.Equal("CephConfig", kind)
r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(spec))
r.Len(descs, 2)

r.Equal("CephConfig", descs[0].Kind)
r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(descs[0].Spec))

r.Equal("CephOSDConfig", descs[1].Kind)
r.JSONEq(`{"allow_crimson":true}`, string(descs[1].Spec))
}
11 changes: 0 additions & 11 deletions ceph/config/spec/testdata/sample_NewFromDescriptionJSON.json

This file was deleted.

11 changes: 11 additions & 0 deletions ceph/config/spec/testdata/sample_NewFromDescriptionMulti.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
kind: CephConfig
spec:
global:
rbd_cache: "true"
osd:
rocksdb_perf: "true"
---
kind: CephOSDConfig
spec:
allow_crimson: true
5 changes: 5 additions & 0 deletions ceph/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ func (m *Mock) ApplyCephConfigOption(ctx context.Context, section, key, value st
return args.Error(0)
}

func (m *Mock) ApplyCephOSDConfigOption(_ context.Context, key, value string) error {
args := m.Called(key, value)
return args.Error(0)
}

func (m *Mock) ClusterReport(ctx context.Context) (models.ClusterReport, error) {
args := m.Called()
return args.Get(0).(models.ClusterReport), args.Error(1)
Expand Down
Loading

0 comments on commit bbde782

Please sign in to comment.