introduce version parser and IsAtLeast constraint

This commit is contained in:
Kyle 2022-02-22 16:00:42 -08:00
parent 4e84633fc0
commit 13e97cd25d
3 changed files with 283 additions and 49 deletions

View File

@ -21,7 +21,6 @@ import (
"net"
"net/http"
"os"
"strings"
"sync"
"syscall"
"time"
@ -32,6 +31,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/digitalocean/ceph_exporter/collectors"
"github.com/digitalocean/ceph_exporter/version"
)
const (
@ -42,6 +42,10 @@ const (
)
var (
// CephVersion is the parsed *version.Version from the Ceph version
CephVersion *version.Version
versionLock sync.Mutex
errCephVersionUnsupported = errors.New("ceph version unsupported")
)
@ -77,10 +81,12 @@ func (ln emfileAwareTcpListener) Accept() (c net.Conn, err error) {
// prometheus. It also implements a prometheus.Collector interface in order
// to register it correctly.
type CephExporter struct {
mu sync.Mutex
conn collectors.Conn
collectors map[string][]prometheus.Collector
logger *logrus.Logger
mu sync.Mutex
conn collectors.Conn
cluster string
config string
rgwMode int
logger *logrus.Logger
}
// Verify that the exporter implements the interface correctly.
@ -90,44 +96,37 @@ var _ prometheus.Collector = &CephExporter{}
// to it. We can choose to enable a collector to extract stats out of by adding
// it to the list of collectors.
func NewCephExporter(conn collectors.Conn, cluster string, config string, rgwMode int, logger *logrus.Logger) *CephExporter {
return &CephExporter{
conn: conn,
cluster: cluster,
config: config,
rgwMode: rgwMode,
logger: logger,
}
}
func (c *CephExporter) getCollectors() []prometheus.Collector {
standardCollectors := []prometheus.Collector{
collectors.NewClusterUsageCollector(conn, cluster, logger),
collectors.NewPoolUsageCollector(conn, cluster, logger),
collectors.NewPoolInfoCollector(conn, cluster, logger),
collectors.NewClusterHealthCollector(conn, cluster, logger),
collectors.NewMonitorCollector(conn, cluster, logger),
collectors.NewOSDCollector(conn, cluster, logger),
collectors.NewClusterUsageCollector(c.conn, c.cluster, c.logger),
collectors.NewPoolUsageCollector(c.conn, c.cluster, c.logger),
collectors.NewPoolInfoCollector(c.conn, c.cluster, c.logger),
collectors.NewClusterHealthCollector(c.conn, c.cluster, c.logger),
collectors.NewMonitorCollector(c.conn, c.cluster, c.logger),
collectors.NewOSDCollector(c.conn, c.cluster, c.logger),
}
c := &CephExporter{
conn: conn,
collectors: map[string][]prometheus.Collector{
"nautilus": standardCollectors,
"octopus": standardCollectors,
"pacific": standardCollectors,
},
logger: logger,
}
switch rgwMode {
switch c.rgwMode {
case collectors.RGWModeForeground:
for version := range c.collectors {
c.collectors[version] = append(c.collectors[version], collectors.NewRGWCollector(cluster, config, false, logger))
}
standardCollectors = append(standardCollectors, collectors.NewRGWCollector(c.cluster, c.config, false, c.logger))
case collectors.RGWModeBackground:
for version := range c.collectors {
c.collectors[version] = append(c.collectors[version], collectors.NewRGWCollector(cluster, config, true, logger))
}
standardCollectors = append(standardCollectors, collectors.NewRGWCollector(c.cluster, c.config, true, c.logger))
case collectors.RGWModeDisabled:
// nothing to do
default:
logger.WithField("rgwMode", rgwMode).Warn("RGW collector disabled due to invalid mode")
c.logger.WithField("rgwMode", c.rgwMode).Warn("RGW collector disabled due to invalid mode")
}
return c
return standardCollectors
}
func (c *CephExporter) cephVersionCmd() []byte {
@ -142,10 +141,10 @@ func (c *CephExporter) cephVersionCmd() []byte {
return cmd
}
func (c *CephExporter) getCephVersion() (string, error) {
func (c *CephExporter) setCephVersion() error {
buf, _, err := c.conn.MonCommand(c.cephVersionCmd())
if err != nil {
return "", err
return err
}
cephVersion := &struct {
@ -154,30 +153,31 @@ func (c *CephExporter) getCephVersion() (string, error) {
err = json.Unmarshal(buf, cephVersion)
if err != nil {
return "", err
return err
}
if strings.Contains(cephVersion.Version, "nautilus") {
return "nautilus", nil
} else if strings.Contains(cephVersion.Version, "octopus") {
return "octopus", nil
} else if strings.Contains(cephVersion.Version, "pacific") {
return "pacific", nil
parsedVersion, err := version.ParseCephVersion(cephVersion.Version)
if err != nil {
return err
}
return "", errCephVersionUnsupported
versionLock.Lock()
CephVersion = parsedVersion
versionLock.Unlock()
return nil
}
// Describe sends all the descriptors of the collectors included to
// the provided channel.
func (c *CephExporter) Describe(ch chan<- *prometheus.Desc) {
version, err := c.getCephVersion()
err := c.setCephVersion()
if err != nil {
c.logger.WithError(err).Error("failed to determine ceph version")
c.logger.WithError(err).Error("failed to set ceph version")
return
}
for _, cc := range c.collectors[version] {
for _, cc := range c.getCollectors() {
cc.Describe(ch)
}
}
@ -186,16 +186,16 @@ func (c *CephExporter) Describe(ch chan<- *prometheus.Desc) {
// prometheus. Collect could be called several times concurrently
// and thus its run is protected by a single mutex.
func (c *CephExporter) Collect(ch chan<- prometheus.Metric) {
version, err := c.getCephVersion()
err := c.setCephVersion()
if err != nil {
c.logger.WithError(err).Error("failed to determine ceph version")
c.logger.WithError(err).Error("failed to set ceph version")
return
}
c.mu.Lock()
defer c.mu.Unlock()
for _, cc := range c.collectors[version] {
for _, cc := range c.getCollectors() {
cc.Collect(ch)
}
}

109
version/version.go Normal file
View File

@ -0,0 +1,109 @@
package version
import (
"errors"
"strconv"
"strings"
)
// Version contains all the Ceph version details
type Version struct {
Major int
Minor int
Patch int
Revision int
Commit string
}
var (
// ErrInvalidVersion indicates that the given version string was invalid
ErrInvalidVersion = errors.New("invalid version")
// Nautilus is the *Version at which Ceph nautilus was released
Nautilus = &Version{Major: 14, Minor: 2, Patch: 0, Revision: 0, Commit: ""}
// Octopus is the *Version at which Ceph octopus was released
Octopus = &Version{Major: 15, Minor: 2, Patch: 0, Revision: 0, Commit: ""}
// Pacific is the *Version at which Ceph pacific was released
Pacific = &Version{Major: 16, Minor: 2, Patch: 0, Revision: 0, Commit: ""}
)
// IsAtLeast returns true if the version is at least as new as the given constraint
// the commit is not considered
func (version *Version) IsAtLeast(constraint *Version) bool {
if version.Major > constraint.Major {
return true
} else if version.Major < constraint.Major {
return false
}
if version.Minor > constraint.Minor {
return true
} else if version.Minor < constraint.Minor {
return false
}
if version.Patch > constraint.Patch {
return true
} else if version.Patch < constraint.Patch {
return false
}
if version.Revision > constraint.Revision {
return true
} else if version.Revision < constraint.Revision {
return false
}
// the versions must be the same
return true
}
// ParseCephVersion parses the given ceph version string to a *Version or error
func ParseCephVersion(cephVersion string) (*Version, error) {
splitVersion := strings.Split(cephVersion, " ")
if len(splitVersion) < 3 {
return nil, ErrInvalidVersion
}
someVersions := strings.Split(splitVersion[2], ".")
if len(someVersions) != 3 {
return nil, ErrInvalidVersion
}
otherVersions := strings.Split(someVersions[2], "-")
if len(otherVersions) != 3 {
return nil, ErrInvalidVersion
}
major, err := strconv.Atoi(someVersions[0])
if err != nil {
return nil, err
}
minor, err := strconv.Atoi(someVersions[1])
if err != nil {
return nil, err
}
patch, err := strconv.Atoi(otherVersions[0])
if err != nil {
return nil, err
}
revision, err := strconv.Atoi(otherVersions[1])
if err != nil {
return nil, err
}
commit := otherVersions[2]
return &Version{
Major: major,
Minor: minor,
Patch: patch,
Revision: revision,
Commit: commit,
}, nil
}

125
version/version_test.go Normal file
View File

@ -0,0 +1,125 @@
package version
import (
"reflect"
"testing"
)
func TestParseCephVersion(t *testing.T) {
type args struct {
cephVersion string
}
tests := []struct {
name string
args args
want *Version
wantErr bool
}{
{
name: "invalid version 1",
args: args{cephVersion: "totally real version"},
want: nil,
wantErr: true,
},
{
name: "invalid version 2",
args: args{cephVersion: "ceph version 14.2.18-97"},
want: nil,
wantErr: true,
},
{
name: "nautilus",
args: args{cephVersion: "ceph version 14.2.18-97-gcc1e126 (cc1e1267bc7afc8288c718fc3e59c5a6735f6f4a) nautilus (stable)"},
want: &Version{Major: 14, Minor: 2, Patch: 18, Revision: 97, Commit: "gcc1e126"},
wantErr: false,
},
{
name: "octopus",
args: args{cephVersion: "ceph version 15.2.0-1-gcc1e126"},
want: &Version{Major: 15, Minor: 2, Patch: 0, Revision: 1, Commit: "gcc1e126"},
wantErr: false,
},
{
name: "pacific",
args: args{cephVersion: "ceph version 16.2.3-33-gcc1e126"},
want: &Version{Major: 16, Minor: 2, Patch: 3, Revision: 33, Commit: "gcc1e126"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseCephVersion(tt.args.cephVersion)
if (err != nil) != tt.wantErr {
t.Errorf("ParseCephVersion() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseCephVersion() got = %v, want %v", got, tt.want)
}
})
}
}
func TestVersion_IsAtLeast(t *testing.T) {
type fields struct {
Major int
Minor int
Patch int
Revision int
Commit string
}
type args struct {
constraint *Version
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "equal versions",
fields: fields{Major: Nautilus.Major, Minor: Nautilus.Minor, Patch: Nautilus.Patch, Revision: Nautilus.Revision, Commit: Nautilus.Commit},
args: args{constraint: Nautilus},
want: true,
},
{
name: "slightly older",
fields: fields{Major: Pacific.Major, Minor: Pacific.Minor - 1, Patch: Pacific.Patch, Revision: Pacific.Revision, Commit: Pacific.Commit},
args: args{constraint: Pacific},
want: false,
},
{
name: "significantly newer",
fields: fields{Major: Pacific.Major, Minor: Pacific.Minor - 1, Patch: Pacific.Patch, Revision: Pacific.Revision, Commit: Pacific.Commit},
args: args{constraint: Octopus},
want: true,
},
{
name: "older revision",
fields: fields{Major: 16, Minor: 2, Patch: 0, Revision: 1},
args: args{constraint: &Version{Major: 16, Minor: 2, Patch: 0, Revision: 2}},
want: false,
},
{
name: "newer revision",
fields: fields{Major: 16, Minor: 2, Patch: 0, Revision: 1},
args: args{constraint: Pacific},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
version := &Version{
Major: tt.fields.Major,
Minor: tt.fields.Minor,
Patch: tt.fields.Patch,
Revision: tt.fields.Revision,
Commit: tt.fields.Commit,
}
if got := version.IsAtLeast(tt.args.constraint); got != tt.want {
t.Errorf("IsAtLeast() = %v, want %v", got, tt.want)
}
})
}
}