Golang client NIC.ru

Hello! I hasten to share some development. Golang client for NIC.ru API.

We have a close integration with nic.ru, this has happened historically, I’m sure many people use nic.ru DNS services, maybe not as actively as we do, but anyway, this is important for us, we previously had a development in Python, but lately we have been actively moving to Go, so we had to support this business. I will be glad if it will make life easier / save time for someone.

Initial data

Initially, there is such a package for python https://pypi.org/project/nic-api/

It was written by our colleague some time ago, I don’t claim that it works, but I don’t claim the opposite either, we didn’t have the opportunity to check it, since there was no need to use it, nevertheless, it’s impossible not to mention it.

The API format is described herethere is little fun there, everything is in xml, but you don’t have to lose heart, you have to work with what they give.

structures

After reading the documentation, we got the following structures

Request

This is the general structure for creating entries

type Request struct {
	XMLName xml.Name `xml:"request"`
	Text    string   `xml:",chardata"`
	RrList  *RrList  `xml:"rr-list"`
}

RrList

A structure describing a list of entries

type RrList struct {
	Text string `xml:",chardata"`
	Rr   []*RR  `xml:"rr"`
}

RR

DNS record structure

type RR struct {
	Text    string `xml:",chardata"`
	ID      string `xml:"id,attr,omitempty"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
	Ttl     string `xml:"ttl"`
	Type    string `xml:"type"`
	Soa     *Soa   `xml:"soa"`
	Ns      *Ns    `xml:"ns"`
	A       *A     `xml:"a"`
	Txt     *Txt   `xml:"txt"`
	Cname   *Cname `xml:"cname"`
}

A

A structure that contains the IPv4 address of the A-record. So far, we do not have IPv6, we have not even delved into how to live with this matter in NIC.ru, but the main feature here is that, compared to CNAME, there are no attributes, the value is stored as text

type A string

func (a *A) String() string {
	return string(*a)
}

CNAME

The structure that stores the cascading name, everything is taken from the API, IdnName can be something like “our-favorite-site.rf”, while Name will store a value that is understandable to all nodes on the Internet, in this case:

xn-----6kccd6bhcmmf0dp7e6b4b.xn--p1ai
type Cname struct {
	Text    string `xml:",chardata"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
}

Service

The structure reflecting the NIC.ru service, it reflects the tariff, the number of domains, etc., service information.

type Service struct {
	Text         string `xml:",chardata"`
	Admin        string `xml:"admin,attr"`
	DomainsLimit string `xml:"domains-limit,attr"`
	DomainsNum   string `xml:"domains-num,attr"`
	Enable       string `xml:"enable,attr"`
	HasPrimary   string `xml:"has-primary,attr"`
	Name         string `xml:"name,attr"`
	Payer        string `xml:"payer,attr"`
	Tariff       string `xml:"tariff,attr"`
	RrLimit      string `xml:"rr-limit,attr"`
	RrNum        string `xml:"rr-num,attr"`
}

SOA

SOA record structure

type Soa struct {
	Text  string `xml:",chardata"`
	Mname struct {
		Text    string `xml:",chardata"`
		Name    string `xml:"name"`
		IdnName string `xml:"idn-name,omitempty"`
	} `xml:"mname"`
	Rname struct {
		Text    string `xml:",chardata"`
		Name    string `xml:"name"`
		IdnName string `xml:"idn-name,omitempty"`
	} `xml:"rname"`
	Serial  string `xml:"serial"`
	Refresh string `xml:"refresh"`
	Retry   string `xml:"retry"`
	Expire  string `xml:"expire"`
	Minimum string `xml:"minimum"`
}

NS

NS record structure

type Ns struct {
	Text    string `xml:",chardata"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
}

TXT

TXT record structure

type Ns struct {
	Text    string `xml:",chardata"`
	Name    string `xml:"name"`
	IdnName string `xml:"idn-name,omitempty"`
}

zone

Zone structure

type Zone struct {
	Text       string `xml:",chardata"`
	Admin      string `xml:"admin,attr"`
	Enable     string `xml:"enable,attr"`
	HasChanges string `xml:"has-changes,attr"`
	HasPrimary string `xml:"has-primary,attr"`
	ID         string `xml:"id,attr"`
	IdnName    string `xml:"idn-name,attr"`
	Name       string `xml:"name,attr"`
	Payer      string `xml:"payer,attr"`
	Service    string `xml:"service,attr"`
	Rr         []*RR  `xml:"rr"`
}

revision

Revision structure

type Revision struct {
	Text   string `xml:",chardata"`
	Date   string `xml:"date,attr"`
	Ip     string `xml:"ip,attr"`
	Number string `xml:"number,attr"`
}

error

The structure of the error in the response from the API

type Error struct {
	Text string `xml:",chardata"`
	Code string `xml:"code,attr"`
}

Response

API response structure

type Response struct {
	XMLName xml.Name `xml:"response"`
	Text    string   `xml:",chardata"`
	Status  string   `xml:"status"`
	Errors  struct {
		Text  string `xml:",chardata"`
		Error *error `xml:"error"`
	} `xml:"errors"`
	Data struct {
		Text     string `xml:",chardata"`
		Service  []*Service
		Zone     []*Zone     `xml:"zone"`
		Address  []*A        `xml:"address"`
		Revision []*Revision `xml:"revision"`
	} `xml:"data"`
}

How to use

The client is here https://github.com/maetx777/nic.ru-golang-client.git, ready to use. AT examples describes how to create and delete A/CNAME records, as well as how to download the entire zone.

Creating a utility

Next, I will describe a little how to create a console utility for managing the apish, the code is uploaded to the repository.

cmd/nicru/main.go

create file cmd/nicru/main.go:

package main

import (
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

func main() {
	cmd := &cobra.Command{
		Use: `nicru`,
		Short: `утилита для управления записями в DNS NIC.ru`,
	}
	if err := cmd.Execute(); err != nil {
		logrus.Infoln(err.Error())
	}
}

This file will be fed to the assembly of the utility, here we see the default command, when we call it, we will get nothing, the code will exit and that’s it, unless an error occurs, otherwise, the logrus module will show us Fatal, and the utility will exit with a non-zero code.

cmd/nicru/config.go

first, we initialize the config structure, which can be used in different commands:

package main

import api "github.com/maetx777/nic.ru-golang-client/client"

var config = &api.Config{
	Credentials: &api.Credentials{
		OAuth2: &api.OAuth2Creds{
			ClientID: "",
			SecretID: "",
		},
		Username: "",
		Password: "",
	},
	ZoneName:       "",
	DnsServiceName: "",
	CachePath:      "",
}

cmd/nicru/add-a.go

Let’s add a command to the utility to create an A-record.

package main

import (
	"fmt"
	api "github.com/maetx777/nic.ru-golang-client/client"
	"github.com/spf13/cobra"
)

func addACmd() *cobra.Command {

	var (
		names  []string //здесь будем хранить список записей, которые нужно создать
		target string   //здесь хранится ipv4-адрес, на которые будет ссылаться каждое имя, описанное выше
		ttl    int      //TTL для каждой записи
	)

	cmd := &cobra.Command{
		Use:   `add-a`,
		Short: `добавление A-записей`,
		Long: `oauth2 ключи нужно получить в ЛК nic.ru - https://www.nic.ru/manager/oauth.cgi?step=oauth.app_register
имя DNS-сервиса можно посмотреть по адресу https://www.nic.ru/manager/services.cgi?step=srv.my_dns&view.order_by=domain
`,
		Run: func(cmd *cobra.Command, args []string) {
			//инициализируем клиента
			client := api.NewClient(config)
			if response, err := client.AddA(names, target, ttl); err != nil {
				//обрабатываем ошибку
				fmt.Printf("Add A-record error: %s\n", err.Error())
				return
			} else {
				//печатаем результат
				for _, record := range response.Data.Zone[0].Rr {
					fmt.Printf("Added A-record: %s -> %s\n", record.Name, record.A.String())
				}
			}
			//после создания записей, нужно закоммитить результат, чтобы DNS уехал в "прод"
			if _, err := client.CommitZone(); err != nil {
				fmt.Printf("Commit error: %s\n", err.Error())
			} else {
				fmt.Printf("Zone committed\n")
			}
		},
	}
	//создаём флаги для заполнения конфига
	cmd.PersistentFlags().StringVar(&config.Credentials.OAuth2.ClientID, `oauth2-client-id`, ``, `oauth2 client id`)
	cmd.PersistentFlags().StringVar(&config.Credentials.OAuth2.SecretID, `oauth2-secret-id`, ``, `oauth2 secret id`)
	cmd.PersistentFlags().StringVar(&config.Credentials.Username, `username`, ``, `логин от ЛК nic.ru (******/NIC-D)`)
	cmd.PersistentFlags().StringVar(&config.Credentials.Password, `password`, ``, `пароль от nic.ru`)
	cmd.PersistentFlags().StringVar(&config.ZoneName, `zone-name`, `example.com`, `имя DNS-зоны`)
	cmd.PersistentFlags().StringVar(&config.DnsServiceName, `service-name`, `EXAMPLE`, `имя DNS-сервиса`)
	cmd.PersistentFlags().StringVar(&config.CachePath, `cache-path`, `/tmp/.nic.ru.token`, `путь до файла, где будет храниться авторизация от API`)
	//создаём флаги для указания имён создаваемых записей, ipv4-таргета и TTL
	cmd.PersistentFlags().StringSliceVar(&names, `names`, []string{}, `имена, которые нужно создать`)
	cmd.PersistentFlags().StringVar(&target, `target`, ``, `куда нужно отправить имена (например, 1.2.3.4)`)
	cmd.PersistentFlags().IntVar(&ttl, `ttl`, 600, `TTL для созданным записей`)

	return cmd
}

It remains to include the command in main.go:

package main

import (
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

func main() {
	cmd := &cobra.Command{
		Use:   `nicru`,
		Short: `утилита для управления записями в DNS NIC.ru`,
	}
	cmd.AddCommand(addACmd()) // подключаем команду add-a
	if err := cmd.Execute(); err != nil {
		logrus.Infoln(err.Error())
	}
}

We collect the utility:

go build ./cmd/nicru

Testing the launch of the utility

$ ./nicru --help

утилита для управления записями в DNS NIC.ru

Usage:
  nicru [command]

Available Commands:
  add-a       добавление A-записей
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command

Flags:
  -h, --help   help for nicru

Use "nicru [command] --help" for more information about a command.

We look at the help of the add-a command

$ ./nicru add-a --help

oauth2 ключи нужно получить в ЛК nic.ru - https://www.nic.ru/manager/oauth.cgi?step=oauth.app_register
имя DNS-сервиса можно посмотреть по адресу https://www.nic.ru/manager/services.cgi?step=srv.my_dns&view.order_by=domain

Usage:
  nicru add-a [flags]

Flags:
      --cache-path string         путь до файла, где будет храниться авторизация от API (default "/tmp/.nic.ru.token")
  -h, --help                      help for add-a
      --names strings             имена, которые нужно создать
      --oauth2-client-id string   oauth2 client id
      --oauth2-secret-id string   oauth2 secret id
      --password string           пароль от nic.ru
      --service-name string       имя DNS-сервиса (default "EXAMPLE")
      --target string             куда нужно отправить имена (например, 1.2.3.4)
      --ttl int                   TTL для созданным записей (default 600)
      --username string           логин от ЛК nic.ru (******/NIC-D)
      --zone-name string          имя DNS-зоны (default "example.com")

Testing record creation

$ ./nicru add-a --oauth2-client-id *** --oauth2-secret-id *** --username ***/NIC-D --password xJbXi199u8QFZo8Bm --service-name EXAMPLE --zone-name example.com --names foo123 --target 127.0.0.1 --ttl 3600
Added A-record: foo123 -> 127.0.0.1
Zone committed

The record has been successfully created, in a similar way, you can refine the creation of CNAME, the removal of A / CNAME, and other functions implemented in the module.

What can be done to make it easier

In order not to bother with submitting oauth2-creds, as well as login and password, we use vault in our version of the utility, right in the cmd package we have a function that, if there is a ~/.vault-token file, goes to vault, takes everything credits are needed, and then substitutes them into the command as a default, so we do not need to specify the entire footcloth with authorization each time, but, if necessary, we can override this matter at each start.

Conclusion

Initially, there was an idea to share with everyone some, in my opinion, developments, we have to write a lot, sometimes we find ready-made solutions, sometimes not, and then we write our bicycles.

There are no comments in the code yet, due to the fact that today the karma has dropped sharply due to the comment in the post. Given the rapid deterioration of karma, soon I will no longer be able to write anything here, so this is the final post.

We will definitely develop the client, comment everything, develop the readme and use it ourselves directly from the github

I wish you all peace and good! Ready to answer any questions.

Similar Posts

Leave a Reply