sshfp-generator: initial commit.

Fully working version.
TODO: Few minor changes

Signed-off-by: Artur Marud <artur@status.im>
This commit is contained in:
Artur Marud 2022-06-24 02:41:58 +02:00
parent cc366c072c
commit be748b44d6
No known key found for this signature in database
GPG Key ID: 3A50153F6C80C7F9
22 changed files with 960 additions and 1 deletions

3
.gitignore vendored
View File

@ -7,9 +7,10 @@
# Test binary, built with `go test -c`
*.test
test*
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
vendor/

34
README.md Normal file
View File

@ -0,0 +1,34 @@
# Description
SSHFP Tool is a tool created in Golang to glue Consul DB and Cloudflare.
Main purpose is creating SSHFP records to get rid of "host key verification failed".
## Building
```
go mod -vendor
go build -mod vendor
```
## Usage
Supported env variables:
`DOMAIN_NAME` - Domain name which will be working on
`CF_TOKEN` - CloudFlare Token with write access to above domain
`CONSUL_TOKEN` - Consul Token with priviledges to read services
It's possible to create json formatted config file (example in `testcfg`)
As it has been designed to work with `consul watches` passing proper .json file
to STDIN is required. Ex:
`cat watches.dump | ./infra-sshfp-cf`
## Current state
- CloudFlare integration is fully implemented
- SSHFP Record creation based on tag in Consul form.
- Implemented Consul watches integration
- Implemented logic to manipulate states (merging config, etc)
## TODO:
- A few major changes

23
cloudflare/interface.go Normal file
View File

@ -0,0 +1,23 @@
package cloudflare
import (
"infra-sshfp-cf/sshfp"
"github.com/cloudflare/cloudflare-go"
)
type Repository interface {
FindRecords(hostname string, recordType string) ([]cloudflare.DNSRecord, error)
CreateDNSRecord(hostname string, recordType string, payload cloudflare.DNSRecord) (int, error)
DeleteDNSRecord(recordID string) error
UpdateDNSRecord(hostname string, recordID string, payload cloudflare.DNSRecord) error
}
type Service interface {
FindHostByName(hostname string) (bool, error)
GetSSHFPRecordsForHost(hostname string) ([]*sshfp.SSHFPRecord, error)
CreateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) (int, error)
DeleteSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error
UpdateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error
ApplyConfigPlan(configPlan sshfp.ConfigPlan) (int, error)
}

72
cloudflare/repository.go Normal file
View File

@ -0,0 +1,72 @@
package cloudflare
import (
"context"
"errors"
"github.com/cloudflare/cloudflare-go"
"github.com/sirupsen/logrus"
)
type repository struct {
api *cloudflare.API
domainId string
domainName string
ctx context.Context
}
func NewRepository(token string, domain string) Repository {
logrus.Infof("cloudflare: Creating Repository")
api, err := cloudflare.NewWithAPIToken(token)
if err != nil {
logrus.Error(err)
return nil
}
id, err := api.ZoneIDByName(domain)
if err != nil {
logrus.Error(err)
return nil
}
return &repository{api: api, domainId: id, domainName: domain, ctx: context.Background()}
}
func (r *repository) FindRecords(hostname string, recordType string) ([]cloudflare.DNSRecord, error) {
logrus.Debugf("cloudflare: FindRecords: %s", hostname)
filterSet := cloudflare.DNSRecord{Type: recordType, Name: hostname + "." + r.domainName}
logrus.Debugf("%+v", filterSet)
return r.api.DNSRecords(r.ctx, r.domainId, filterSet)
}
func (r *repository) CreateDNSRecord(hostname string, recordType string, payload cloudflare.DNSRecord) (int, error) {
logrus.Infof("cloudflare: CreateDNSRecord: %s", hostname)
payload.Name = hostname + "." + r.domainName
logrus.Debugf("cloudflare: CreateDNSRecord - payload: %s", payload)
resp, err := r.api.CreateDNSRecord(r.ctx, r.domainId, payload)
logrus.Debugf("%+v", resp)
if err != nil {
return -1, err
}
if !resp.Success {
return -1, errors.New("cannot create record")
}
return resp.Count, nil
}
func (r *repository) DeleteDNSRecord(recordID string) error {
logrus.Debugf("cloudflare: DeleteDNSRecord - %s", recordID)
return r.api.DeleteDNSRecord(r.ctx, r.domainId, recordID)
}
func (r *repository) UpdateDNSRecord(hostname string, recordID string, payload cloudflare.DNSRecord) error {
logrus.Debugf("cloudflare: UpdateeDNSRecord - %s", recordID)
return r.api.UpdateDNSRecord(r.ctx, r.domainId, recordID, payload)
}

136
cloudflare/service.go Normal file
View File

@ -0,0 +1,136 @@
package cloudflare
import (
"errors"
"fmt"
"infra-sshfp-cf/sshfp"
"github.com/cloudflare/cloudflare-go"
"github.com/sirupsen/logrus"
)
type service struct {
r Repository
}
func NewService(r Repository) Service {
logrus.Debug("cloudflare: Creating Service")
return &service{r: r}
}
func (s *service) FindHostByName(hostname string) (bool, error) {
logrus.Debugf("cloudflare: FindHostByName %s", hostname)
recs, err := s.r.FindRecords(hostname, "A")
if err != nil {
logrus.Errorf("CF: %s", err)
return false, err
}
if len(recs) > 0 {
logrus.Infof("FindHostByName: Host found: %s", hostname)
logrus.Debugf("%+v", recs)
return true, nil
}
logrus.Infof("FindHostByName: Host not found: %s", hostname)
return false, nil
}
func (s *service) GetSSHFPRecordsForHost(hostname string) ([]*sshfp.SSHFPRecord, error) {
logrus.Debugf("cloudflare: GetSSHFPRecordsForHost %s", hostname)
recs, err := s.r.FindRecords(hostname, "SSHFP")
if err != nil {
logrus.Errorf("CF: %s", err)
return nil, err
}
logrus.Debugf("%+v", recs)
output := make([]*sshfp.SSHFPRecord, 0)
for _, v := range recs {
data, ok := v.Data.(map[string]interface{})
/*
data["type"] - float64
data["fingerprint"] - string
data["algorithm"] - float64
*/
if !ok {
return nil, errors.New("cannot parse data")
}
output = append(output, &sshfp.SSHFPRecord{RecordID: v.ID, Algorithm: fmt.Sprintf("%.f", data["algorithm"]), Type: fmt.Sprintf("%.f", data["type"]), Fingerprint: data["fingerprint"].(string)})
}
return output, nil
}
func (s *service) CreateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) (int, error) {
logrus.Infof("cloudflare: CreateSSHFPRecord: %+v", record)
payload := s.preparePayloadFromSSHRecord(record)
return s.r.CreateDNSRecord(hostname, "SSHFP", payload)
}
func (s *service) DeleteSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error {
logrus.Infof("cloudflare: DeleteDNSRecord: %+v", record)
//In case we have record ID we can just delete the record.
if record.RecordID != "" {
return s.r.DeleteDNSRecord(record.RecordID)
}
recs, err := s.GetSSHFPRecordsForHost(hostname)
if err != nil {
return err
}
//Comparing fingerprints should be more than sufficient
for _, rec := range recs {
if rec.Fingerprint == record.Fingerprint {
return s.r.DeleteDNSRecord(rec.RecordID)
}
}
return errors.New("record not found")
}
func (s *service) ApplyConfigPlan(configPlan sshfp.ConfigPlan) (int, error) {
var item int = 0
for _, v := range configPlan {
switch v.Operation {
case sshfp.CREATE:
_, err := s.CreateSSHFPRecord(v.Hostname, *v.Record)
if err != nil {
return item, err
}
case sshfp.DELETE:
err := s.DeleteSSHFPRecord(v.Hostname, *v.Record)
if err != nil {
return item, err
}
case sshfp.UPDATE:
err := s.UpdateSSHFPRecord(v.Hostname, *v.Record)
if err != nil {
return item, err
}
}
item++
}
return item, nil
}
func (s *service) UpdateSSHFPRecord(hostname string, record sshfp.SSHFPRecord) error {
logrus.Infof("UpdateSSHFPRecord: %+v", record)
payload := s.preparePayloadFromSSHRecord(record)
return s.r.UpdateDNSRecord(hostname, record.RecordID, payload)
}
func (s *service) preparePayloadFromSSHRecord(record sshfp.SSHFPRecord) cloudflare.DNSRecord {
data := make(map[string]string)
data["algorithm"] = record.Algorithm
data["type"] = record.Type
data["fingerprint"] = record.Fingerprint
return cloudflare.DNSRecord{Type: "SSHFP", Data: data, Content: fmt.Sprintf("%s %s %s", record.Algorithm, record.Type, record.Fingerprint)}
}

7
config/entity.go Normal file
View File

@ -0,0 +1,7 @@
package config
type Config struct {
ConsulToken string `json:"consulKey"`
CloudflareToken string `json:"cloudflareKey"`
DomainName string `json:"domain"`
}

11
config/interface.go Normal file
View File

@ -0,0 +1,11 @@
package config
type Repository interface {
LoadFile(fileName string) error
SaveFile(fileName string) error
GetConfig() *Config
}
type Service interface {
LoadConfig(fileName string) (*Config, error)
}

47
config/repository.go Normal file
View File

@ -0,0 +1,47 @@
package config
import (
"encoding/json"
"io/ioutil"
"github.com/sirupsen/logrus"
)
type repository struct {
data Config
}
func NewFileRepository() Repository {
return &repository{data: Config{}}
}
func (r *repository) LoadFile(fileName string) error {
logrus.Infof("config: LoadFile %s", fileName)
content, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
err = json.Unmarshal(content, &r.data)
if err != nil {
return err
}
return nil
}
func (r *repository) SaveFile(fileName string) error {
logrus.Infof("config: SaveFile %s", fileName)
content, err := json.MarshalIndent(r.data, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(fileName, content, 0644)
if err != nil {
return err
}
return nil
}
func (r *repository) GetConfig() *Config {
return &r.data
}

45
config/service.go Normal file
View File

@ -0,0 +1,45 @@
package config
import (
"errors"
"os"
"github.com/sirupsen/logrus"
)
type service struct {
r Repository
}
func NewService(r Repository) Service {
return &service{r: r}
}
func (s *service) LoadConfig(fileName string) (*Config, error) {
if s.r == nil || fileName == "" {
logrus.Infoln("config: LoadConfig")
cfToken, exists := os.LookupEnv("CF_TOKEN")
if !exists {
return nil, errors.New("cannot find env variable CF_TOKEN")
}
consulToken, exists := os.LookupEnv("CONSUL_TOKEN")
if !exists {
return nil, errors.New("cannot find env variable CONSUL_TOKEN")
}
domainaName, exists := os.LookupEnv("DOMAIN_NAME")
if exists {
return nil, errors.New("cannot find env variable DOMAIN_NAME")
}
return &Config{ConsulToken: consulToken, CloudflareToken: cfToken, DomainName: domainaName}, nil
}
err := s.r.LoadFile(fileName)
if err != nil {
return nil, err
}
return s.r.GetConfig(), nil
}

15
consul/entity.go Normal file
View File

@ -0,0 +1,15 @@
package consul
type host struct {
Node struct {
Node string
}
Service struct {
Meta map[string]string
CreateIndex int
ModifyIndex int
}
}
type rawHosts []host
type hostsMap map[string]host

14
consul/interface.go Normal file
View File

@ -0,0 +1,14 @@
package consul
type Repository interface {
GetData() error
ParseData() (hostsMap, error)
}
type Service interface {
LoadData() error
GetHostnames() []string
GetModifiedIndex(hostname string) int
GetCreateIndex(hostname string) int
GetMetaData(hostname string) map[string]string
}

52
consul/service.go Normal file
View File

@ -0,0 +1,52 @@
package consul
import (
"github.com/sirupsen/logrus"
)
type service struct {
r Repository
hostsMap hostsMap
}
//Generates new service instance based on the repo
func NewService(repository Repository) Service {
logrus.Debug("consul: NewService")
return &service{r: repository}
}
//Function calling underlying repository, retreive data and store as interface map
func (s *service) LoadData() error {
logrus.Debug("consul: LoadData")
err := s.r.GetData()
if err != nil {
return err
}
s.hostsMap, err = s.r.ParseData()
logrus.Debugf("%+v", s.hostsMap)
return err
}
func (s *service) GetHostnames() []string {
logrus.Debug("consul: GetHostnames")
hostnames := make([]string, 0)
for k := range s.hostsMap {
hostnames = append(hostnames, k)
}
return hostnames
}
func (s *service) GetModifiedIndex(hostname string) int {
return s.hostsMap[hostname].Service.ModifyIndex
}
func (s *service) GetCreateIndex(hostname string) int {
return s.hostsMap[hostname].Service.CreateIndex
}
func (s *service) GetMetaData(hostname string) map[string]string {
return s.hostsMap[hostname].Service.Meta
}

50
consul/stdinRepository.go Normal file
View File

@ -0,0 +1,50 @@
package consul
import (
"encoding/json"
"io"
"os"
"github.com/sirupsen/logrus"
)
type repo struct {
rawData []byte
}
//Create new file repository satisfying Repository interface
func NewStdinRepository() Repository {
logrus.Debug("consul: Creating stdin reader")
return &repo{}
}
//GetData - Load data from stdin and store in the memory
func (r *repo) GetData() error {
var err error
logrus.Debug("consul: GetData: Opening stdin")
rawData, err := io.ReadAll(os.Stdin)
if err != nil {
logrus.Fatal(err)
}
r.rawData = rawData
return err
}
//ParseData - Parse loaded data and return as json.
func (r *repo) ParseData() (hostsMap, error) {
logrus.Debugf("consul: ParseData: Parsing Data")
var hosts rawHosts
if err := json.Unmarshal(r.rawData, &hosts); err != nil {
return nil, err
}
hostsMap := make(hostsMap)
for _, v := range hosts {
hostsMap[v.Node.Node] = v
}
return hostsMap, nil
}

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module infra-sshfp-cf
go 1.18
require (
github.com/cloudflare/cloudflare-go v0.41.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/mattn/go-sqlite3 v1.14.13 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
)

26
go.sum Normal file
View File

@ -0,0 +1,26 @@
github.com/cloudflare/cloudflare-go v0.40.0 h1:OjW+SYY7+NVSTj+/6kORqvu33LH7uZ0hUd/0qOqucxU=
github.com/cloudflare/cloudflare-go v0.40.0/go.mod h1:MmAqiRfD8rjKEuUe4MYNHfHjYhFWfW7PNe12CCQWqPY=
github.com/cloudflare/cloudflare-go v0.41.0 h1:4HiWuBpBj1fMiyWDfIlLGxnuPuEbkLi+SZWq9tgCFfc=
github.com/cloudflare/cloudflare-go v0.41.0/go.mod h1:o0jm+vdFrhwy7GOT3PB/71JQ6kElUQcifPc2Z9KTxeE=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

84
main.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"infra-sshfp-cf/cloudflare"
"infra-sshfp-cf/config"
"infra-sshfp-cf/consul"
"infra-sshfp-cf/sshfp"
"infra-sshfp-cf/statestore"
"github.com/sirupsen/logrus"
)
func main() {
//Debug loglevel
logrus.SetLevel(logrus.InfoLevel)
//Create configuration components
cfgService := config.NewService(config.NewFileRepository())
config, err := cfgService.LoadConfig("testcfg")
if err != nil {
logrus.Fatal(err)
}
//Create cloudflare components
cloudflare := cloudflare.NewService(cloudflare.NewRepository(config.CloudflareToken, config.DomainName))
//Create STDIN Listener and start listening on (blocking operation)
consul := consul.NewService(consul.NewStdinRepository())
err = consul.LoadData()
//Code below is executed upon data receipt
if err != nil {
logrus.Fatal(err)
}
hosts := consul.GetHostnames()
//Open statestore
statestore := statestore.NewService(statestore.NewMapRepository("statestore.json"))
//Iterate over hosts and check modify indexes
for _, hostname := range hosts {
modifiedIndex := consul.GetModifiedIndex(hostname)
modified, _ := statestore.CheckIfModified(hostname, modifiedIndex)
if modified {
//Check if hosts has A record in CF - if not ignore
exists, err := cloudflare.FindHostByName(hostname)
if err != nil || !exists {
continue
}
//Generate DNS records based on metadata
sshfp := sshfp.NewService()
consulKeys := sshfp.ParseConsulSSHRecords(consul.GetMetaData(hostname))
//GetCurrent configuration
cloudflareKeys, err := cloudflare.GetSSHFPRecordsForHost(hostname)
if err != nil {
logrus.Error(err)
}
configPlan := sshfp.PrepareConfiguration(hostname, cloudflareKeys, consulKeys)
//ConfigPlan is empty, but host was flagged as modified. It's very likely that is a new host, or db is corrupted
if len(configPlan) == 0 {
statestore.SaveState(hostname, modifiedIndex)
}
if len(configPlan) > 0 {
sshfp.PrintConfigPlan(configPlan)
itemsApplied, err := cloudflare.ApplyConfigPlan(configPlan)
if err != nil {
logrus.Error(err)
}
if itemsApplied == len(configPlan) {
statestore.SaveState(hostname, modifiedIndex)
}
}
}
}
}

27
sshfp/entity.go Normal file
View File

@ -0,0 +1,27 @@
package sshfp
type SSHFPRecord struct {
Algorithm string
Type string
Fingerprint string
RecordID string
}
type AlgorithmTypes map[string]string
type HashTypes map[string]string
type ConfigPlanElement struct {
Hostname string
Operation Operation
Record *SSHFPRecord
}
type ConfigPlan []ConfigPlanElement
type Operation int
const (
UPDATE Operation = iota
CREATE
DELETE
)

8
sshfp/interface.go Normal file
View File

@ -0,0 +1,8 @@
package sshfp
type Service interface {
ParseConsulSSHRecord(key string, value string) (*SSHFPRecord, error)
ParseConsulSSHRecords(records map[string]string) []*SSHFPRecord
PrepareConfiguration(hostname string, current []*SSHFPRecord, new []*SSHFPRecord) ConfigPlan
PrintConfigPlan(configPlan ConfigPlan)
}

176
sshfp/service.go Normal file
View File

@ -0,0 +1,176 @@
package sshfp
import (
"errors"
"strings"
"github.com/sirupsen/logrus"
)
type service struct {
Algorithms AlgorithmTypes
Hashes HashTypes
}
func NewService() Service {
//Based on: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
hashes := make(HashTypes)
hashes["sha1"] = "1"
hashes["sha256"] = "2"
algorithms := make(AlgorithmTypes)
algorithms["rsa"] = "1"
algorithms["dsa"] = "2"
algorithms["ecdsa"] = "3"
algorithms["ed25519"] = "4"
algorithms["ed449"] = "6"
return &service{Algorithms: algorithms, Hashes: hashes}
}
func (s *service) ParseConsulSSHRecord(key string, value string) (*SSHFPRecord, error) {
logrus.Debugf("SSHFP: ParseConsulSSHRecord: %s %s", key, value)
output := &SSHFPRecord{}
key = strings.ToLower(key)
value = strings.ToLower(value)
output.Fingerprint = value
splittedKey := strings.Split(key, "-")
if len(splittedKey) < 3 {
return nil, errors.New("incorrect format")
}
//Assumption: Last field is hash type
output.Type = s.Hashes[splittedKey[len(splittedKey)-1]]
//Check first field - for "ecdsa" we can return value, for "ssh", we have to check second field.
switch splittedKey[0] {
case "ecdsa":
output.Algorithm = s.Algorithms[splittedKey[0]]
case "ssh":
output.Algorithm = s.Algorithms[splittedKey[1]]
}
if output.Type == "" || output.Algorithm == "" || output.Fingerprint == "" {
return nil, errors.New("cannot parse record")
}
return output, nil
}
func (s *service) ParseConsulSSHRecords(records map[string]string) []*SSHFPRecord {
output := make([]*SSHFPRecord, 0)
for k, v := range records {
sshRecord, err := s.ParseConsulSSHRecord(k, v)
if err != nil {
continue
}
output = append(output, sshRecord)
}
return output
}
func (s *service) PrepareConfiguration(hostname string, current []*SSHFPRecord, new []*SSHFPRecord) ConfigPlan {
logrus.Debug("SSHFP: PrepareConfiguration")
/* Config plan have to cover the following scenarios:
0. Both configs are empty - do nothing.
1. New config is empty - DELETE everything
2. Old config is empty - CREATE everything
3. Record in new config doesn't exist in old config - CREATE record
4. Record in new config has different fingerprint than the old one - UPDATE and remove from oldMap
5. Record in new config and record in old config are the same - just remove from oldMap to avoid unwanted removal
Records left in oldMap should be qualified for removal
*/
configPlan := make(ConfigPlan, 0)
// Scenario 0
if len(current) == 0 && len(new) == 0 {
logrus.Debug("Both configs are empty - do nothing")
return configPlan
}
//Scenario 1
if len(current) > 0 && len(new) == 0 {
logrus.Debug("New config is empty - remove config")
for _, v := range current {
configPlan = append(configPlan, ConfigPlanElement{Operation: DELETE, Record: v, Hostname: hostname})
}
return configPlan
}
//Scenario 2
if len(current) == 0 && len(new) > 0 {
logrus.Debug("Old config is empty - create config")
for _, v := range new {
configPlan = append(configPlan, ConfigPlanElement{Operation: CREATE, Record: v, Hostname: hostname})
}
return configPlan
}
//To handle scenarios 3-5 we have to create temporary maps with string "<algorithm><type>" as key.
//Assumption: Pair algorithm+hash type is unique
oldMap := make(map[string]*SSHFPRecord)
newMap := make(map[string]*SSHFPRecord)
//Create temporary maps for better searching
for _, v := range current {
oldMap[v.Algorithm+v.Type] = v
}
for _, v := range new {
newMap[v.Algorithm+v.Type] = v
}
for k := range newMap {
//Scenario 3
if _, ok := oldMap[k]; !ok {
logrus.Debugf("Record not found in current config: %s", k)
configPlan = append(configPlan, ConfigPlanElement{Operation: CREATE, Record: newMap[k], Hostname: hostname})
continue
}
//Scenario 4
if oldMap[k].Fingerprint != newMap[k].Fingerprint {
logrus.Debugf("Updating record in current config: %s", k)
newMap[k].RecordID = oldMap[k].RecordID
configPlan = append(configPlan, ConfigPlanElement{Operation: UPDATE, Record: newMap[k], Hostname: hostname})
delete(oldMap, k)
continue
}
//Scenario 5
if oldMap[k].Fingerprint == newMap[k].Fingerprint {
delete(oldMap, k)
continue
}
}
//Cleanup
for _, v := range oldMap {
configPlan = append(configPlan, ConfigPlanElement{Operation: DELETE, Record: v, Hostname: hostname})
}
return configPlan
}
func (s *service) PrintConfigPlan(configPlan ConfigPlan) {
logrus.Debug("SSHFP: PrintConfigPlan")
logrus.Infof("Config Plan:")
for _, v := range configPlan {
logrus.Infof("Hostname: %s", v.Hostname)
logrus.Infof("Operation: %v", v.Operation)
logrus.Infof("Record: %+v", v.Record)
logrus.Infof("---")
}
}

11
statestore/interface.go Normal file
View File

@ -0,0 +1,11 @@
package statestore
type Repository interface {
GetModifyIndex(hostname string) (int, error)
SetModifyIndex(hostname string, index int) error
}
type Service interface {
CheckIfModified(hostname string, index int) (bool, error)
SaveState(hostname string, index int) error
}

View File

@ -0,0 +1,73 @@
package statestore
import (
"encoding/json"
"io/ioutil"
"github.com/sirupsen/logrus"
)
type mapRepository struct {
db map[string]int
filename string
}
func NewMapRepository(filename string) Repository {
repo := new(mapRepository)
repo.filename = filename
err := repo.openDatabase()
if err == nil {
return repo
}
db := make(map[string]int)
repo.db = db
err = repo.saveDatabase()
if err != nil {
logrus.Error("mapRepository: cannot save database %s", filename)
}
return repo
}
func (r *mapRepository) GetModifyIndex(hostname string) (int, error) {
if value, ok := r.db[hostname]; ok {
return value, nil
}
return -1, nil
}
func (r *mapRepository) SetModifyIndex(hostname string, index int) error {
r.db[hostname] = index
return r.saveDatabase()
}
func (r *mapRepository) openDatabase() error {
logrus.Infof("mapRepository: openDatabase %s", r.filename)
content, err := ioutil.ReadFile(r.filename)
if err != nil {
return err
}
err = json.Unmarshal(content, &r.db)
if err != nil {
return err
}
return nil
}
func (r *mapRepository) saveDatabase() error {
logrus.Infof("mapRepository: saveDatabase %s", r.filename)
content, err := json.MarshalIndent(&r.db, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(r.filename, content, 0644)
if err != nil {
return err
}
return nil
}

32
statestore/service.go Normal file
View File

@ -0,0 +1,32 @@
package statestore
import "github.com/sirupsen/logrus"
type service struct {
r Repository
}
func NewService(r Repository) Service {
return &service{r: r}
}
// CheckIfModified - returns if host is modified or not. Err can be ignored or not depends on repository
func (s *service) CheckIfModified(hostname string, index int) (bool, error) {
logrus.Debugf("statestore: CheckIfModified %s", hostname)
indexDb, err := s.r.GetModifyIndex(hostname)
if err != nil {
return true, err
}
if indexDb == index {
return false, nil
}
return true, err
}
func (s *service) SaveState(hostname string, index int) error {
return s.r.SetModifyIndex(hostname, index)
}