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(¶ms)") 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 }