1
0

Merge pull request #3 from johnjaylward/correct_paging

update to support paging and custom config location.
This commit is contained in:
Anagani Sai Kiran 2019-07-23 16:59:01 +05:30 committed by GitHub
commit d863936ac6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 168 additions and 31 deletions

View File

@ -1,12 +1,16 @@
# DIGITAL OCEAN DYNAMIC IP API CLIENT # DIGITAL OCEAN DYNAMIC IP API CLIENT
A simple script in Go language to automatically update Digital ocean DNS records if you have a dynamic IP. Since it can be compiled on any platform, you can use it along with raspberrypi etc. A simple script in Go language to automatically update Digital ocean DNS records if you have a dynamic IP. Since it can be compiled on any platform, you can use it along with raspberrypi etc.
To find your Dynamic IP, this program will call out to https://diagnostic.opendns.com/myip.
## requirements ## requirements
Requires Git and Go for building. Requires Git and Go for building.
Requires that the record already exists in DigitalOcean's DNS so that it can be updated. Requires that the record already exists in DigitalOcean's DNS so that it can be updated.
(manually find your IP and add it to DO's DNS it will later be updated) (manually find your IP and add it to DO's DNS it will later be updated)
Requires a Digital Ocean API key that can be created at https://cloud.digitalocean.com/account/api/tokens.
## Usage ## Usage
```bash ```bash
git clone https://github.com/anaganisk/digitalocean-dynamic-dns-ip.git git clone https://github.com/anaganisk/digitalocean-dynamic-dns-ip.git
@ -16,6 +20,7 @@ create a file ".digitalocean-dynamic-ip.json"(dot prefix to hide the file) and p
```json ```json
{ {
"apikey": "samplekeydasjkdhaskjdhrwofihsamplekey", "apikey": "samplekeydasjkdhaskjdhrwofihsamplekey",
"doPageSize" : 20,
"domains": [ "domains": [
{ {
"domain": "example.com", "domain": "example.com",
@ -31,18 +36,30 @@ create a file ".digitalocean-dynamic-ip.json"(dot prefix to hide the file) and p
"records": [ "records": [
{ {
"name": "subdomainOrRecord2", "name": "subdomainOrRecord2",
"type": "A" "type": "A",
"TTL": 30
} }
] ]
} }
] ]
} }
``` ```
The TTL can optionally be updated if passed in the configuration. Digital Ocean has a minimum TTL of 30 seconds. The `type` and the `name` must match existing records in the Digital Ocean DNS configuration. Only `types` of `A` and `AAAA` allowed at the moment.
If you want to reduce the number of calls made to the digital ocean API and have more than 20 DNS records in your domain, you can adjust the `doPageSize` parameter. By default, Digital Ocean returns 20 records per page.
```bash ```bash
#run #run
go build digitalocean-dynamic-ip.go go build digitalocean-dynamic-ip.go
./digitalocean-dynamic-ip ./digitalocean-dynamic-ip
``` ```
Optionally, you can create the configuration file with any name wherever you want, and pass it as a command line argument:
````bash
#run
./digitalocean-dynamic-ip /path/tp/my/config.json
```
You can either set this to run periodically with a cronjob or use your own method. You can either set this to run periodically with a cronjob or use your own method.
```bash ```bash
# run crontab -e # run crontab -e

View File

@ -3,9 +3,14 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"flag"
"fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"net/url"
"os"
"strconv" "strconv"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
@ -17,10 +22,13 @@ func checkError(err error) {
} }
} }
var config ClientConfig
// ClientConfig : configuration json // ClientConfig : configuration json
type ClientConfig struct { type ClientConfig struct {
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
Domains []Domain `json:"domains"` DOPageSize int `json:"doPageSize"`
Domains []Domain `json:"domains"`
} }
// Domain : domains to be changed // Domain : domains to be changed
@ -31,22 +39,56 @@ type Domain struct {
// DNSRecord : Modifyiable DNS record // DNSRecord : Modifyiable DNS record
type DNSRecord struct { type DNSRecord struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Type string `json:"type"`
Data string `json:"data"` Name string `json:"name"`
Type string `json:"type"` Priority *int `json:"priority"`
Port *int `json:"port"`
Weight *int `json:"weight"`
TTL int `json:"ttl"`
Flags *uint8 `json:"flags"`
Tag *string `json:"tag"`
Data string `json:"data"`
} }
// DOResponse : DigitalOcean DNS Records response. // DOResponse : DigitalOcean DNS Records response.
type DOResponse struct { type DOResponse struct {
DomainRecords []DNSRecord `json:"domain_records"` DomainRecords []DNSRecord `json:"domain_records"`
Meta struct {
Total int `json:"total"`
} `json:"meta"`
Links struct {
Pages struct {
First string `json:"first"`
Previous string `json:"prev"`
Next string `json:"next"`
Last string `json:"last"`
} `json:"pages"`
} `json:"links"`
} }
//GetConfig : get configuration file ~/.digitalocean-dynamic-ip.json //GetConfig : get configuration file ~/.digitalocean-dynamic-ip.json
func GetConfig() ClientConfig { func GetConfig() ClientConfig {
homeDirectory, err := homedir.Dir() cmdHelp := flag.Bool("h", false, "Show the help message")
checkError(err) cmdHelp2 := flag.Bool("help", false, "Show the help message")
getfile, err := ioutil.ReadFile(homeDirectory + "/.digitalocean-dynamic-ip.json") flag.Parse()
if *cmdHelp || *cmdHelp2 {
usage()
os.Exit(1)
}
configFile := ""
if len(flag.Args()) == 0 {
var err error
configFile, err = homedir.Dir()
checkError(err)
configFile += "/.digitalocean-dynamic-ip.json"
} else {
configFile = flag.Args()[0]
}
getfile, err := ioutil.ReadFile(configFile)
checkError(err) checkError(err)
var config ClientConfig var config ClientConfig
json.Unmarshal(getfile, &config) json.Unmarshal(getfile, &config)
@ -54,6 +96,21 @@ func GetConfig() ClientConfig {
return config return config
} }
func usage() {
os.Stdout.WriteString(fmt.Sprintf("To use this program you can specify the following command options:\n"+
"-h | -help\n\tShow this help message\n"+
"[config_file]\n\tlocation of the configuration file\n\n"+
"If the [config_file] parameter is not passed, then the default\n"+
"config location of ~/.digitalocean-dynamic-ip.json will be used.\n\n"+
"example usages:\n\t%[1]s -help\n"+
"\t%[1]s\n"+
"\t%[1]s %[2]s\n"+
"",
os.Args[0],
"/path/to/my/config.json",
))
}
//CheckLocalIP : get current IP of server. //CheckLocalIP : get current IP of server.
func CheckLocalIP() string { func CheckLocalIP() string {
currentIPRequest, err := http.Get("https://diagnostic.opendns.com/myip") currentIPRequest, err := http.Get("https://diagnostic.opendns.com/myip")
@ -65,18 +122,33 @@ func CheckLocalIP() string {
} }
//GetDomainRecords : Get DNS records of current domain. //GetDomainRecords : Get DNS records of current domain.
func GetDomainRecords(apiKey string, domain string) DOResponse { func GetDomainRecords(domain string) []DNSRecord {
ret := make([]DNSRecord, 0)
var page DOResponse
pageParam := ""
// 20 is the default page size
if config.DOPageSize > 0 && config.DOPageSize != 20 {
pageParam = "?per_page=" + strconv.Itoa(config.DOPageSize)
}
for url := "https://api.digitalocean.com/v2/domains/" + url.PathEscape(domain) + "/records" + pageParam; url != ""; url = page.Links.Pages.Next {
page = getPage(url)
ret = append(ret, page.DomainRecords...)
}
return ret
}
func getPage(url string) DOResponse {
log.Println(url)
client := &http.Client{} client := &http.Client{}
request, err := http.NewRequest("GET", request, err := http.NewRequest("GET", url, nil)
"https://api.digitalocean.com/v2/domains/"+domain+"/records",
nil)
checkError(err) checkError(err)
request.Header.Add("Content-type", "Application/json") request.Header.Add("Content-type", "Application/json")
request.Header.Add("Authorization", "Bearer "+apiKey) request.Header.Add("Authorization", "Bearer "+config.APIKey)
response, err := client.Do(request) response, err := client.Do(request)
checkError(err) checkError(err)
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
// log.Print(string(body))
var jsonDOResponse DOResponse var jsonDOResponse DOResponse
e := json.Unmarshal(body, &jsonDOResponse) e := json.Unmarshal(body, &jsonDOResponse)
checkError(e) checkError(e)
@ -84,37 +156,84 @@ func GetDomainRecords(apiKey string, domain string) DOResponse {
} }
// UpdateRecords : Update DNS records of domain // UpdateRecords : Update DNS records of domain
func UpdateRecords(apiKey string, domain string, currentIP string, currentRecords DOResponse, toUpdateRecords []DNSRecord) { func UpdateRecords(domain string, ip net.IP, toUpdateRecords []DNSRecord) {
for _, currentRecord := range currentRecords.DomainRecords { currentIP := ip.String()
for _, toUpdateRecord := range toUpdateRecords { isIpv6 := ip.To4() == nil
if toUpdateRecord.Name == currentRecord.Name && toUpdateRecord.Type == currentRecord.Type && currentIP != currentRecord.Data { if !isIpv6 {
update := []byte(`{"type":"` + toUpdateRecord.Type + `","data":"` + currentIP + `"}`) // make sure we are using a v4 format
currentIP = ip.To4().String()
}
log.Printf("%s: %d to update\n", domain, len(toUpdateRecords))
updated := 0
doRecords := GetDomainRecords(domain)
// look for the item to update
if len(doRecords) < 1 {
log.Printf("%s: No DNS records found", domain)
return
}
log.Printf("%s: %d DNS records found", domain, len(doRecords))
for _, toUpdateRecord := range toUpdateRecords {
if toUpdateRecord.Type != "A" && toUpdateRecord.Type != "AAAA" {
log.Fatalf("%s: Unsupported type (Only A and AAAA records supported) for updates %+v", domain, toUpdateRecord)
}
if isIpv6 && toUpdateRecord.Type == "A" {
log.Fatalf("%s: You are trying to update an IPV4 A record with an IPV6 address: new ip: %s, config: %+v", domain, currentIP, toUpdateRecord)
}
if !isIpv6 && toUpdateRecord.Type == "AAAA" {
log.Fatalf("%s: You are trying to update an IPV6 A record with an IPV4 address: new ip: %s, config: %+v", domain, currentIP, toUpdateRecord)
}
if toUpdateRecord.ID > 0 {
// update the record directly. skip the extra search
log.Fatalf("%s: Unable to directly update records yet. Record: %+v", domain, toUpdateRecord)
}
log.Printf("%s: trying to update `%s` : `%s`", domain, toUpdateRecord.Type, toUpdateRecord.Name)
for _, doRecord := range doRecords {
//log.Printf("%s: checking `%s` : `%s`", domain, doRecord.Type, doRecord.Name)
if doRecord.Name == toUpdateRecord.Name && doRecord.Type == toUpdateRecord.Type {
if doRecord.Data == currentIP && (toUpdateRecord.TTL < 30 || doRecord.TTL == toUpdateRecord.TTL) {
log.Printf("%s: IP/TTL did not change %+v", domain, doRecord)
continue
}
log.Printf("%s: updating %+v", domain, doRecord)
// set the IP address
doRecord.Data = currentIP
if toUpdateRecord.TTL >= 30 && doRecord.TTL != toUpdateRecord.TTL {
doRecord.TTL = toUpdateRecord.TTL
}
update, err := json.Marshal(doRecord)
checkError(err)
client := &http.Client{} client := &http.Client{}
request, err := http.NewRequest("PUT", request, err := http.NewRequest("PUT",
"https://api.digitalocean.com/v2/domains/"+domain+"/records/"+strconv.FormatInt(int64(currentRecord.ID), 10), "https://api.digitalocean.com/v2/domains/"+url.PathEscape(domain)+"/records/"+strconv.FormatInt(int64(doRecord.ID), 10),
bytes.NewBuffer(update)) bytes.NewBuffer(update))
checkError(err) checkError(err)
request.Header.Set("Content-Type", "application/json") request.Header.Set("Content-Type", "application/json")
request.Header.Add("Authorization", "Bearer "+apiKey) request.Header.Add("Authorization", "Bearer "+config.APIKey)
response, err := client.Do(request) response, err := client.Do(request)
checkError(err) checkError(err)
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
log.Printf("DO update response for %s: %s\n", currentRecord.Name, string(body)) log.Printf("%s: DO update response for %s: %s\n", domain, doRecord.Name, string(body))
updated++
} }
} }
} }
log.Printf("%s: %d of %d records updated\n", domain, updated, len(toUpdateRecords))
} }
func main() { func main() {
config := GetConfig() config = GetConfig()
currentIP := CheckLocalIP() currentIP := CheckLocalIP()
ip := net.ParseIP(currentIP)
for _, domains := range config.Domains { if ip == nil {
domainName := domains.Domain log.Fatalf("current IP address `%s` is not a valid IP address", currentIP)
apiKey := config.APIKey }
currentDomainRecords := GetDomainRecords(apiKey, domainName) for _, domain := range config.Domains {
log.Println(domainName) domainName := domain.Domain
UpdateRecords(apiKey, domainName, currentIP, currentDomainRecords, domains.Records) log.Printf("%s: START\n", domainName)
UpdateRecords(domainName, ip, domain.Records)
log.Printf("%s: END\n", domainName)
} }
} }

View File

@ -1,5 +1,6 @@
{ {
"apikey": "samplekeydasjkdhaskjdhrwofihsamplekey", "apikey": "samplekeydasjkdhaskjdhrwofihsamplekey",
"doPageSize" : 20,
"domains": [ "domains": [
{ {
"domain": "example.com", "domain": "example.com",