Всем привет! Спешу поделиться кое-какой разработкой. Golang-клиент для NIC.ru API.
У нас есть тесная интеграция с nic.ru, так сложилось исторически, уверен, многие пользуются DNS-услугами nic.ru, может быть, не так активно как мы, но, так или иначе, для нас это важно, ранее у нас была разработка на Python, но, в последнее время мы активно переезжаем на Go, поэтому пришлось поддержать это дело. Буду рад, если кому-то это облегчит жизнь / сэкономить время.
Исходные данные
Изначально есть вот такой пакет для python https://pypi.org/project/nic-api/
Его написал наш коллега некоторое время назад, не утверждаю, что оно работает, но и не утверждаю обратное, у нас не было возможности это проверить, так как не было необходимости этим пользоваться, тем не менее, нельзя это не упомянуть.
Формат API описан тут, веселого там мало, всё на xml, но, унывать не приходится, приходится работать с тем, что дают.
Структуры
После чтения документации у нас получились следующие структуры
Request
Это общая структура для создания записей
type Request struct {
XMLName xml.Name `xml:"request"`
Text string `xml:",chardata"`
RrList *RrList `xml:"rr-list"`
}
RrList
Структура, описывающая список записей
type RrList struct {
Text string `xml:",chardata"`
Rr []*RR `xml:"rr"`
}
RR
Структура записи в DNS
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
Структура, хранящая в себе IPv4-адрес А-записи. У нас, пока что, нет IPv6, не вникали даже, как жить с этим делом в NIC.ru, но, тут главная особенность в том, что, по сравнению с CNAME, нет никаких атрибутов, значение хранится как текст
type A string
func (a *A) String() string {
return string(*a)
}
CNAME
Структура, хранящая в себе каскадное имя, всё взято из API, IdnName может быть чем-то типа "наш-любимый-сайт.рф", в то время, как Name буде хранить значение, которые понятно всем узлам в сети Интернет, в данном случае:
xn-----6kccd6bhcmmf0dp7e6b4b.xn--p1ai
type Cname struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
IdnName string `xml:"idn-name,omitempty"`
}
Service
Структура, отражающая сервис NIC.ru, тут отражён тариф, количество доменов и т.п, сервисная информация.
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-записи
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-записи
type Ns struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
IdnName string `xml:"idn-name,omitempty"`
}
TXT
Структура TXT-записи
type Ns struct {
Text string `xml:",chardata"`
Name string `xml:"name"`
IdnName string `xml:"idn-name,omitempty"`
}
Zone
Структура зоны
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
Структура ревизии
type Revision struct {
Text string `xml:",chardata"`
Date string `xml:"date,attr"`
Ip string `xml:"ip,attr"`
Number string `xml:"number,attr"`
}
Error
Структура ошибки в ответе от API
type Error struct {
Text string `xml:",chardata"`
Code string `xml:"code,attr"`
}
Response
Структура ответа от API
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"`
}
Как пользоваться
Клиент лежит тут https://github.com/maetx777/nic.ru-golang-client.git, готов к использованию. В examples описано как создавать и удалять A/CNAME записи, а также как скачать зону целиком.
Создание утилиты
Далее немного опишу как создать консольную утилиту для управления апишкой, код загружен в репозиторий.
cmd/nicru/config.go
для начала создадим структуру для хранения значений вводимых аргументов
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/main.go
создадим файл 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`,
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
`,
}
//создаём флаги для заполнения конфига
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`)
cmd.AddCommand(addACmd()) // подключаем команду add-a
cmd.AddCommand(commitCmd()) // подключаем команду add-a
if err := cmd.Execute(); err != nil {
logrus.Infoln(err.Error())
}
}
Этот файл собирает всю утилиту, в ней описаны две команды
addAcmd - команда для добавления А-записи
commitCmd - команда для фиксации изменений в зоне
cmd/nicru/config.go
для начала инициализируем структуру конфига, которую можно использовать в разных командах:
cmd/nicru/add-a.go
Создаём команду для создания А-записи.
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-записей`,
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())
}
}
},
}
//создаём флаги для указания имён создаваемых записей, 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
}
Создаём команду для фиксации изменений в зоне
package main
import (
"fmt"
api "github.com/maetx777/nic.ru-golang-client/client"
"github.com/spf13/cobra"
)
func commitCmd() *cobra.Command {
cmd := &cobra.Command{
Use: `commit`,
Short: `фиксация изменений`,
Run: func(cmd *cobra.Command, args []string) {
//инициализируем клиента
client := api.NewClient(config)
//коммитим результат
if _, err := client.CommitZone(); err != nil {
fmt.Printf("Commit error: %s\n", err.Error())
} else {
fmt.Printf("Zone committed\n")
}
},
}
return cmd
}
Собираем утилиту:
go build ./cmd/nicru
Тестируем запуск утилиты
$ ./nicru --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 [command]
Available Commands:
add-a добавление A-записей
commit фиксация изменений
completion Generate the autocompletion script for the specified shell
help Help about any command
Flags:
--cache-path string путь до файла, где будет храниться авторизация от API (default "/tmp/.nic.ru.token")
-h, --help help for nicru
--oauth2-client-id string oauth2 client id
--oauth2-secret-id string oauth2 secret id
--password string пароль от nic.ru
--service-name string имя DNS-сервиса (default "EXAMPLE")
--username string логин от ЛК nic.ru (******/NIC-D)
--zone-name string имя DNS-зоны (default "example.com")
Use "nicru [command] --help" for more information about a command.
Смотрим хелп команды add-a
$ ./nicru add-a --help
добавление A-записей
Usage:
nicru add-a [flags]
Flags:
-h, --help help for add-a
--names strings имена, которые нужно создать
--target string куда нужно отправить имена (например, 1.2.3.4)
--ttl int TTL для созданным записей (default 600)
Global Flags:
--cache-path string путь до файла, где будет храниться авторизация от API (default "/tmp/.nic.ru.token")
--oauth2-client-id string oauth2 client id
--oauth2-secret-id string oauth2 secret id
--password string пароль от nic.ru
--service-name string имя DNS-сервиса (default "EXAMPLE")
--username string логин от ЛК nic.ru (******/NIC-D)
--zone-name string имя DNS-зоны (default "example.com")
Тестируем создание записи
$ ./nicru add-a --oauth2-client-id *** --oauth2-secret-id *** --username ***/NIC-D --password *** --service-name EXAMPLE --zone-name example.com --names foo123 --target 127.0.0.1 --ttl 3600
Added A-record: foo123 -> 127.0.0.1
Фиксируем изменения в зоне
$ ./nicru add-a --oauth2-client-id *** --oauth2-secret-id *** --username ***/NIC-D --password *** --service-name EXAMPLE --zone-name example.com
Zone committed
Запись успешно создана, аналогичным способом можно доработать создание CNAME, удаление A/CNAME, и другие функции, реализованные в модуле.
Что можно сделать для удобства
Чтобы не заморачиваться с подачей oauth2-кредов, а также логина и пароля, мы в своей версии утилиты используем vault, прямо в пакете cmd у нас есть функция, которые, при наличии файла ~/.vault-token, идёт в ваулт, забирает все нужны креды, а дальше подставляет их в команду в качестве дефолта, таким образом, нам не нужно каждый раз указывать всю портянку с авторизацией, но, при необходимости, можно это дело переопределить при каждом запуске.
Заключение
Изначально была идея делиться со всеми какими-то, полезными, по моему мнению, разработками, мы вынуждены много писать, иногда находим готовые решения, иногда - нет, и тогда мы пишем свои велосипеды.
Комментариев в коде пока что нет, в связи с тем, что сегодня карма резко понизилась из-за комментария в посте. С учётом стремительного ухудшения кармы, в скором времени я уже ничего не смогу писать тут, так что это завершающий пост.
Клиента обязательно разовьём, всё прокомментируем, разовьём ридми и сами будем им пользоваться непосредственно из гитхаба
Желаю всем мира и добра! Готов ответить на любые вопросы.
Комментарии (14)
shepardeg
09.05.2022 07:04+6Не хочется быть хейтером, но ник.ру самый худший регистратор которым довелось пользоваться. Рад что кто-то пытается облегчить его использование другим
ky0
09.05.2022 09:48Полезная штука, но вообще в 2022 году как-то ожидаешь, что если у DNS-хостинга есть апи, то есть и Терраформ…
З.Ы. — не совсем в тему, просто вспомнилось, что у Яндекс.Облака нет DNS-модуля для certbot`а. Может, кто-то уже решал эту проблему как-то, помимо костыля с виртуалкой, реализующей HTTP challenge?skymal4ik
09.05.2022 10:13Согласен, провайдер для терраформа выглядел бы заманчивее, чем библиотека на одном языке.
Borz
09.05.2022 12:04Для certbot нет, но есть для acme.sh: https://github.com/acmesh-official/acme.sh/blob/master/dnsapi/dns_yandex.sh
trawl
09.05.2022 13:00А вам прям обязательно wildcard нужны? А то может проще http challenge по каждому домену завести?
ky0
09.05.2022 13:15Проще, не спорю — но DNS-запись быстренько добавил-удалил, а в случае HTTP хоть какой-то конфиг сайта должен торчать наружу, что не всегда удобно для внутренних сервисов.
trawl
09.05.2022 13:59Я в своё время отказался от вилдкарда в сторону http challenge, и добавил локейшены в nginx, которые вели в абсолютно другой root, не имеющий отношения к самим проектам. Но да, понимаю, что такое подойдёт не всем
Borz
09.05.2022 19:20через DNS-запись можно в 2 шага делать - сперва получать значение для TXT-записи, а потом проходить окончательную проверку и генерацию
kirill-scherba
09.05.2022 18:58-1Хороший клиент. Всегда удобней пользоваться функциями на go вместо сырых вызов rest api. Спасибо, что поделились своей разработкой. Не могу сказать, что у меня много доменов в nic.ru, но при случае смогу воспользоваться вашим клиентом.
Спасибо.
Altaev
Глядя на карму автора, так не хочется быть первым комментатором - мало ли что :)
К автору и сообществу: посоветуйте, чего почитать про go для общего понимания.
tommyangelo27
Например — go.dev/tour/list или gobyexample.com
Altaev
Спасибо.