yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/aws/dnsrecordset.go (about)

     1  // Copyright 2019 Yunion
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package aws
    16  
    17  import (
    18  	"strconv"
    19  	"strings"
    20  
    21  	"github.com/aws/aws-sdk-go/service/route53"
    22  
    23  	"yunion.io/x/jsonutils"
    24  	"yunion.io/x/log"
    25  	"yunion.io/x/pkg/errors"
    26  	"yunion.io/x/pkg/util/stringutils"
    27  
    28  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    29  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    30  )
    31  
    32  type resourceRecord struct {
    33  	Value string `json:"Value"`
    34  }
    35  
    36  type SGeoLocationCode struct {
    37  	// The two-letter code for the continent.
    38  	//
    39  	// Valid values: AF | AN | AS | EU | OC | NA | SA
    40  	//
    41  	// Constraint: Specifying ContinentCode with either CountryCode or SubdivisionCode
    42  	// returns an InvalidInput error.
    43  	ContinentCode string `json:"ContinentCode"`
    44  
    45  	// The two-letter code for the country.
    46  	CountryCode string `json:"CountryCode"`
    47  
    48  	// The code for the subdivision. Route 53 currently supports only states in
    49  	// the United States.
    50  	SubdivisionCode string `json:"SubdivisionCode"`
    51  }
    52  
    53  type SAliasTarget struct {
    54  	DNSName              string `json:"DNSName"`
    55  	EvaluateTargetHealth *bool  `json:"EvaluateTargetHealth"`
    56  	HostedZoneId         string `json:"HostedZoneId"`
    57  }
    58  
    59  type SdnsRecordSet struct {
    60  	hostedZone              *SHostedZone
    61  	AliasTarget             SAliasTarget     `json:"AliasTarget"`
    62  	Name                    string           `json:"Name"`
    63  	ResourceRecords         []resourceRecord `json:"ResourceRecords"`
    64  	TTL                     int64            `json:"TTL"`
    65  	TrafficPolicyInstanceId string           `json:"TrafficPolicyInstanceId"`
    66  	Type                    string           `json:"Type"`
    67  	SetIdentifier           string           `json:"SetIdentifier"` // 区别 多值 等名称重复的记录
    68  	// policy info
    69  	Failover         string            `json:"Failover"`
    70  	GeoLocation      *SGeoLocationCode `json:"GeoLocation"`
    71  	Region           string            `json:"Region"` // latency based
    72  	MultiValueAnswer *bool             `json:"MultiValueAnswer"`
    73  	Weight           *int64            `json:"Weight"`
    74  
    75  	HealthCheckId string `json:"HealthCheckId"`
    76  }
    77  
    78  func (client *SAwsClient) GetSdnsRecordSets(HostedZoneId string) ([]SdnsRecordSet, error) {
    79  	resourceRecordSets, err := client.GetRoute53ResourceRecordSets(HostedZoneId)
    80  	if err != nil {
    81  		return nil, errors.Wrapf(err, "client.GetRoute53ResourceRecordSets(%s)", HostedZoneId)
    82  	}
    83  	result := []SdnsRecordSet{}
    84  	err = unmarshalAwsOutput(resourceRecordSets, "", &result)
    85  	if err != nil {
    86  		return nil, errors.Wrap(err, "unmarshalAwsOutput(ResourceRecordSets)")
    87  	}
    88  
    89  	return result, nil
    90  }
    91  
    92  func (client *SAwsClient) GetRoute53ResourceRecordSets(HostedZoneId string) ([]*route53.ResourceRecordSet, error) {
    93  	// client
    94  	s, err := client.getAwsRoute53Session()
    95  	if err != nil {
    96  		return nil, errors.Wrap(err, "client.getAwsRoute53Session()")
    97  	}
    98  	route53Client := route53.New(s)
    99  
   100  	// fetch records
   101  	resourceRecordSets := []*route53.ResourceRecordSet{}
   102  	listParams := route53.ListResourceRecordSetsInput{}
   103  	StartRecordName := ""
   104  	MaxItems := "100"
   105  	for true {
   106  		if len(StartRecordName) > 0 {
   107  			listParams.StartRecordName = &StartRecordName
   108  		}
   109  		listParams.MaxItems = &MaxItems
   110  		listParams.HostedZoneId = &HostedZoneId
   111  		ret, err := route53Client.ListResourceRecordSets(&listParams)
   112  		if err != nil {
   113  			return nil, errors.Wrap(err, "route53Client.ListResourceRecordSets()")
   114  		}
   115  		resourceRecordSets = append(resourceRecordSets, ret.ResourceRecordSets...)
   116  		if ret.IsTruncated == nil || !*ret.IsTruncated {
   117  			break
   118  		}
   119  		StartRecordName = *ret.NextRecordName
   120  	}
   121  	return resourceRecordSets, nil
   122  }
   123  
   124  // CREATE, DELETE, UPSERT
   125  func (client *SAwsClient) ChangeResourceRecordSets(action string, hostedZoneId string, resourceRecordSets ...*route53.ResourceRecordSet) error {
   126  	s, err := client.getAwsRoute53Session()
   127  	if err != nil {
   128  		return errors.Wrap(err, "client.getAwsRoute53Session()")
   129  	}
   130  	route53Client := route53.New(s)
   131  
   132  	ChangeBatch := route53.ChangeBatch{}
   133  	for i := 0; i < len(resourceRecordSets); i++ {
   134  		change := route53.Change{}
   135  		change.Action = &action
   136  		change.ResourceRecordSet = resourceRecordSets[i]
   137  		ChangeBatch.Changes = append(ChangeBatch.Changes, &change)
   138  	}
   139  
   140  	changeParams := route53.ChangeResourceRecordSetsInput{}
   141  	changeParams.HostedZoneId = &hostedZoneId
   142  	changeParams.ChangeBatch = &ChangeBatch
   143  	_, err = route53Client.ChangeResourceRecordSets(&changeParams)
   144  	if err != nil {
   145  		return errors.Wrap(err, "route53Client.ChangeResourceRecordSets(&params)")
   146  	}
   147  	return nil
   148  }
   149  
   150  func Getroute53ResourceRecordSet(client *SAwsClient, opts *cloudprovider.DnsRecordSet) (*route53.ResourceRecordSet, error) {
   151  	resourceRecordSet := route53.ResourceRecordSet{}
   152  	resourceRecordSet.SetName(opts.DnsName)
   153  	resourceRecordSet.SetTTL(opts.Ttl)
   154  	resourceRecordSet.SetType(string(opts.DnsType))
   155  	if len(opts.ExternalId) > 0 {
   156  		resourceRecordSet.SetSetIdentifier(opts.ExternalId)
   157  	}
   158  	records := []*route53.ResourceRecord{}
   159  	values := strings.Split(opts.DnsValue, "\n")
   160  	for i := 0; i < len(values); i++ {
   161  		value := values[i]
   162  		if opts.DnsType == cloudprovider.DnsTypeTXT || opts.DnsType == cloudprovider.DnsTypeSPF {
   163  			value = "\"" + value + "\""
   164  		}
   165  		if opts.DnsType == cloudprovider.DnsTypeMX {
   166  			value = strconv.FormatInt(opts.MxPriority, 10) + " " + value
   167  		}
   168  		records = append(records, &route53.ResourceRecord{Value: &value})
   169  	}
   170  	resourceRecordSet.SetResourceRecords(records)
   171  
   172  	// traffic policy info--------------------------------------------
   173  	if opts.PolicyType == cloudprovider.DnsPolicyTypeSimple {
   174  		return &resourceRecordSet, nil
   175  	}
   176  	// SetIdentifier 设置policy需要 ,也可以通过externalId设置
   177  	if resourceRecordSet.SetIdentifier == nil {
   178  		resourceRecordSet.SetSetIdentifier(stringutils.UUID4())
   179  	}
   180  	// addition option(health check)
   181  	if opts.PolicyOptions != nil {
   182  		health := struct {
   183  			HealthCheckId string
   184  		}{}
   185  		opts.PolicyOptions.Unmarshal(&health)
   186  		if len(health.HealthCheckId) > 0 {
   187  			resourceRecordSet.SetHealthCheckId(health.HealthCheckId)
   188  		}
   189  	}
   190  
   191  	// failover choice:PRIMARY|SECONDARY
   192  	if opts.PolicyType == cloudprovider.DnsPolicyTypeFailover {
   193  		resourceRecordSet.SetFailover(string(opts.PolicyValue))
   194  	}
   195  	// geolocation
   196  	if opts.PolicyType == cloudprovider.DnsPolicyTypeByGeoLocation {
   197  		Geo := route53.GeoLocation{}
   198  		locations, err := client.ListGeoLocations()
   199  		if err != nil {
   200  			return nil, errors.Wrap(err, "client.ListGeoLocations()")
   201  		}
   202  		matchedIndex := -1
   203  		for i := 0; i < len(locations); i++ {
   204  			if locations[i].SubdivisionName != nil {
   205  				if string(opts.PolicyValue) == *locations[i].SubdivisionName {
   206  					matchedIndex = i
   207  					break
   208  				}
   209  			}
   210  			if locations[i].CountryName != nil {
   211  				if string(opts.PolicyValue) == *locations[i].CountryName {
   212  					matchedIndex = i
   213  					break
   214  				}
   215  			}
   216  			if locations[i].ContinentCode != nil {
   217  				if string(opts.PolicyValue) == *locations[i].ContinentCode {
   218  					matchedIndex = i
   219  					break
   220  				}
   221  			}
   222  		}
   223  		if matchedIndex < 0 || matchedIndex >= len(locations) {
   224  			return nil, errors.Wrap(cloudprovider.ErrNotSupported, "Can't find Support for this location")
   225  		}
   226  		Geo.ContinentCode = locations[matchedIndex].ContinentCode
   227  		Geo.CountryCode = locations[matchedIndex].CountryCode
   228  		Geo.SubdivisionCode = locations[matchedIndex].SubdivisionCode
   229  		resourceRecordSet.SetGeoLocation(&Geo)
   230  	}
   231  	//  latency ,region based
   232  	if opts.PolicyType == cloudprovider.DnsPolicyTypeLatency {
   233  		resourceRecordSet.SetRegion(string(opts.PolicyValue))
   234  	}
   235  	// MultiValueAnswer ,bool
   236  	if opts.PolicyType == cloudprovider.DnsPolicyTypeMultiValueAnswer {
   237  		var multiValueAnswer bool = true
   238  		resourceRecordSet.SetMultiValueAnswer(multiValueAnswer)
   239  	}
   240  	// Weighted.,int64 value
   241  	if opts.PolicyType == cloudprovider.DnsPolicyTypeWeighted {
   242  		weight, _ := strconv.Atoi(string(opts.PolicyValue))
   243  		resourceRecordSet.SetWeight(int64(weight))
   244  	}
   245  
   246  	return &resourceRecordSet, nil
   247  }
   248  
   249  func (client *SAwsClient) AddDnsRecordSet(hostedZoneId string, opts *cloudprovider.DnsRecordSet) error {
   250  	resourceRecordSet, err := Getroute53ResourceRecordSet(client, opts)
   251  	if err != nil {
   252  		return errors.Wrapf(err, "Getroute53ResourceRecordSet(%s)", jsonutils.Marshal(opts).String())
   253  	}
   254  	err = client.ChangeResourceRecordSets("CREATE", hostedZoneId, resourceRecordSet)
   255  	if err != nil {
   256  		return errors.Wrapf(err, `self.client.changeResourceRecordSets(opts, "CREATE",%s)`, hostedZoneId)
   257  	}
   258  	return nil
   259  }
   260  
   261  func (client *SAwsClient) UpdateDnsRecordSet(hostedZoneId string, opts *cloudprovider.DnsRecordSet) error {
   262  	resourceRecordSet, err := Getroute53ResourceRecordSet(client, opts)
   263  	if err != nil {
   264  		return errors.Wrapf(err, "Getroute53ResourceRecordSet(%s)", jsonutils.Marshal(opts).String())
   265  	}
   266  	err = client.ChangeResourceRecordSets("UPSERT", hostedZoneId, resourceRecordSet)
   267  	if err != nil {
   268  		return errors.Wrapf(err, `self.client.changeResourceRecordSets(opts, "CREATE",%s)`, hostedZoneId)
   269  	}
   270  	return nil
   271  }
   272  
   273  func (client *SAwsClient) RemoveDnsRecordSet(hostedZoneId string, opts *cloudprovider.DnsRecordSet) error {
   274  	resourceRecordSets, err := client.GetRoute53ResourceRecordSets(hostedZoneId)
   275  	if err != nil {
   276  		return errors.Wrapf(err, "self.client.GetRoute53ResourceRecordSets(%s)", hostedZoneId)
   277  	}
   278  	for i := 0; i < len(resourceRecordSets); i++ {
   279  		srecordSet := SdnsRecordSet{}
   280  		err = unmarshalAwsOutput(resourceRecordSets[i], "", &srecordSet)
   281  		if err != nil {
   282  			return errors.Wrap(err, "unmarshalAwsOutput(ResourceRecordSets)")
   283  		}
   284  		if srecordSet.match(opts) {
   285  			err := client.ChangeResourceRecordSets("DELETE", hostedZoneId, resourceRecordSets[i])
   286  			if err != nil {
   287  				return errors.Wrapf(err, `self.client.changeResourceRecordSets(opts, "DELETE",%s)`, hostedZoneId)
   288  			}
   289  			return nil
   290  		}
   291  	}
   292  	return nil
   293  }
   294  
   295  func (self *SdnsRecordSet) GetStatus() string {
   296  	return api.DNS_RECORDSET_STATUS_AVAILABLE
   297  }
   298  
   299  func (self *SdnsRecordSet) GetEnabled() bool {
   300  	return true
   301  }
   302  
   303  func (self *SdnsRecordSet) GetGlobalId() string {
   304  	return self.SetIdentifier
   305  }
   306  
   307  func (self *SdnsRecordSet) GetDnsName() string {
   308  	if self.hostedZone == nil {
   309  		return self.Name
   310  	}
   311  	if self.Name == self.hostedZone.Name {
   312  		return "@"
   313  	}
   314  	return strings.TrimSuffix(self.Name, "."+self.hostedZone.Name)
   315  }
   316  
   317  func (self *SdnsRecordSet) GetDnsType() cloudprovider.TDnsType {
   318  	return cloudprovider.TDnsType(self.Type)
   319  }
   320  
   321  func (self *SdnsRecordSet) GetDnsValue() string {
   322  	var records []string
   323  	for i := 0; i < len(self.ResourceRecords); i++ {
   324  		value := self.ResourceRecords[i].Value
   325  		if self.Type == "TXT" || self.Type == "SPF" {
   326  			value = value[1 : len(value)-1]
   327  		}
   328  		if self.Type == "MX" {
   329  			strs := strings.Split(value, " ")
   330  			if len(strs) >= 2 {
   331  				value = strs[1]
   332  			}
   333  		}
   334  		records = append(records, value)
   335  	}
   336  	return strings.Join(records, "\n")
   337  }
   338  
   339  func (self *SdnsRecordSet) GetTTL() int64 {
   340  	return self.TTL
   341  }
   342  
   343  func (self *SdnsRecordSet) GetMxPriority() int64 {
   344  	if self.GetDnsType() != cloudprovider.DnsTypeMX {
   345  		return 0
   346  	}
   347  	strs := strings.Split(self.GetDnsValue(), " ")
   348  	if len(strs) > 0 {
   349  		mx, err := strconv.ParseInt(strs[0], 10, 64)
   350  		if err == nil {
   351  			return mx
   352  		}
   353  	}
   354  	log.Errorf("can't parse mxpriority:%s", self.GetDnsValue())
   355  	return 0
   356  }
   357  
   358  // trafficpolicy 信息
   359  func (self *SdnsRecordSet) GetPolicyType() cloudprovider.TDnsPolicyType {
   360  	/*
   361  		Failover         string          `json:"Failover"`
   362  		GeoLocation      GeoLocationCode `json:"GeoLocation"`
   363  		Region           string          `json:"Region"` // latency based
   364  		MultiValueAnswer *bool           `json:"MultiValueAnswer"`
   365  		Weight           *int64          `json:"Weight"`
   366  	*/
   367  
   368  	if len(self.Failover) > 0 {
   369  		return cloudprovider.DnsPolicyTypeFailover
   370  	}
   371  	if self.GeoLocation != nil {
   372  		return cloudprovider.DnsPolicyTypeByGeoLocation
   373  	}
   374  	if len(self.Region) > 0 {
   375  		return cloudprovider.DnsPolicyTypeLatency
   376  	}
   377  	if self.MultiValueAnswer != nil {
   378  		return cloudprovider.DnsPolicyTypeMultiValueAnswer
   379  	}
   380  	if self.Weight != nil {
   381  		return cloudprovider.DnsPolicyTypeWeighted
   382  	}
   383  	return cloudprovider.DnsPolicyTypeSimple
   384  
   385  }
   386  
   387  func (self *SdnsRecordSet) GetPolicyOptions() *jsonutils.JSONDict {
   388  	options := jsonutils.NewDict()
   389  	if len(self.HealthCheckId) > 0 {
   390  		options.Add(jsonutils.NewString(self.HealthCheckId), "health_check_id")
   391  	}
   392  	return options
   393  }
   394  
   395  func CodeMatch(s string, d *string) bool {
   396  	if d == nil {
   397  		return len(s) == 0
   398  	}
   399  	return s == *d
   400  }
   401  
   402  func (self *SdnsRecordSet) GetPolicyValue() cloudprovider.TDnsPolicyValue {
   403  	if len(self.Failover) > 0 {
   404  		return cloudprovider.TDnsPolicyValue(self.Failover)
   405  	}
   406  	if self.GeoLocation != nil {
   407  		locations, err := self.hostedZone.client.ListGeoLocations()
   408  		log.Errorf("List aws route53 locations failed!")
   409  		if err != nil {
   410  			return ""
   411  		}
   412  		for i := 0; i < len(locations); i++ {
   413  			if CodeMatch(self.GeoLocation.SubdivisionCode, locations[i].SubdivisionCode) &&
   414  				CodeMatch(self.GeoLocation.CountryCode, locations[i].CountryCode) &&
   415  				CodeMatch(self.GeoLocation.ContinentCode, locations[i].ContinentCode) {
   416  				if locations[i].SubdivisionCode != nil {
   417  					return cloudprovider.TDnsPolicyValue(*locations[i].SubdivisionName)
   418  				}
   419  				if locations[i].CountryCode != nil {
   420  					return cloudprovider.TDnsPolicyValue(*locations[i].CountryName)
   421  				}
   422  				if locations[i].ContinentCode != nil {
   423  					return cloudprovider.TDnsPolicyValue(*locations[i].ContinentName)
   424  				}
   425  			}
   426  		}
   427  		return ""
   428  	}
   429  	if len(self.Region) > 0 {
   430  		return cloudprovider.TDnsPolicyValue(self.Region)
   431  	}
   432  	if self.MultiValueAnswer != nil {
   433  		return cloudprovider.DnsPolicyValueEmpty
   434  	}
   435  	if self.Weight != nil {
   436  		return cloudprovider.TDnsPolicyValue(strconv.FormatInt(*self.Weight, 10))
   437  	}
   438  	return cloudprovider.DnsPolicyValueEmpty
   439  }
   440  
   441  func (self *SdnsRecordSet) match(change *cloudprovider.DnsRecordSet) bool {
   442  	if change.DnsName != self.GetDnsName() {
   443  		return false
   444  	}
   445  	if change.DnsValue != self.GetDnsValue() {
   446  		return false
   447  	}
   448  	if change.Ttl != self.GetTTL() {
   449  		return false
   450  	}
   451  	if change.DnsType != self.GetDnsType() {
   452  		return false
   453  	}
   454  	if change.ExternalId != self.GetGlobalId() {
   455  		return false
   456  	}
   457  	return true
   458  }