2017-11-11 03:02:34 +08:00
|
|
|
package ovh
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2018-01-05 08:19:35 +08:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
|
2023-05-21 01:21:45 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
2019-05-21 03:39:19 +08:00
|
|
|
"github.com/ovh/go-ovh/ovh"
|
2017-11-11 03:02:34 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
type ovhProvider struct {
|
|
|
|
client *ovh.Client
|
|
|
|
zones map[string]bool
|
|
|
|
}
|
|
|
|
|
2018-01-05 08:19:35 +08:00
|
|
|
var features = providers.DocumentationNotes{
|
2022-03-03 00:19:15 +08:00
|
|
|
providers.CanGetZones: providers.Can(),
|
2017-11-11 03:02:34 +08:00
|
|
|
providers.CanUseAlias: providers.Cannot(),
|
2019-05-18 23:11:04 +08:00
|
|
|
providers.CanUseCAA: providers.Can(),
|
2023-03-17 07:59:44 +08:00
|
|
|
providers.CanUseLOC: providers.Unimplemented(),
|
2017-11-11 03:02:34 +08:00
|
|
|
providers.CanUsePTR: providers.Cannot(),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.CanUseSRV: providers.Can(),
|
2019-05-20 00:19:55 +08:00
|
|
|
providers.CanUseSSHFP: providers.Can(),
|
2022-03-03 00:19:15 +08:00
|
|
|
providers.CanUseTLSA: providers.Can(),
|
2018-01-05 08:19:35 +08:00
|
|
|
providers.DocCreateDomains: providers.Cannot("New domains require registration"),
|
|
|
|
providers.DocDualHost: providers.Can(),
|
|
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error) {
|
|
|
|
appKey, appSecretKey, consumerKey := m["app-key"], m["app-secret-key"], m["consumer-key"]
|
|
|
|
|
2023-11-28 07:18:24 +08:00
|
|
|
c, err := ovh.NewClient(getOVHEndpoint(m), appKey, appSecretKey, consumerKey)
|
2019-05-21 03:39:19 +08:00
|
|
|
if c == nil {
|
2017-11-11 03:02:34 +08:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-10-17 03:42:54 +08:00
|
|
|
ovh := &ovhProvider{client: c}
|
|
|
|
if err := ovh.fetchZones(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return ovh, nil
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
|
2023-11-28 07:18:24 +08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-11-11 03:02:34 +08:00
|
|
|
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() {
|
2021-03-08 02:19:22 +08:00
|
|
|
fns := providers.DspFuncs{
|
2021-05-05 02:15:31 +08:00
|
|
|
Initializer: newDsp,
|
2021-03-09 09:14:30 +08:00
|
|
|
RecordAuditor: AuditRecords,
|
2021-03-08 02:19:22 +08:00
|
|
|
}
|
2017-11-11 03:02:34 +08:00
|
|
|
providers.RegisterRegistrarType("OVH", newReg)
|
2021-03-08 02:19:22 +08:00
|
|
|
providers.RegisterDomainServiceProviderType("OVH", fns, features)
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
|
|
_, ok := c.zones[domain]
|
|
|
|
if !ok {
|
2020-02-18 21:59:18 +08:00
|
|
|
return nil, fmt.Errorf("'%s' not a zone in ovh account", domain)
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
|
2021-12-21 00:20:17 +08:00
|
|
|
ns, err := c.fetchNS(domain)
|
2017-11-11 03:02:34 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-03-04 05:42:47 +08:00
|
|
|
return models.ToNameservers(ns)
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
type errNoExist struct {
|
|
|
|
domain string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e errNoExist) Error() string {
|
|
|
|
return fmt.Sprintf("Domain %s not found in your ovh account", e.domain)
|
|
|
|
}
|
|
|
|
|
2021-04-03 22:31:25 +08:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-02-18 21:59:18 +08:00
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
2023-05-03 01:04:59 +08:00
|
|
|
func (c *ovhProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
2020-02-29 00:14:02 +08:00
|
|
|
if !c.zones[domain] {
|
|
|
|
return nil, errNoExist{domain}
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
|
2020-02-29 00:14:02 +08:00
|
|
|
records, err := c.fetchRecords(domain)
|
2017-11-11 03:02:34 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-02-29 00:14:02 +08:00
|
|
|
var actual models.Records
|
2017-11-11 03:02:34 +08:00
|
|
|
for _, r := range records {
|
2020-11-14 05:32:40 +08:00
|
|
|
rec, err := nativeToRecord(r, domain)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-02-16 01:02:50 +08:00
|
|
|
if rec != nil {
|
|
|
|
actual = append(actual, rec)
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
}
|
2023-04-15 03:22:23 +08:00
|
|
|
|
2020-02-29 00:14:02 +08:00
|
|
|
return actual, nil
|
|
|
|
}
|
|
|
|
|
2023-04-15 03:22:23 +08:00
|
|
|
// 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) {
|
2021-10-17 00:45:06 +08:00
|
|
|
|
2023-10-23 01:56:13 +08:00
|
|
|
corrections, err := c.getDiff2DomainCorrections(dc, actual)
|
2022-12-19 23:21:54 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-11-11 03:02:34 +08:00
|
|
|
|
2023-11-28 07:18:24 +08:00
|
|
|
// Only refresh zone if there's a real modification
|
|
|
|
reportOnlyCorrections := true
|
|
|
|
for _, c := range corrections {
|
|
|
|
if c.F != nil {
|
|
|
|
reportOnlyCorrections = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reportOnlyCorrections {
|
2022-12-19 23:21:54 +08:00
|
|
|
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 {
|
2023-02-01 20:27:00 +08:00
|
|
|
case diff2.REPORT:
|
|
|
|
corrections = append(corrections, &models.Correction{Msg: inst.MsgsJoined})
|
2022-12-19 23:21:54 +08:00
|
|
|
case diff2.CHANGE:
|
2022-12-12 04:02:58 +08:00
|
|
|
corrections = append(corrections, &models.Correction{
|
2022-12-19 23:21:54 +08:00
|
|
|
Msg: inst.Msgs[0],
|
|
|
|
F: c.updateRecordFunc(inst.Old[0].Original.(*Record), inst.New[0], dc.Name),
|
2022-12-12 04:02:58 +08:00
|
|
|
})
|
2022-12-19 23:21:54 +08:00
|
|
|
case diff2.CREATE:
|
2022-12-12 04:02:58 +08:00
|
|
|
corrections = append(corrections, &models.Correction{
|
2022-12-19 23:21:54 +08:00
|
|
|
Msg: inst.Msgs[0],
|
|
|
|
F: c.createRecordFunc(inst.New[0], dc.Name),
|
2022-12-12 04:02:58 +08:00
|
|
|
})
|
2022-12-19 23:21:54 +08:00
|
|
|
case diff2.DELETE:
|
|
|
|
rec := inst.Old[0].Original.(*Record)
|
2022-12-12 04:02:58 +08:00
|
|
|
corrections = append(corrections, &models.Correction{
|
2022-12-19 23:21:54 +08:00
|
|
|
Msg: inst.Msgs[0],
|
|
|
|
F: c.deleteRecordFunc(rec.ID, dc.Name),
|
2022-12-12 04:02:58 +08:00
|
|
|
})
|
2023-02-01 20:27:00 +08:00
|
|
|
default:
|
|
|
|
panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
|
2022-12-12 04:02:58 +08:00
|
|
|
}
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
return corrections, nil
|
|
|
|
}
|
|
|
|
|
2020-11-14 05:32:40 +08:00
|
|
|
func nativeToRecord(r *Record, origin string) (*models.RecordConfig, error) {
|
2018-02-16 01:02:50 +08:00
|
|
|
if r.FieldType == "SOA" {
|
2020-11-14 05:32:40 +08:00
|
|
|
return nil, nil
|
2018-02-16 01:02:50 +08:00
|
|
|
}
|
|
|
|
rec := &models.RecordConfig{
|
|
|
|
TTL: uint32(r.TTL),
|
|
|
|
Original: r,
|
|
|
|
}
|
2018-03-22 02:49:54 +08:00
|
|
|
|
2018-02-16 01:02:50 +08:00
|
|
|
rtype := r.FieldType
|
|
|
|
|
2023-08-08 23:55:16 +08:00
|
|
|
// ovh uses a custom type for SPF, DKIM and DMARC
|
|
|
|
if rtype == "SPF" || rtype == "DKIM" || rtype == "DMARC" {
|
2018-03-22 02:49:54 +08:00
|
|
|
rtype = "TXT"
|
|
|
|
}
|
|
|
|
|
|
|
|
rec.SetLabel(r.SubDomain, origin)
|
|
|
|
if err := rec.PopulateFromString(rtype, r.Target, origin); err != nil {
|
2020-11-14 05:32:40 +08:00
|
|
|
return nil, fmt.Errorf("unparsable record received from ovh: %w", err)
|
2018-02-16 01:02:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// ovh default is 3600
|
|
|
|
if rec.TTL == 0 {
|
|
|
|
rec.TTL = 3600
|
|
|
|
}
|
|
|
|
|
2020-11-14 05:32:40 +08:00
|
|
|
return rec, nil
|
2018-02-16 01:02:50 +08:00
|
|
|
}
|
|
|
|
|
2017-11-11 03:02:34 +08:00
|
|
|
func (c *ovhProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
|
|
|
2019-05-23 21:25:26 +08:00
|
|
|
// get the actual in-use nameservers
|
|
|
|
actualNs, err := c.fetchRegistrarNS(dc.Name)
|
2017-11-11 03:02:34 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-05-23 21:25:26 +08:00
|
|
|
// get the actual used ones + the configured one through dnscontrol
|
|
|
|
expectedNs := []string{}
|
2017-11-11 03:02:34 +08:00
|
|
|
for _, d := range dc.Nameservers {
|
2019-05-23 21:25:26 +08:00
|
|
|
expectedNs = append(expectedNs, d.Name)
|
2017-11-11 03:02:34 +08:00
|
|
|
}
|
|
|
|
|
2019-05-23 21:25:26 +08:00
|
|
|
sort.Strings(actualNs)
|
|
|
|
actual := strings.Join(actualNs, ",")
|
|
|
|
|
|
|
|
sort.Strings(expectedNs)
|
|
|
|
expected := strings.Join(expectedNs, ",")
|
|
|
|
|
|
|
|
// check if we need to change something
|
|
|
|
if actual != expected {
|
2017-11-11 03:02:34 +08:00
|
|
|
return []*models.Correction{
|
|
|
|
{
|
2019-05-23 21:25:26 +08:00
|
|
|
Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", actual, expected),
|
2017-11-11 03:02:34 +08:00
|
|
|
F: func() error {
|
2019-05-23 21:25:26 +08:00
|
|
|
err := c.updateNS(dc.Name, expectedNs)
|
2017-11-11 03:02:34 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}},
|
|
|
|
}, nil
|
|
|
|
}
|
2019-05-23 21:25:26 +08:00
|
|
|
|
2017-11-11 03:02:34 +08:00
|
|
|
return nil, nil
|
|
|
|
}
|