mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
* OVH DNS Provider (#143) This adds the OVH Provider along with its documentation. Unfortunately we can't set this DNS provider to support `CanUsePTR`, because OVH only supports setting PTR target on the Arpa zone. * OVH Registrar provider (#143) This implements OVH as a registrar provider. Note that NS modifications are done in a "best effort" mode, as the provider doesn't wait for the modifications to be fully applied (the operation that can take a long time). * Allow support for dual providers scenarios Since OVH released their APIv6, it is now possible to update zone apex NS records, opening the door to complete dual providers scenarii. This change implements apex NS management in an OVH zone.
This commit is contained in:
committed by
Tom Limoncelli
parent
fea1d7afff
commit
e44dde52e2
202
providers/ovh/ovhProvider.go
Normal file
202
providers/ovh/ovhProvider.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package ovh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/providers"
|
||||
"github.com/StackExchange/dnscontrol/providers/diff"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"github.com/xlucas/go-ovh/ovh"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ovhProvider struct {
|
||||
client *ovh.Client
|
||||
zones map[string]bool
|
||||
}
|
||||
|
||||
var docNotes = providers.DocumentationNotes{
|
||||
providers.DocDualHost: providers.Can(),
|
||||
providers.DocCreateDomains: providers.Cannot("New domains require registration"),
|
||||
providers.DocOfficiallySupported: providers.Cannot(),
|
||||
providers.CanUseAlias: providers.Cannot(),
|
||||
providers.CanUseTLSA: providers.Can(),
|
||||
providers.CanUseCAA: providers.Cannot(),
|
||||
providers.CanUsePTR: providers.Cannot(),
|
||||
}
|
||||
|
||||
func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error) {
|
||||
appKey, appSecretKey, consumerKey := m["app-key"], m["app-secret-key"], m["consumer-key"]
|
||||
|
||||
c := ovh.NewClient(ovh.ENDPOINT_EU_OVHCOM, appKey, appSecretKey, consumerKey, false)
|
||||
|
||||
// Check for time lag
|
||||
if err := c.PollTimeshift(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ovhProvider{client: c}, nil
|
||||
}
|
||||
|
||||
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() {
|
||||
providers.RegisterRegistrarType("OVH", newReg)
|
||||
providers.RegisterDomainServiceProviderType("OVH", newDsp, providers.CanUseSRV, providers.CanUseTLSA, docNotes)
|
||||
}
|
||||
|
||||
func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
if err := c.fetchZones(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, ok := c.zones[domain]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s not listed in zones for ovh account", domain)
|
||||
}
|
||||
|
||||
ns, err := c.fetchNS(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.StringsToNameservers(ns), nil
|
||||
}
|
||||
|
||||
type errNoExist struct {
|
||||
domain string
|
||||
}
|
||||
|
||||
func (e errNoExist) Error() string {
|
||||
return fmt.Sprintf("Domain %s not found in your ovh account", e.domain)
|
||||
}
|
||||
|
||||
func (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
dc.CombineMXs()
|
||||
|
||||
if !c.zones[dc.Name] {
|
||||
return nil, errNoExist{dc.Name}
|
||||
}
|
||||
|
||||
records, err := c.fetchRecords(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var actual []*models.RecordConfig
|
||||
for _, r := range records {
|
||||
if r.FieldType == "SOA" {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.SubDomain == "" {
|
||||
r.SubDomain = "@"
|
||||
}
|
||||
|
||||
// ovh uses a custom type for SPF and DKIM
|
||||
if r.FieldType == "SPF" || r.FieldType == "DKIM" {
|
||||
r.FieldType = "TXT"
|
||||
}
|
||||
|
||||
// ovh default is 3600
|
||||
if r.TTL == 0 {
|
||||
r.TTL = 3600
|
||||
}
|
||||
|
||||
rec := &models.RecordConfig{
|
||||
NameFQDN: dnsutil.AddOrigin(r.SubDomain, dc.Name),
|
||||
Name: r.SubDomain,
|
||||
Type: r.FieldType,
|
||||
Target: r.Target,
|
||||
TTL: uint32(r.TTL),
|
||||
CombinedTarget: true,
|
||||
Original: r,
|
||||
}
|
||||
actual = append(actual, rec)
|
||||
}
|
||||
|
||||
// Normalize
|
||||
models.Downcase(actual)
|
||||
|
||||
differ := diff.New(dc)
|
||||
_, create, delete, modify := differ.IncrementalDiff(actual)
|
||||
|
||||
corrections := []*models.Correction{}
|
||||
|
||||
for _, del := range delete {
|
||||
rec := del.Existing.Original.(*Record)
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: del.String(),
|
||||
F: c.deleteRecordFunc(rec.Id, dc.Name),
|
||||
})
|
||||
}
|
||||
|
||||
for _, cre := range create {
|
||||
rec := cre.Desired
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: cre.String(),
|
||||
F: c.createRecordFunc(rec, dc.Name),
|
||||
})
|
||||
}
|
||||
|
||||
for _, mod := range modify {
|
||||
oldR := mod.Existing.Original.(*Record)
|
||||
newR := mod.Desired
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: mod.String(),
|
||||
F: c.updateRecordFunc(oldR, newR, dc.Name),
|
||||
})
|
||||
}
|
||||
|
||||
if len(corrections) > 0 {
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: "REFRESH zone " + dc.Name,
|
||||
F: func() error {
|
||||
return c.refreshZone(dc.Name)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
func (c *ovhProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
|
||||
ns, err := c.fetchRegistrarNS(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Strings(ns)
|
||||
found := strings.Join(ns, ",")
|
||||
|
||||
desiredNs := []string{}
|
||||
for _, d := range dc.Nameservers {
|
||||
desiredNs = append(desiredNs, d.Name)
|
||||
}
|
||||
sort.Strings(desiredNs)
|
||||
desired := strings.Join(desiredNs, ",")
|
||||
|
||||
if found != desired {
|
||||
return []*models.Correction{
|
||||
{
|
||||
Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", found, desired),
|
||||
F: func() error {
|
||||
err := c.updateNS(dc.Name, desiredNs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
257
providers/ovh/protocol.go
Normal file
257
providers/ovh/protocol.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package ovh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
)
|
||||
|
||||
type Void struct {
|
||||
}
|
||||
|
||||
// fetchDomainList gets list of zones for account
|
||||
func (c *ovhProvider) fetchZones() error {
|
||||
if c.zones != nil {
|
||||
return nil
|
||||
}
|
||||
c.zones = map[string]bool{}
|
||||
|
||||
var response []string
|
||||
|
||||
err := c.client.Call("GET", "/domain/zone", nil, &response)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range response {
|
||||
c.zones[d] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
LastUpdate string `json:"lastUpdate,omitempty"`
|
||||
HasDNSAnycast bool `json:"hasDNSAnycast,omitempty"`
|
||||
NameServers []string `json:"nameServers"`
|
||||
DNSSecSupported bool `json:"dnssecSupported"`
|
||||
}
|
||||
|
||||
// get info about a zone.
|
||||
func (c *ovhProvider) fetchZone(fqdn string) (*Zone, error) {
|
||||
var response Zone
|
||||
|
||||
err := c.client.Call("GET", "/domain/zone/"+fqdn, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Target string `json:"target,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
FieldType string `json:"fieldType,omitempty"`
|
||||
Id int64 `json:"id,omitempty"`
|
||||
SubDomain string `json:"subDomain,omitempty"`
|
||||
}
|
||||
|
||||
type records struct {
|
||||
recordsId []int
|
||||
}
|
||||
|
||||
func (c *ovhProvider) fetchRecords(fqdn string) ([]*Record, error) {
|
||||
var recordIds []int
|
||||
|
||||
err := c.client.Call("GET", "/domain/zone/"+fqdn+"/record", nil, &recordIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records := make([]*Record, len(recordIds))
|
||||
for i, id := range recordIds {
|
||||
r, err := c.fecthRecord(fqdn, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records[i] = r
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (c *ovhProvider) fecthRecord(fqdn string, id int) (*Record, error) {
|
||||
var response Record
|
||||
|
||||
err := c.client.Call("GET", fmt.Sprintf("/domain/zone/%s/record/%d", fqdn, id), nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// Returns a function that can be invoked to delete a record in a zone.
|
||||
func (c *ovhProvider) deleteRecordFunc(id int64, fqdn string) func() error {
|
||||
return func() error {
|
||||
err := c.client.Call("DELETE", fmt.Sprintf("/domain/zone/%s/record/%d", fqdn, id), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function that can be invoked to create a record in a zone.
|
||||
func (c *ovhProvider) createRecordFunc(rc *models.RecordConfig, fqdn string) func() error {
|
||||
return func() error {
|
||||
record := Record{
|
||||
SubDomain: dnsutil.TrimDomainName(rc.NameFQDN, fqdn),
|
||||
FieldType: rc.Type,
|
||||
Target: rc.Content(),
|
||||
TTL: rc.TTL,
|
||||
}
|
||||
if record.SubDomain == "@" {
|
||||
record.SubDomain = ""
|
||||
}
|
||||
var response Record
|
||||
err := c.client.Call("POST", fmt.Sprintf("/domain/zone/%s/record", fqdn), &record, &response)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function that can be invoked to update a record in a zone.
|
||||
func (c *ovhProvider) updateRecordFunc(old *Record, rc *models.RecordConfig, fqdn string) func() error {
|
||||
return func() error {
|
||||
record := Record{
|
||||
SubDomain: dnsutil.TrimDomainName(rc.NameFQDN, fqdn),
|
||||
FieldType: rc.Type,
|
||||
Target: rc.Content(),
|
||||
TTL: rc.TTL,
|
||||
Zone: fqdn,
|
||||
Id: old.Id,
|
||||
}
|
||||
if record.SubDomain == "@" {
|
||||
record.SubDomain = ""
|
||||
}
|
||||
|
||||
return c.client.Call("PUT", fmt.Sprintf("/domain/zone/%s/record/%d", fqdn, old.Id), &record, &Void{})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ovhProvider) refreshZone(fqdn string) error {
|
||||
return c.client.Call("POST", fmt.Sprintf("/domain/zone/%s/refresh", fqdn), nil, &Void{})
|
||||
}
|
||||
|
||||
// fetch the NS OVH attributed to this zone (which is distinct from fetchRealNS which
|
||||
// get the exact NS stored at the registrar
|
||||
func (c *ovhProvider) fetchNS(fqdn string) ([]string, error) {
|
||||
zone, err := c.fetchZone(fqdn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return zone.NameServers, nil
|
||||
}
|
||||
|
||||
type CurrentNameServer struct {
|
||||
ToDelete bool `json:"toDelete,omitempty"`
|
||||
Ip string `json:"ip,omitempty"`
|
||||
IsUsed bool `json:"isUsed,omitempty"`
|
||||
Id int `json:"id,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// Retrieve the NS currently being deployed to the registrar
|
||||
func (c *ovhProvider) fetchRegistrarNS(fqdn string) ([]string, error) {
|
||||
var nameServersId []int
|
||||
err := c.client.Call("GET", "/domain/"+fqdn+"/nameServer", nil, &nameServersId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nameServers []string
|
||||
for _, id := range nameServersId {
|
||||
var ns CurrentNameServer
|
||||
err = c.client.Call("GET", fmt.Sprintf("/domain/%s/nameServer/%d", fqdn, id), nil, &ns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// skip NS that we asked for deletion
|
||||
if ns.ToDelete {
|
||||
continue
|
||||
}
|
||||
nameServers = append(nameServers, ns.Host)
|
||||
}
|
||||
|
||||
return nameServers, nil
|
||||
}
|
||||
|
||||
type DomainNS struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Ip string `json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateNS struct {
|
||||
NameServers []DomainNS `json:"nameServers"`
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Function string `json:"function,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
CanAccelerate bool `json:"canAccelerate,omitempty"`
|
||||
LastUpdate string `json:"lastUpdate,omitempty"`
|
||||
CreationDate string `json:"creationDate,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
TodoDate string `json:"todoDate,omitempty"`
|
||||
Id int64 `json:"id,omitempty"`
|
||||
CanCancel bool `json:"canCancel,omitempty"`
|
||||
DoneDate string `json:"doneDate,omitempty"`
|
||||
CanRelaunch bool `json:"canRelaunch,omitempty"`
|
||||
}
|
||||
|
||||
type Domain struct {
|
||||
NameServerType string `json:"nameServerType,omitempty"`
|
||||
TransferLockStatus string `json:"transferLockStatus,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ovhProvider) updateNS(fqdn string, ns []string) error {
|
||||
// we first need to make sure we can edit the NS
|
||||
// by default zones are in "hosted" mode meaning they default
|
||||
// to OVH default NS. In this mode, the NS can't be updated.
|
||||
domain := Domain{NameServerType: "external"}
|
||||
err := c.client.Call("PUT", fmt.Sprintf("/domain/%s", fqdn), &domain, &Void{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var newNs []DomainNS
|
||||
for _, n := range ns {
|
||||
newNs = append(newNs, DomainNS{
|
||||
Host: n,
|
||||
})
|
||||
}
|
||||
|
||||
update := UpdateNS{
|
||||
NameServers: newNs,
|
||||
}
|
||||
var task Task
|
||||
err = c.client.Call("POST", fmt.Sprintf("/domain/%s/nameServers/update", fqdn), &update, &task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if task.Status == "error" {
|
||||
return fmt.Errorf("API error while updating ns for %s: %s", fqdn, task.Comment)
|
||||
}
|
||||
|
||||
// we don't wait for the task execution. One of the reason is that
|
||||
// NS modification can take time in the registrar, the other is that every task
|
||||
// in OVH is usually executed a few minutes after they have been registered.
|
||||
// We count on the fact that `GetNameservers` uses the registrar API to get
|
||||
// a coherent view (including pending modifications) of the registered NS.
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user