Files
mgmt/etcd/methods.go
James Shubin d30ff6cfae legal: Remove year
Instead of constantly making these updates, let's just remove the year
since things are stored in git anyways, and this is not an actual modern
legal risk anymore.
2025-01-26 16:24:51 -05:00

250 lines
9.0 KiB
Go

// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package etcd
import (
"context"
"fmt"
"strings"
"github.com/purpleidea/mgmt/util/errwrap"
etcdtypes "go.etcd.io/etcd/client/pkg/v3/types"
etcd "go.etcd.io/etcd/client/v3"
etcdutil "go.etcd.io/etcd/client/v3/clientv3util"
)
// volunteer offers yourself up to be a server if needed. If you specify a nil
// value for urls, then this will unvolunteer yourself.
func (obj *EmbdEtcd) volunteer(ctx context.Context, urls etcdtypes.URLs) error {
if obj.Debug {
if urls == nil {
obj.Logf("unvolunteer...")
defer obj.Logf("unvolunteer: done!")
} else {
obj.Logf("volunteer: %s", urls.String())
defer obj.Logf("volunteer: done!")
}
}
// volunteer to be a server
key := fmt.Sprintf(obj.NS+volunteerPathFmt, obj.Hostname)
ifs := []etcd.Cmp{} // list matching the desired state
ops := []etcd.Op{} // list of ops in this txn
els := []etcd.Op{}
if urls != nil {
data := urls.String() // value is usually a peer "serverURL"
// XXX: bug: https://github.com/etcd-io/etcd/issues/10566
// XXX: reverse things with els to workaround the bug :(
//ifs = append(ifs, etcd.Compare(etcd.Value(key), "!=", data)) // desired state
//ops = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
ifs = append(ifs, etcd.Compare(etcd.Value(key), "=", data)) // desired state
ifs = append(ifs, etcd.Compare(etcd.LeaseValue(key), "=", obj.leaseID))
els = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
} else { // delete message if set to erase
ifs = append(ifs, etcdutil.KeyExists(key)) // desired state
ops = append(ops, etcd.OpDelete(key))
}
_, err := obj.client.Txn(ctx, ifs, ops, els)
msg := "volunteering failed"
if urls == nil {
msg = "unvolunteering failed"
}
return errwrap.Wrapf(err, msg)
}
// nominate nominates a particular client to be a server (peer). If you specify
// a nil value for urls, then this will unnominate that member.
func (obj *EmbdEtcd) nominate(ctx context.Context, hostname string, urls etcdtypes.URLs) error {
if obj.Debug {
if urls == nil {
obj.Logf("unnominate(%s)...", hostname)
defer obj.Logf("unnominate(%s): done!", hostname)
} else {
obj.Logf("nominate(%s): %s", hostname, urls.String())
defer obj.Logf("nominate(%s): done!", hostname)
}
}
// nominate someone to be a server
key := fmt.Sprintf(obj.NS+nominatedPathFmt, hostname)
ifs := []etcd.Cmp{} // list matching the desired state
ops := []etcd.Op{} // list of ops in this txn
els := []etcd.Op{}
if urls != nil {
data := urls.String()
// XXX: bug: https://github.com/etcd-io/etcd/issues/10566
// XXX: reverse things with els to workaround the bug :(
//ifs = append(ifs, etcd.Compare(etcd.Value(key), "!=", data)) // desired state
//ops = append(ops, etcd.OpPut(key, data)) // TODO: add a TTL? (etcd.WithLease)
ifs = append(ifs, etcd.Compare(etcd.Value(key), "=", data)) // desired state
els = append(ops, etcd.OpPut(key, data)) // TODO: add a TTL? (etcd.WithLease)
} else { // delete message if set to erase
ifs = append(ifs, etcdutil.KeyExists(key)) // desired state
ops = append(ops, etcd.OpDelete(key))
}
_, err := obj.client.Txn(ctx, ifs, ops, els)
msg := "nominate failed"
if urls == nil {
msg = "unnominate failed"
}
return errwrap.Wrapf(err, msg)
}
// advertise idempotently advertises the list of available client endpoints for
// the given member. If you specify a nil value for urls, then this will remove
// that member.
func (obj *EmbdEtcd) advertise(ctx context.Context, hostname string, urls etcdtypes.URLs) error {
if obj.Debug {
if urls == nil {
obj.Logf("unadvertise(%s)...", hostname)
defer obj.Logf("unadvertise(%s): done!", hostname)
} else {
obj.Logf("advertise(%s): %s", hostname, urls.String())
defer obj.Logf("advertise(%s): done!", hostname)
}
}
// advertise endpoints
key := fmt.Sprintf(obj.NS+endpointsPathFmt, hostname)
ifs := []etcd.Cmp{} // list matching the desired state
ops := []etcd.Op{} // list of ops in this txn
els := []etcd.Op{}
if urls != nil {
data := urls.String() // value is usually a "clientURL"
// XXX: bug: https://github.com/etcd-io/etcd/issues/10566
// XXX: reverse things with els to workaround the bug :(
//ifs = append(ifs, etcd.Compare(etcd.Value(key), "!=", data)) // desired state
//ops = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
ifs = append(ifs, etcd.Compare(etcd.Value(key), "=", data)) // desired state
ifs = append(ifs, etcd.Compare(etcd.LeaseValue(key), "=", obj.leaseID))
els = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
} else { // delete in this case
ifs = append(ifs, etcdutil.KeyExists(key)) // desired state
ops = append(ops, etcd.OpDelete(key))
}
_, err := obj.client.Txn(ctx, ifs, ops, els)
msg := "advertising failed"
if urls == nil {
msg = "unadvertising failed"
}
return errwrap.Wrapf(err, msg)
}
// getVolunteers returns a urls map of available etcd server volunteers.
func (obj *EmbdEtcd) getVolunteers(ctx context.Context) (etcdtypes.URLsMap, error) {
if obj.Debug {
obj.Logf("getVolunteers()")
defer obj.Logf("getVolunteers(): done!")
}
p := obj.NS + VolunteerPath
keyMap, err := obj.client.Get(ctx, p, etcd.WithPrefix())
if err != nil {
return nil, errwrap.Wrapf(err, "can't get peer volunteers")
}
volunteers := make(etcdtypes.URLsMap)
for key, val := range keyMap { // loop through directory of volunteers
if !strings.HasPrefix(key, p) {
continue
}
name := key[len(p):] // get name of volunteer
if val == "" { // skip "erased" values
continue
}
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
if err != nil {
return nil, errwrap.Wrapf(err, "data format error")
}
volunteers[name] = urls // add to map
}
return volunteers, nil
}
// getNominated returns a urls map of nominated etcd server volunteers.
// NOTE: I know 'nominees' might be more correct, but is less consistent here
func (obj *EmbdEtcd) getNominated(ctx context.Context) (etcdtypes.URLsMap, error) {
if obj.Debug {
obj.Logf("getNominated()")
defer obj.Logf("getNominated(): done!")
}
p := obj.NS + NominatedPath
keyMap, err := obj.client.Get(ctx, p, etcd.WithPrefix()) // map[string]string, bool
if err != nil {
return nil, errwrap.Wrapf(err, "can't get nominated peers")
}
nominated := make(etcdtypes.URLsMap)
for key, val := range keyMap { // loop through directory of nominated
if !strings.HasPrefix(key, p) {
continue
}
name := key[len(p):] // get name of nominee
if val == "" { // skip "erased" values
continue
}
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
if err != nil {
return nil, errwrap.Wrapf(err, "data format error")
}
nominated[name] = urls // add to map
}
return nominated, nil
}
// getEndpoints returns a urls map of available endpoints for clients.
func (obj *EmbdEtcd) getEndpoints(ctx context.Context) (etcdtypes.URLsMap, error) {
if obj.Debug {
obj.Logf("getEndpoints()")
defer obj.Logf("getEndpoints(): done!")
}
p := obj.NS + EndpointsPath
keyMap, err := obj.client.Get(ctx, p, etcd.WithPrefix())
if err != nil {
return nil, errwrap.Wrapf(err, "can't get client endpoints")
}
endpoints := make(etcdtypes.URLsMap)
for key, val := range keyMap { // loop through directory of endpoints
if !strings.HasPrefix(key, p) {
continue
}
name := key[len(p):] // get name of volunteer
if val == "" { // skip "erased" values
continue
}
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
if err != nil {
return nil, errwrap.Wrapf(err, "data format error")
}
endpoints[name] = urls // add to map
}
return endpoints, nil
}