github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/softlayer/softlayer.go (about)

     1  package softlayer
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/StackExchange/dnscontrol/models"
    10  	"github.com/StackExchange/dnscontrol/providers"
    11  	"github.com/StackExchange/dnscontrol/providers/diff"
    12  	"github.com/miekg/dns/dnsutil"
    13  
    14  	"github.com/softlayer/softlayer-go/datatypes"
    15  	"github.com/softlayer/softlayer-go/filter"
    16  	"github.com/softlayer/softlayer-go/services"
    17  	"github.com/softlayer/softlayer-go/session"
    18  )
    19  
    20  type SoftLayer struct {
    21  	Session *session.Session
    22  }
    23  
    24  func init() {
    25  	providers.RegisterDomainServiceProviderType("SOFTLAYER", newReg, providers.CanUseSRV)
    26  }
    27  
    28  func newReg(conf map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
    29  	s := session.New(conf["username"], conf["api_key"], conf["endpoint_url"], conf["timeout"])
    30  
    31  	if len(s.UserName) == 0 || len(s.APIKey) == 0 {
    32  		return nil, fmt.Errorf("SoftLayer UserName and APIKey must be provided")
    33  	}
    34  
    35  	//s.Debug = true
    36  
    37  	api := &SoftLayer{
    38  		Session: s,
    39  	}
    40  
    41  	return api, nil
    42  }
    43  
    44  func (s *SoftLayer) GetNameservers(domain string) ([]*models.Nameserver, error) {
    45  	// Always use the same nameservers for softlayer
    46  	nservers := []string{"ns1.softlayer.com", "ns2.softlayer.com"}
    47  	return models.StringsToNameservers(nservers), nil
    48  }
    49  
    50  func (s *SoftLayer) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    51  	corrections := []*models.Correction{}
    52  
    53  	domain, err := s.getDomain(&dc.Name)
    54  
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	actual, err := s.getExistingRecords(domain)
    60  
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	_, create, delete, modify := diff.New(dc).IncrementalDiff(actual)
    66  
    67  	for _, del := range delete {
    68  		existing := del.Existing.Original.(datatypes.Dns_Domain_ResourceRecord)
    69  		corrections = append(corrections, &models.Correction{
    70  			Msg: del.String(),
    71  			F:   s.deleteRecordFunc(*existing.Id),
    72  		})
    73  	}
    74  
    75  	for _, cre := range create {
    76  		corrections = append(corrections, &models.Correction{
    77  			Msg: cre.String(),
    78  			F:   s.createRecordFunc(cre.Desired, domain),
    79  		})
    80  	}
    81  
    82  	for _, mod := range modify {
    83  		existing := mod.Existing.Original.(datatypes.Dns_Domain_ResourceRecord)
    84  		corrections = append(corrections, &models.Correction{
    85  			Msg: mod.String(),
    86  			F:   s.updateRecordFunc(&existing, mod.Desired),
    87  		})
    88  	}
    89  
    90  	return corrections, nil
    91  }
    92  
    93  func (s *SoftLayer) getDomain(name *string) (*datatypes.Dns_Domain, error) {
    94  	domains, err := services.GetAccountService(s.Session).
    95  		Filter(filter.Path("domains.name").Eq(name).Build()).
    96  		Mask("resourceRecords").
    97  		GetDomains()
    98  
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	if len(domains) == 0 {
   104  		return nil, fmt.Errorf("Didn't find a domain matching %s", *name)
   105  	} else if len(domains) > 1 {
   106  		return nil, fmt.Errorf("Found %d domains matching %s", len(domains), *name)
   107  	}
   108  
   109  	return &domains[0], nil
   110  }
   111  
   112  func (s *SoftLayer) getExistingRecords(domain *datatypes.Dns_Domain) ([]*models.RecordConfig, error) {
   113  	actual := []*models.RecordConfig{}
   114  
   115  	for _, record := range domain.ResourceRecords {
   116  		recType := strings.ToUpper(*record.Type)
   117  
   118  		if recType == "SOA" {
   119  			continue
   120  		}
   121  
   122  		recConfig := &models.RecordConfig{
   123  			Type:     recType,
   124  			Target:   *record.Data,
   125  			TTL:      uint32(*record.Ttl),
   126  			Original: record,
   127  		}
   128  
   129  		switch recType {
   130  		case "SRV":
   131  			var service, protocol string = "", "_tcp"
   132  
   133  			if record.Weight != nil {
   134  				recConfig.SrvWeight = uint16(*record.Weight)
   135  			}
   136  			if record.Port != nil {
   137  				recConfig.SrvPort = uint16(*record.Port)
   138  			}
   139  			if record.Priority != nil {
   140  				recConfig.SrvPriority = uint16(*record.Priority)
   141  			}
   142  			if record.Protocol != nil {
   143  				protocol = *record.Protocol
   144  			}
   145  			if record.Service != nil {
   146  				service = *record.Service
   147  			}
   148  
   149  			recConfig.Name = fmt.Sprintf("%s.%s", service, strings.ToLower(protocol))
   150  
   151  		case "MX":
   152  			if record.MxPriority != nil {
   153  				recConfig.MxPreference = uint16(*record.MxPriority)
   154  			}
   155  
   156  			fallthrough
   157  
   158  		default:
   159  			recConfig.Name = *record.Host
   160  		}
   161  
   162  		recConfig.NameFQDN = dnsutil.AddOrigin(recConfig.Name, *domain.Name)
   163  		actual = append(actual, recConfig)
   164  	}
   165  
   166  	return actual, nil
   167  }
   168  
   169  func (s *SoftLayer) createRecordFunc(desired *models.RecordConfig, domain *datatypes.Dns_Domain) func() error {
   170  	var ttl, preference, domainId int = int(desired.TTL), int(desired.MxPreference), *domain.Id
   171  	var weight, priority, port int = int(desired.SrvWeight), int(desired.SrvPriority), int(desired.SrvPort)
   172  	var host, data, newType string = desired.Name, desired.Target, desired.Type
   173  	var err error = nil
   174  
   175  	srvRegexp := regexp.MustCompile(`^_(?P<Service>\w+)\.\_(?P<Protocol>\w+)$`)
   176  
   177  	return func() error {
   178  		newRecord := datatypes.Dns_Domain_ResourceRecord{
   179  			DomainId: &domainId,
   180  			Ttl:      &ttl,
   181  			Type:     &newType,
   182  			Data:     &data,
   183  			Host:     &host,
   184  		}
   185  
   186  		switch newType {
   187  		case "MX":
   188  			service := services.GetDnsDomainResourceRecordMxTypeService(s.Session)
   189  
   190  			newRecord.MxPriority = &preference
   191  
   192  			newMx := datatypes.Dns_Domain_ResourceRecord_MxType{
   193  				Dns_Domain_ResourceRecord: newRecord,
   194  			}
   195  
   196  			_, err = service.CreateObject(&newMx)
   197  
   198  		case "SRV":
   199  			service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session)
   200  			result := srvRegexp.FindStringSubmatch(host)
   201  
   202  			if len(result) != 3 {
   203  				return fmt.Errorf("SRV Record must match format \"_service._protocol\" not %s", host)
   204  			}
   205  
   206  			var serviceName, protocol string = result[1], strings.ToLower(result[2])
   207  
   208  			newSrv := datatypes.Dns_Domain_ResourceRecord_SrvType{
   209  				Dns_Domain_ResourceRecord: newRecord,
   210  				Service:                   &serviceName,
   211  				Port:                      &port,
   212  				Priority:                  &priority,
   213  				Protocol:                  &protocol,
   214  				Weight:                    &weight,
   215  			}
   216  
   217  			_, err = service.CreateObject(&newSrv)
   218  
   219  		default:
   220  			service := services.GetDnsDomainResourceRecordService(s.Session)
   221  			_, err = service.CreateObject(&newRecord)
   222  		}
   223  
   224  		return err
   225  	}
   226  }
   227  
   228  func (s *SoftLayer) deleteRecordFunc(resId int) func() error {
   229  	// seems to be no problem deleting MX and SRV records via common interface
   230  	return func() error {
   231  		_, err := services.GetDnsDomainResourceRecordService(s.Session).
   232  			Id(resId).
   233  			DeleteObject()
   234  
   235  		return err
   236  	}
   237  }
   238  
   239  func (s *SoftLayer) updateRecordFunc(existing *datatypes.Dns_Domain_ResourceRecord, desired *models.RecordConfig) func() error {
   240  	var ttl, preference int = int(desired.TTL), int(desired.MxPreference)
   241  	var priority, weight, port int = int(desired.SrvPriority), int(desired.SrvWeight), int(desired.SrvPort)
   242  
   243  	return func() error {
   244  		var changes bool = false
   245  		var err error = nil
   246  
   247  		switch desired.Type {
   248  		case "MX":
   249  			service := services.GetDnsDomainResourceRecordMxTypeService(s.Session)
   250  			updated := datatypes.Dns_Domain_ResourceRecord_MxType{}
   251  
   252  			if desired.Name != *existing.Host {
   253  				updated.Host = &desired.Name
   254  				changes = true
   255  			}
   256  
   257  			if desired.Target != *existing.Data {
   258  				updated.Data = &desired.Target
   259  				changes = true
   260  			}
   261  
   262  			if ttl != *existing.Ttl {
   263  				updated.Ttl = &ttl
   264  				changes = true
   265  			}
   266  
   267  			if preference != *existing.MxPriority {
   268  				updated.MxPriority = &preference
   269  				changes = true
   270  			}
   271  
   272  			if !changes {
   273  				return fmt.Errorf("Error: Didn't find changes when I expect some.")
   274  			}
   275  
   276  			_, err = service.Id(*existing.Id).EditObject(&updated)
   277  
   278  		case "SRV":
   279  			service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session)
   280  			updated := datatypes.Dns_Domain_ResourceRecord_SrvType{}
   281  
   282  			if desired.Name != *existing.Host {
   283  				updated.Host = &desired.Name
   284  				changes = true
   285  			}
   286  
   287  			if desired.Target != *existing.Data {
   288  				updated.Data = &desired.Target
   289  				changes = true
   290  			}
   291  
   292  			if ttl != *existing.Ttl {
   293  				updated.Ttl = &ttl
   294  				changes = true
   295  			}
   296  
   297  			if priority != *existing.Priority {
   298  				updated.Priority = &priority
   299  				changes = true
   300  			}
   301  
   302  			if weight != *existing.Weight {
   303  				updated.Weight = &weight
   304  				changes = true
   305  			}
   306  
   307  			if port != *existing.Port {
   308  				updated.Port = &port
   309  				changes = true
   310  			}
   311  
   312  			// TODO: handle service & protocol - or does that just result in a
   313  			// delete and recreate?
   314  
   315  			if !changes {
   316  				return fmt.Errorf("Error: Didn't find changes when I expect some.")
   317  			}
   318  
   319  			_, err = service.Id(*existing.Id).EditObject(&updated)
   320  
   321  		default:
   322  			service := services.GetDnsDomainResourceRecordService(s.Session)
   323  			updated := datatypes.Dns_Domain_ResourceRecord{}
   324  
   325  			if desired.Name != *existing.Host {
   326  				updated.Host = &desired.Name
   327  				changes = true
   328  			}
   329  
   330  			if desired.Target != *existing.Data {
   331  				updated.Data = &desired.Target
   332  				changes = true
   333  			}
   334  
   335  			if ttl != *existing.Ttl {
   336  				updated.Ttl = &ttl
   337  				changes = true
   338  			}
   339  
   340  			if !changes {
   341  				return fmt.Errorf("Error: Didn't find changes when I expect some.")
   342  			}
   343  
   344  			_, err = service.Id(*existing.Id).EditObject(&updated)
   345  		}
   346  
   347  		return err
   348  	}
   349  }