Skip to content

Commit

Permalink
[GOAL2-782] Export DNS configuration (algorand#43)
Browse files Browse the repository at this point in the history
* Add support for zone exporting.

* Add space between functions.
  • Loading branch information
tsachizehub authored and David Shoots committed Jun 20, 2019
1 parent ece2bbb commit 0e9aa9e
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 14 deletions.
130 changes: 122 additions & 8 deletions cmd/algons/dnsCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bufio"
"context"
"fmt"
"io/ioutil"
"net"
"os"
"regexp"
Expand All @@ -40,13 +41,19 @@ var (
recordType string
noPrompt bool
excludePattern string
exportNetwork string
outputFilename string
)

func init() {
dnsCmd.AddCommand(checkCmd)
dnsCmd.AddCommand(addCmd)
dnsCmd.AddCommand(deleteCmd)
dnsCmd.AddCommand(listCmd)
dnsCmd.AddCommand(exportCmd)

listCmd.AddCommand(listRecordsCmd)
listCmd.AddCommand(listZonesCmd)

addCmd.Flags().StringVarP(&addFromName, "from", "f", "", "From name to add new DNS entry")
addCmd.MarkFlagRequired("from")
Expand All @@ -58,9 +65,13 @@ func init() {
deleteCmd.Flags().BoolVarP(&noPrompt, "no-prompt", "y", false, "No prompting for records deletion")
deleteCmd.Flags().StringVarP(&excludePattern, "exclude", "e", "", "name records exclude pattern")

listCmd.Flags().StringVarP(&listNetwork, "network", "n", "", "Domain name for records to list")
listCmd.Flags().StringVarP(&recordType, "recordType", "t", "", "DNS record type to list (A, CNAME, SRV)")
listCmd.MarkFlagRequired("network")
listRecordsCmd.Flags().StringVarP(&listNetwork, "network", "n", "", "Domain name for records to list")
listRecordsCmd.Flags().StringVarP(&recordType, "recordType", "t", "", "DNS record type to list (A, CNAME, SRV)")
listRecordsCmd.MarkFlagRequired("network")

exportCmd.Flags().StringVarP(&exportNetwork, "network", "n", "", "Domain name to export")
exportCmd.MarkFlagRequired("network")
exportCmd.Flags().StringVarP(&outputFilename, "zonefile", "z", "", "Output file for backup ( intead of outputing it to stdout ) ")
}

type byIP []net.IP
Expand All @@ -81,8 +92,17 @@ var dnsCmd = &cobra.Command{

var listCmd = &cobra.Command{
Use: "list",
Short: "List the DNS/SRV entries of the given network",
Long: "List the DNS/SRV entries of the given network",
Short: "List the A/SRV/Zones entries of the given network",
Long: "List the A/SRV/Zones entries of the given network",
Run: func(cmd *cobra.Command, args []string) {
cmd.HelpFunc()(cmd, args)
},
}

var listRecordsCmd = &cobra.Command{
Use: "records",
Short: "List the A/SRV entries of the given network",
Long: "List the A/SRV entries of the given network",
Run: func(cmd *cobra.Command, args []string) {
recordType = strings.ToUpper(recordType)
if recordType == "" || recordType == "A" || recordType == "CNAME" || recordType == "SRV" {
Expand All @@ -94,6 +114,17 @@ var listCmd = &cobra.Command{
},
}

var listZonesCmd = &cobra.Command{
Use: "zones",
Short: "List the zones",
Long: "List the zones",
Run: func(cmd *cobra.Command, args []string) {
if !doListZones() {
os.Exit(1)
}
},
}

var checkCmd = &cobra.Command{
Use: "check",
Short: "Check the status",
Expand Down Expand Up @@ -140,6 +171,16 @@ var deleteCmd = &cobra.Command{
},
}

var exportCmd = &cobra.Command{
Use: "export",
Short: "Export DNS record entries for a specified network",
Run: func(cmd *cobra.Command, args []string) {
if !doExportZone(exportNetwork, outputFilename) {
os.Exit(1)
}
},
}

func doAddDNS(from string, to string) (err error) {
cfZoneID, cfEmail, cfKey, err := getClouldflareCredentials()
if err != nil {
Expand All @@ -166,11 +207,23 @@ func doAddDNS(from string, to string) (err error) {
return
}

func getClouldflareCredentials() (zoneID string, email string, authKey string, err error) {
zoneID = os.Getenv("CLOUDFLARE_ZONE_ID")
func getClouldflareAuthCredentials() (email string, authKey string, err error) {
email = os.Getenv("CLOUDFLARE_EMAIL")
authKey = os.Getenv("CLOUDFLARE_AUTH_KEY")
if zoneID == "" || email == "" || authKey == "" {
if email == "" || authKey == "" {
err = fmt.Errorf("one or more credentials missing from ENV")
}
return
}

func getClouldflareCredentials() (zoneID string, email string, authKey string, err error) {
email, authKey, err = getClouldflareAuthCredentials()
if err != nil {
return
}

zoneID = os.Getenv("CLOUDFLARE_ZONE_ID")
if zoneID == "" {
err = fmt.Errorf("one or more credentials missing from ENV")
}
return
Expand Down Expand Up @@ -333,3 +386,64 @@ func listEntries(listNetwork string, recordType string) {
}
}
}

func doExportZone(network string, outputFilename string) bool {
cfEmail, cfKey, err := getClouldflareAuthCredentials()
if err != nil {
fmt.Fprintf(os.Stderr, "error getting DNS credentials: %v", err)
return false
}
cloudflareCred := cloudflare.NewCred(cfEmail, cfKey)
zones, err := cloudflareCred.GetZones(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "Error retrieving zones entries: %v\n", err)
return false
}
zoneID := ""
// find a zone that matches the requested network name.
for _, z := range zones {
if z.DomainName == network {
zoneID = z.ZoneID
break
}
fmt.Printf("%s : %s\n", z.DomainName, z.ZoneID)
}
if zoneID == "" {
fmt.Fprintf(os.Stderr, "No matching zoneID was found for %s\n", network)
return false
}
cloudflareDNS := cloudflare.NewDNS(zoneID, cfEmail, cfKey)
exportedZone, err := cloudflareDNS.ExportZone(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to export zone : %v\n", err)
return false
}
if outputFilename != "" {
err = ioutil.WriteFile(outputFilename, exportedZone, 0666)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to write exported zone file : %v\n", err)
return false
}
} else {
fmt.Fprint(os.Stdout, string(exportedZone))
}
return true
}

func doListZones() bool {
cfEmail, cfKey, err := getClouldflareAuthCredentials()
if err != nil {
fmt.Fprintf(os.Stderr, "error getting DNS credentials: %v", err)
return false
}
cloudflareCred := cloudflare.NewCred(cfEmail, cfKey)
zones, err := cloudflareCred.GetZones(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "Error listing zones entries: %v\n", err)
return false
}
for _, z := range zones {
fmt.Printf("%s : %s\n", z.DomainName, z.ZoneID)
}
return true
}
84 changes: 78 additions & 6 deletions tools/network/cloudflare/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cloudflare
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
Expand All @@ -29,19 +30,34 @@ const (
AutomaticTTL = 1
)

// DNS is the cloudflare package main access class. Initiate an instance of this class to access the clouldflare APIs.
type DNS struct {
zoneID string
// Cred contains the credentials used to authenticate with the cloudflare API.
type Cred struct {
authEmail string
authKey string
}

// DNS is the cloudflare package main access class. Initiate an instance of this class to access the clouldflare APIs.
type DNS struct {
zoneID string
Cred
}

// NewCred creates a new credential structure used to authenticate with the cloudflare service.
func NewCred(authEmail string, authKey string) *Cred {
return &Cred{
authEmail: authEmail,
authKey: authKey,
}
}

// NewDNS create a new instance of clouldflare DNS services class
func NewDNS(zoneID string, authEmail string, authKey string) *DNS {
return &DNS{
zoneID: zoneID,
authEmail: authEmail,
authKey: authKey,
zoneID: zoneID,
Cred: Cred{
authEmail: authEmail,
authKey: authKey,
},
}
}

Expand Down Expand Up @@ -241,3 +257,59 @@ func (d *DNS) UpdateSRVRecord(ctx context.Context, recordID string, name string,
}
return nil
}

// Zone represent a single zone on the cloudflare API.
type Zone struct {
DomainName string
ZoneID string
}

// GetZones returns a list of zones that are associated with cloudflare.
func (c *Cred) GetZones(ctx context.Context) (zones []Zone, err error) {
request, err := getZonesRequest(c.authEmail, c.authKey)
if err != nil {
return nil, err
}
client := &http.Client{}
response, err := client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}

parsedResponse, err := parseGetZonesResponse(response)
if err != nil {
return nil, err
}
if parsedResponse.Success == false {
return nil, fmt.Errorf("failed to retrieve zone records : %v", parsedResponse)
}

for _, z := range parsedResponse.Result {
zones = append(zones,
Zone{
DomainName: z.Name,
ZoneID: z.ID,
},
)
}
return zones, err
}

// ExportZone exports the zone into a BIND config bytes array
func (d *DNS) ExportZone(ctx context.Context) (exportedZoneBytes []byte, err error) {
request, err := exportZoneRequest(d.zoneID, d.authEmail, d.authKey)
if err != nil {
return nil, err
}
client := &http.Client{}
response, err := client.Do(request.WithContext(ctx))
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
return body, nil
}
97 changes: 97 additions & 0 deletions tools/network/cloudflare/zones.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (C) 2019 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package cloudflare

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)

func getZonesRequest(authEmail, authKey string) (*http.Request, error) {
// construct the query
requestURI, err := url.Parse(cloudFlareURI)
if err != nil {
return nil, err
}
requestURI.Path = requestURI.Path + "zones"
request, err := http.NewRequest("GET", requestURI.String(), nil)
if err != nil {
return nil, err
}
addHeaders(request, authEmail, authKey)
return request, nil
}

// GetZonesResult is the JSON response for a DNS create request
type GetZonesResult struct {
Success bool `json:"success"`
Errors []interface{} `json:"errors"`
Messages []interface{} `json:"messages"`
Result []GetZonesResultItem `json:"result"`
ResultInfo GetZonesResultPage `json:"result_info"`
}

// GetZonesResultPage is the result of the response for the DNS create request
type GetZonesResultPage struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
Count int `json:"count"`
TotalCount int `json:"total_count"`
}

// GetZonesResultItem is the result of the response for the DNS create request
type GetZonesResultItem struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Paused bool `json:"paused"`
Type string `json:"type"`
DevelopmentMode int `json:"development_mode"`
NameServers []string `json:"name_servers"`
OriginalNameServers []string `json:"original_name_servers"`
}

func parseGetZonesResponse(response *http.Response) (*GetZonesResult, error) {
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
var parsedReponse GetZonesResult
if err := json.Unmarshal(body, &parsedReponse); err != nil {
return nil, err
}
return &parsedReponse, nil
}

func exportZoneRequest(zoneID, authEmail, authKey string) (*http.Request, error) {
// construct the query
requestURI, err := url.Parse(cloudFlareURI)
if err != nil {
return nil, err
}
requestURI.Path = requestURI.Path + "zones/" + zoneID + "/dns_records/export"
request, err := http.NewRequest("GET", requestURI.String(), nil)
if err != nil {
return nil, err
}
addHeaders(request, authEmail, authKey)
return request, nil
}

0 comments on commit 0e9aa9e

Please sign in to comment.