mirror of
https://github.com/anaganisk/digitalocean-dynamic-dns-ip.git
synced 2024-08-30 17:42:10 +00:00
Merge pull request #3 from johnjaylward/correct_paging
update to support paging and custom config location.
This commit is contained in:
commit
d863936ac6
19
README.md
19
README.md
@ -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
|
||||||
|
@ -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,9 +22,12 @@ 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"`
|
||||||
|
DOPageSize int `json:"doPageSize"`
|
||||||
Domains []Domain `json:"domains"`
|
Domains []Domain `json:"domains"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,21 +40,55 @@ 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"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
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")
|
||||||
|
cmdHelp2 := flag.Bool("help", false, "Show the help message")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *cmdHelp || *cmdHelp2 {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := ""
|
||||||
|
if len(flag.Args()) == 0 {
|
||||||
|
var err error
|
||||||
|
configFile, err = homedir.Dir()
|
||||||
checkError(err)
|
checkError(err)
|
||||||
getfile, err := ioutil.ReadFile(homeDirectory + "/.digitalocean-dynamic-ip.json")
|
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()
|
||||||
|
isIpv6 := ip.To4() == nil
|
||||||
|
if !isIpv6 {
|
||||||
|
// 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 {
|
for _, toUpdateRecord := range toUpdateRecords {
|
||||||
if toUpdateRecord.Name == currentRecord.Name && toUpdateRecord.Type == currentRecord.Type && currentIP != currentRecord.Data {
|
if toUpdateRecord.Type != "A" && toUpdateRecord.Type != "AAAA" {
|
||||||
update := []byte(`{"type":"` + toUpdateRecord.Type + `","data":"` + currentIP + `"}`)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"apikey": "samplekeydasjkdhaskjdhrwofihsamplekey",
|
"apikey": "samplekeydasjkdhaskjdhrwofihsamplekey",
|
||||||
|
"doPageSize" : 20,
|
||||||
"domains": [
|
"domains": [
|
||||||
{
|
{
|
||||||
"domain": "example.com",
|
"domain": "example.com",
|
||||||
|
Loading…
Reference in New Issue
Block a user