mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-11 18:08:57 +08:00
268 lines
6.7 KiB
Go
268 lines
6.7 KiB
Go
package ovh
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
|
"github.com/ovh/go-ovh/ovh"
|
|
)
|
|
|
|
type ovhProvider struct {
|
|
client *ovh.Client
|
|
zones map[string]bool
|
|
}
|
|
|
|
var features = providers.DocumentationNotes{
|
|
// The default for unlisted capabilities is 'Cannot'.
|
|
// See providers/capabilities.go for the entire list of capabilities.
|
|
providers.CanGetZones: providers.Can(),
|
|
providers.CanConcur: providers.Cannot(),
|
|
providers.CanUseAlias: providers.Cannot(),
|
|
providers.CanUseCAA: providers.Can(),
|
|
providers.CanUseLOC: providers.Unimplemented(),
|
|
providers.CanUsePTR: providers.Cannot(),
|
|
providers.CanUseSRV: providers.Can(),
|
|
providers.CanUseSSHFP: providers.Can(),
|
|
providers.CanUseTLSA: providers.Can(),
|
|
providers.DocCreateDomains: providers.Cannot("New domains require registration"),
|
|
providers.DocDualHost: providers.Can(),
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
}
|
|
|
|
func newOVH(m map[string]string, _ json.RawMessage) (*ovhProvider, error) {
|
|
appKey, appSecretKey, consumerKey := m["app-key"], m["app-secret-key"], m["consumer-key"]
|
|
|
|
c, err := ovh.NewClient(getOVHEndpoint(m), appKey, appSecretKey, consumerKey)
|
|
if c == nil {
|
|
return nil, err
|
|
}
|
|
|
|
ovh := &ovhProvider{client: c}
|
|
if err := ovh.fetchZones(); err != nil {
|
|
return nil, err
|
|
}
|
|
return ovh, nil
|
|
}
|
|
|
|
func getOVHEndpoint(params map[string]string) string {
|
|
if ep, ok := params["endpoint"]; ok && ep != "" {
|
|
switch strings.ToLower(ep) {
|
|
case "eu":
|
|
return ovh.OvhEU
|
|
case "ca":
|
|
return ovh.OvhCA
|
|
case "us":
|
|
return ovh.OvhUS
|
|
default:
|
|
return ep
|
|
}
|
|
}
|
|
return ovh.OvhEU
|
|
}
|
|
|
|
func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
return newOVH(conf, metadata)
|
|
}
|
|
|
|
func newReg(conf map[string]string) (providers.Registrar, error) {
|
|
return newOVH(conf, nil)
|
|
}
|
|
|
|
func init() {
|
|
fns := providers.DspFuncs{
|
|
Initializer: newDsp,
|
|
RecordAuditor: AuditRecords,
|
|
}
|
|
providers.RegisterRegistrarType("OVH", newReg)
|
|
providers.RegisterDomainServiceProviderType("OVH", fns, features)
|
|
}
|
|
|
|
func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
_, ok := c.zones[domain]
|
|
if !ok {
|
|
return nil, fmt.Errorf("'%s' not a zone in ovh account", domain)
|
|
}
|
|
|
|
ns, err := c.fetchNS(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return models.ToNameservers(ns)
|
|
}
|
|
|
|
type errNoExist struct {
|
|
domain string
|
|
}
|
|
|
|
func (e errNoExist) Error() string {
|
|
return fmt.Sprintf("Domain %s not found in your ovh account", e.domain)
|
|
}
|
|
|
|
// ListZones lists the zones on this account.
|
|
func (c *ovhProvider) ListZones() (zones []string, err error) {
|
|
for zone := range c.zones {
|
|
zones = append(zones, zone)
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
func (c *ovhProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
|
if !c.zones[domain] {
|
|
return nil, errNoExist{domain}
|
|
}
|
|
|
|
records, err := c.fetchRecords(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var actual models.Records
|
|
for _, r := range records {
|
|
rec, err := nativeToRecord(r, domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rec != nil {
|
|
actual = append(actual, rec)
|
|
}
|
|
}
|
|
|
|
return actual, nil
|
|
}
|
|
|
|
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
|
func (c *ovhProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
|
|
|
|
corrections, err := c.getDiff2DomainCorrections(dc, actual)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only refresh zone if there's a real modification
|
|
reportOnlyCorrections := true
|
|
for _, c := range corrections {
|
|
if c.F != nil {
|
|
reportOnlyCorrections = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if !reportOnlyCorrections {
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: "REFRESH zone " + dc.Name,
|
|
F: func() error {
|
|
return c.refreshZone(dc.Name)
|
|
},
|
|
})
|
|
}
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
func (c *ovhProvider) getDiff2DomainCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
|
|
var corrections []*models.Correction
|
|
instructions, err := diff2.ByRecord(actual, dc, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, inst := range instructions {
|
|
switch inst.Type {
|
|
case diff2.REPORT:
|
|
corrections = append(corrections, &models.Correction{Msg: inst.MsgsJoined})
|
|
case diff2.CHANGE:
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: inst.Msgs[0],
|
|
F: c.updateRecordFunc(inst.Old[0].Original.(*Record), inst.New[0], dc.Name),
|
|
})
|
|
case diff2.CREATE:
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: inst.Msgs[0],
|
|
F: c.createRecordFunc(inst.New[0], dc.Name),
|
|
})
|
|
case diff2.DELETE:
|
|
rec := inst.Old[0].Original.(*Record)
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: inst.Msgs[0],
|
|
F: c.deleteRecordFunc(rec.ID, dc.Name),
|
|
})
|
|
default:
|
|
panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
|
|
}
|
|
}
|
|
return corrections, nil
|
|
}
|
|
|
|
func nativeToRecord(r *Record, origin string) (*models.RecordConfig, error) {
|
|
if r.FieldType == "SOA" {
|
|
return nil, nil
|
|
}
|
|
rec := &models.RecordConfig{
|
|
TTL: uint32(r.TTL),
|
|
Original: r,
|
|
}
|
|
|
|
rtype := r.FieldType
|
|
|
|
// ovh uses a custom type for SPF, DKIM and DMARC
|
|
if rtype == "SPF" || rtype == "DKIM" || rtype == "DMARC" {
|
|
rtype = "TXT"
|
|
}
|
|
|
|
rec.SetLabel(r.SubDomain, origin)
|
|
if err := rec.PopulateFromString(rtype, r.Target, origin); err != nil {
|
|
return nil, fmt.Errorf("unparsable record received from ovh: %w", err)
|
|
}
|
|
|
|
// ovh default is 3600
|
|
if rec.TTL == 0 {
|
|
rec.TTL = 3600
|
|
}
|
|
|
|
return rec, nil
|
|
}
|
|
|
|
func (c *ovhProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
|
|
// get the actual in-use nameservers
|
|
actualNs, err := c.fetchRegistrarNS(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get the actual used ones + the configured one through dnscontrol
|
|
expectedNs := []string{}
|
|
for _, d := range dc.Nameservers {
|
|
expectedNs = append(expectedNs, d.Name)
|
|
}
|
|
|
|
sort.Strings(actualNs)
|
|
actual := strings.Join(actualNs, ",")
|
|
|
|
sort.Strings(expectedNs)
|
|
expected := strings.Join(expectedNs, ",")
|
|
|
|
// check if we need to change something
|
|
if actual != expected {
|
|
return []*models.Correction{
|
|
{
|
|
Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", actual, expected),
|
|
F: func() error {
|
|
err := c.updateNS(dc.Name, expectedNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}},
|
|
}, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|