bluecat: rewrite provider implementation (#1627)
This commit is contained in:
parent
6641400f41
commit
6b8d5a0afc
7 changed files with 466 additions and 323 deletions
|
@ -18,9 +18,17 @@ Configuration for [Bluecat](https://www.bluecatnetworks.com).
|
|||
|
||||
- Code: `bluecat`
|
||||
|
||||
{{% notice note %}}
|
||||
_Please contribute by adding a CLI example._
|
||||
{{% /notice %}}
|
||||
Here is an example bash command using the Bluecat provider:
|
||||
|
||||
```bash
|
||||
BLUECAT_PASSWORD=mypassword \
|
||||
BLUECAT_DNS_VIEW=myview \
|
||||
BLUECAT_USER_NAME=myusername \
|
||||
BLUECAT_CONFIG_NAME=myconfig \
|
||||
BLUECAT_SERVER_URL=https://bam.example.com \
|
||||
BLUECAT_TTL=30 \
|
||||
lego --email myemail@example.com --dns bluecat --domains my.example.org run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
@ -54,6 +62,9 @@ More information [here](/lego/dns/#configuration-and-credentials).
|
|||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/bluecat/bluecat.toml -->
|
||||
|
|
|
@ -2,23 +2,15 @@
|
|||
package bluecat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/log"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
)
|
||||
|
||||
const (
|
||||
configType = "Configuration"
|
||||
viewType = "View"
|
||||
zoneType = "Zone"
|
||||
txtType = "TXTRecord"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bluecat/internal"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
|
@ -30,6 +22,7 @@ const (
|
|||
EnvPassword = envNamespace + "PASSWORD"
|
||||
EnvConfigName = envNamespace + "CONFIG_NAME"
|
||||
EnvDNSView = envNamespace + "DNS_VIEW"
|
||||
EnvDebug = envNamespace + "DEBUG"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
|
@ -48,6 +41,7 @@ type Config struct {
|
|||
PollingInterval time.Duration
|
||||
TTL int
|
||||
HTTPClient *http.Client
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||
|
@ -59,20 +53,24 @@ func NewDefaultConfig() *Config {
|
|||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||
},
|
||||
Debug: env.GetOrDefaultBool(EnvDebug, false),
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
token string
|
||||
client *internal.Client
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS.
|
||||
// Credentials must be passed in the environment variables: BLUECAT_SERVER_URL, BLUECAT_USER_NAME and BLUECAT_PASSWORD.
|
||||
// BLUECAT_SERVER_URL should have the scheme, hostname, and port (if required) of the authoritative Bluecat BAM server.
|
||||
// The REST endpoint will be appended.
|
||||
// In addition, the Configuration name and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and BLUECAT_DNS_VIEW.
|
||||
// Credentials must be passed in the environment variables:
|
||||
// - BLUECAT_SERVER_URL
|
||||
// It should have the scheme, hostname, and port (if required) of the authoritative Bluecat BAM server.
|
||||
// The REST endpoint will be appended.
|
||||
// - BLUECAT_USER_NAME and BLUECAT_PASSWORD
|
||||
// - BLUECAT_CONFIG_NAME (the Configuration name)
|
||||
// - BLUECAT_DNS_VIEW (external DNS View Name)
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvServerURL, EnvUserName, EnvPassword, EnvConfigName, EnvDNSView)
|
||||
if err != nil {
|
||||
|
@ -99,114 +97,104 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
|||
return nil, errors.New("bluecat: credentials missing")
|
||||
}
|
||||
|
||||
return &DNSProvider{config: config}, nil
|
||||
client := internal.NewClient(config.BaseURL)
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
return &DNSProvider{config: config, client: client}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters
|
||||
// This will *not* create a subzone to contain the TXT record,
|
||||
// so make sure the FQDN specified is within an extant zone.
|
||||
// This will *not* create a sub-zone to contain the TXT record,
|
||||
// so make sure the FQDN specified is within an existent zone.
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
err := d.login()
|
||||
err := d.client.Login(d.config.UserName, d.config.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: login: %w", err)
|
||||
}
|
||||
|
||||
viewID, err := d.lookupViewID(d.config.DNSView)
|
||||
viewID, err := d.client.LookupViewID(d.config.ConfigName, d.config.DNSView)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: lookupViewID: %w", err)
|
||||
}
|
||||
|
||||
parentZoneID, name, err := d.lookupParentZoneID(viewID, fqdn)
|
||||
parentZoneID, name, err := d.client.LookupParentZoneID(viewID, fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: lookupParentZoneID: %w", err)
|
||||
}
|
||||
|
||||
queryArgs := map[string]string{
|
||||
"parentId": strconv.FormatUint(uint64(parentZoneID), 10),
|
||||
if d.config.Debug {
|
||||
log.Infof("fqdn: %s; viewID: %d; ZoneID: %d; zone: %s", fqdn, viewID, parentZoneID, name)
|
||||
}
|
||||
|
||||
body := bluecatEntity{
|
||||
txtRecord := internal.Entity{
|
||||
Name: name,
|
||||
Type: "TXTRecord",
|
||||
Type: internal.TXTType,
|
||||
Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", d.config.TTL, fqdn, value),
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodPost, "addEntity", body, queryArgs)
|
||||
_, err = d.client.AddEntity(parentZoneID, txtRecord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
addTxtBytes, _ := io.ReadAll(resp.Body)
|
||||
addTxtResp := string(addTxtBytes)
|
||||
// addEntity responds only with body text containing the ID of the created record
|
||||
_, err = strconv.ParseUint(addTxtResp, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecat: addEntity request failed: %s", addTxtResp)
|
||||
return fmt.Errorf("bluecat: add TXT record: %w", err)
|
||||
}
|
||||
|
||||
err = d.deploy(parentZoneID)
|
||||
err = d.client.Deploy(parentZoneID)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: deploy: %w", err)
|
||||
}
|
||||
|
||||
return d.logout()
|
||||
err = d.client.Logout()
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecat: logout: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
err := d.login()
|
||||
err := d.client.Login(d.config.UserName, d.config.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: login: %w", err)
|
||||
}
|
||||
|
||||
viewID, err := d.lookupViewID(d.config.DNSView)
|
||||
viewID, err := d.client.LookupViewID(d.config.ConfigName, d.config.DNSView)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: lookupViewID: %w", err)
|
||||
}
|
||||
|
||||
parentID, name, err := d.lookupParentZoneID(viewID, fqdn)
|
||||
parentZoneID, name, err := d.client.LookupParentZoneID(viewID, fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: lookupParentZoneID: %w", err)
|
||||
}
|
||||
|
||||
queryArgs := map[string]string{
|
||||
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||||
"name": name,
|
||||
"type": txtType,
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
|
||||
txtRecord, err := d.client.GetEntityByName(parentZoneID, name, internal.TXTType)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: get TXT record: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var txtRec entityResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&txtRec)
|
||||
err = d.client.Delete(txtRecord.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
queryArgs = map[string]string{
|
||||
"objectId": strconv.FormatUint(uint64(txtRec.ID), 10),
|
||||
return fmt.Errorf("bluecat: delete TXT record: %w", err)
|
||||
}
|
||||
|
||||
resp, err = d.sendRequest(http.MethodDelete, http.MethodDelete, nil, queryArgs)
|
||||
err = d.client.Deploy(parentZoneID)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: deploy: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = d.deploy(parentID)
|
||||
err = d.client.Logout()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bluecat: logout: %w", err)
|
||||
}
|
||||
|
||||
return d.logout()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
|
|
|
@ -4,7 +4,15 @@ URL = "https://www.bluecatnetworks.com"
|
|||
Code = "bluecat"
|
||||
Since = "v0.5.0"
|
||||
|
||||
Example = ''''''
|
||||
Example = '''
|
||||
BLUECAT_PASSWORD=mypassword \
|
||||
BLUECAT_DNS_VIEW=myview \
|
||||
BLUECAT_USER_NAME=myusername \
|
||||
BLUECAT_CONFIG_NAME=myconfig \
|
||||
BLUECAT_SERVER_URL=https://bam.example.com \
|
||||
BLUECAT_TTL=30 \
|
||||
lego --email myemail@example.com --dns bluecat --domains my.example.org run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
|
@ -18,3 +26,6 @@ Example = ''''''
|
|||
BLUECAT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||
BLUECAT_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||
BLUECAT_HTTP_TIMEOUT = "API request timeout"
|
||||
|
||||
[Links]
|
||||
API = "https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0"
|
||||
|
|
|
@ -1,250 +0,0 @@
|
|||
package bluecat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JSON body for Bluecat entity requests and responses.
|
||||
type bluecatEntity struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
type entityResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
// Starts a new Bluecat API Session.
|
||||
// Authenticates using customerName, userName, password,
|
||||
// and receives a token to be used in for subsequent requests.
|
||||
func (d *DNSProvider) login() error {
|
||||
queryArgs := map[string]string{
|
||||
"username": d.config.UserName,
|
||||
"password": d.config.Password,
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodGet, "login", nil, queryArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
authBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
authResp := string(authBytes)
|
||||
|
||||
if strings.Contains(authResp, "Authentication Error") {
|
||||
msg := strings.Trim(authResp, "\"")
|
||||
return fmt.Errorf("bluecat: request failed: %s", msg)
|
||||
}
|
||||
|
||||
// Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username"
|
||||
d.token = regexp.MustCompile("BAMAuthToken: [^ ]+").FindString(authResp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroys Bluecat Session.
|
||||
func (d *DNSProvider) logout() error {
|
||||
if d.token == "" {
|
||||
// nothing to do
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodGet, "logout", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("bluecat: request failed to delete session with HTTP status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
authBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authResp := string(authBytes)
|
||||
|
||||
if !strings.Contains(authResp, "successfully") {
|
||||
msg := strings.Trim(authResp, "\"")
|
||||
return fmt.Errorf("bluecat: request failed to delete session: %s", msg)
|
||||
}
|
||||
|
||||
d.token = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup the entity ID of the configuration named in our properties.
|
||||
func (d *DNSProvider) lookupConfID() (uint, error) {
|
||||
queryArgs := map[string]string{
|
||||
"parentId": strconv.Itoa(0),
|
||||
"name": d.config.ConfigName,
|
||||
"type": configType,
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var conf entityResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&conf)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
return conf.ID, nil
|
||||
}
|
||||
|
||||
// Find the DNS view with the given name within.
|
||||
func (d *DNSProvider) lookupViewID(viewName string) (uint, error) {
|
||||
confID, err := d.lookupConfID()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
queryArgs := map[string]string{
|
||||
"parentId": strconv.FormatUint(uint64(confID), 10),
|
||||
"name": viewName,
|
||||
"type": viewType,
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var view entityResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&view)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
|
||||
return view.ID, nil
|
||||
}
|
||||
|
||||
// Return the entityId of the parent zone by recursing from the root view.
|
||||
// Also return the simple name of the host.
|
||||
func (d *DNSProvider) lookupParentZoneID(viewID uint, fqdn string) (uint, string, error) {
|
||||
parentViewID := viewID
|
||||
name := ""
|
||||
|
||||
if fqdn != "" {
|
||||
zones := strings.Split(strings.Trim(fqdn, "."), ".")
|
||||
last := len(zones) - 1
|
||||
name = zones[0]
|
||||
|
||||
for i := last; i > -1; i-- {
|
||||
zoneID, err := d.getZone(parentViewID, zones[i])
|
||||
if err != nil || zoneID == 0 {
|
||||
return parentViewID, name, err
|
||||
}
|
||||
if i > 0 {
|
||||
name = strings.Join(zones[0:i], ".")
|
||||
}
|
||||
parentViewID = zoneID
|
||||
}
|
||||
}
|
||||
|
||||
return parentViewID, name, nil
|
||||
}
|
||||
|
||||
// Get the DNS zone with the specified name under the parentId.
|
||||
func (d *DNSProvider) getZone(parentID uint, name string) (uint, error) {
|
||||
queryArgs := map[string]string{
|
||||
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||||
"name": name,
|
||||
"type": zoneType,
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
|
||||
|
||||
// Return an empty zone if the named zone doesn't exist
|
||||
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||
return 0, fmt.Errorf("bluecat: could not find zone named %s", name)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var zone entityResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&zone)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
|
||||
return zone.ID, nil
|
||||
}
|
||||
|
||||
// Deploy the DNS config for the specified entity to the authoritative servers.
|
||||
func (d *DNSProvider) deploy(entityID uint) error {
|
||||
queryArgs := map[string]string{
|
||||
"entityId": strconv.FormatUint(uint64(entityID), 10),
|
||||
}
|
||||
|
||||
resp, err := d.sendRequest(http.MethodPost, "quickDeploy", nil, queryArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send a REST request, using query parameters specified.
|
||||
// The Authorization header will be set if we have an active auth token.
|
||||
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) {
|
||||
url := fmt.Sprintf("%s/Services/REST/v1/%s", d.config.BaseURL, resource)
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if len(d.token) > 0 {
|
||||
req.Header.Set("Authorization", d.token)
|
||||
}
|
||||
|
||||
// Add all query parameters
|
||||
q := req.URL.Query()
|
||||
for argName, argVal := range queryArgs {
|
||||
q.Add(argName, argVal)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp, err := d.config.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bluecat: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
errBytes, _ := io.ReadAll(resp.Body)
|
||||
errResp := string(errBytes)
|
||||
return nil, fmt.Errorf("bluecat: request failed with HTTP status code %d\n Full message: %s",
|
||||
resp.StatusCode, errResp)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
313
providers/dns/bluecat/internal/client.go
Normal file
313
providers/dns/bluecat/internal/client.go
Normal file
|
@ -0,0 +1,313 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Object types.
|
||||
const (
|
||||
ConfigType = "Configuration"
|
||||
ViewType = "View"
|
||||
ZoneType = "Zone"
|
||||
TXTType = "TXTRecord"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
HTTPClient *http.Client
|
||||
|
||||
baseURL string
|
||||
|
||||
token string
|
||||
tokenExp *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
||||
baseURL: baseURL,
|
||||
tokenExp: regexp.MustCompile("BAMAuthToken: [^ ]+"),
|
||||
}
|
||||
}
|
||||
|
||||
// Login Logs in as API user.
|
||||
// Authenticates and receives a token to be used in for subsequent requests.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/login/9.1.0
|
||||
func (c *Client) Login(username, password string) error {
|
||||
queryArgs := map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
resp, err := c.sendRequest(http.MethodGet, "login", nil, queryArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Resource: "login",
|
||||
Message: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
authBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authResp := string(authBytes)
|
||||
if strings.Contains(authResp, "Authentication Error") {
|
||||
return fmt.Errorf("request failed: %s", strings.Trim(authResp, `"`))
|
||||
}
|
||||
|
||||
// Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username"
|
||||
c.token = c.tokenExp.FindString(authResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout Logs out of the current API session.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/logout/9.1.0
|
||||
func (c *Client) Logout() error {
|
||||
if c.token == "" {
|
||||
// nothing to do
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := c.sendRequest(http.MethodGet, "logout", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Resource: "logout",
|
||||
Message: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
authBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authResp := string(authBytes)
|
||||
if !strings.Contains(authResp, "successfully") {
|
||||
return fmt.Errorf("request failed to delete session: %s", strings.Trim(authResp, `"`))
|
||||
}
|
||||
|
||||
c.token = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deploy the DNS config for the specified entity to the authoritative servers.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/POST/v1/quickDeploy/9.1.0
|
||||
func (c *Client) Deploy(entityID uint) error {
|
||||
queryArgs := map[string]string{
|
||||
"entityId": strconv.FormatUint(uint64(entityID), 10),
|
||||
}
|
||||
|
||||
resp, err := c.sendRequest(http.MethodPost, "quickDeploy", nil, queryArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API doc says that 201 is expected but in the reality 200 is return.
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Resource: "quickDeploy",
|
||||
Message: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddEntity A generic method for adding configurations, DNS zones, and DNS resource records.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/POST/v1/addEntity/9.1.0
|
||||
func (c *Client) AddEntity(parentID uint, entity Entity) (uint64, error) {
|
||||
queryArgs := map[string]string{
|
||||
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||||
}
|
||||
|
||||
resp, err := c.sendRequest(http.MethodPost, "addEntity", entity, queryArgs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return 0, &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Resource: "addEntity",
|
||||
Message: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
addTxtBytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
// addEntity responds only with body text containing the ID of the created record
|
||||
addTxtResp := string(addTxtBytes)
|
||||
id, err := strconv.ParseUint(addTxtResp, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("addEntity request failed: %s", addTxtResp)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetEntityByName Returns objects from the database referenced by their database ID and with its properties fields populated.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/getEntityById/9.1.0
|
||||
func (c *Client) GetEntityByName(parentID uint, name, objType string) (*EntityResponse, error) {
|
||||
queryArgs := map[string]string{
|
||||
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||||
"name": name,
|
||||
"type": objType,
|
||||
}
|
||||
|
||||
resp, err := c.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return nil, &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Resource: "getEntityByName",
|
||||
Message: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
var txtRec EntityResponse
|
||||
if err = json.NewDecoder(resp.Body).Decode(&txtRec); err != nil {
|
||||
return nil, fmt.Errorf("JSON decode: %w", err)
|
||||
}
|
||||
|
||||
return &txtRec, nil
|
||||
}
|
||||
|
||||
// Delete Deletes an object using the generic delete method.
|
||||
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/DELETE/v1/delete/9.1.0
|
||||
func (c *Client) Delete(objectID uint) error {
|
||||
queryArgs := map[string]string{
|
||||
"objectId": strconv.FormatUint(uint64(objectID), 10),
|
||||
}
|
||||
|
||||
resp, err := c.sendRequest(http.MethodDelete, "delete", nil, queryArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API doc says that 204 is expected but in the reality 200 is return.
|
||||
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Resource: "delete",
|
||||
Message: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupViewID Find the DNS view with the given name within.
|
||||
func (c *Client) LookupViewID(configName, viewName string) (uint, error) {
|
||||
// Lookup the entity ID of the configuration named in our properties.
|
||||
conf, err := c.GetEntityByName(0, configName, ConfigType)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
view, err := c.GetEntityByName(conf.ID, viewName, ViewType)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return view.ID, nil
|
||||
}
|
||||
|
||||
// LookupParentZoneID Return the entityId of the parent zone by recursing from the root view.
|
||||
// Also return the simple name of the host.
|
||||
func (c *Client) LookupParentZoneID(viewID uint, fqdn string) (uint, string, error) {
|
||||
if fqdn == "" {
|
||||
return viewID, "", nil
|
||||
}
|
||||
|
||||
zones := strings.Split(strings.Trim(fqdn, "."), ".")
|
||||
|
||||
name := zones[0]
|
||||
parentViewID := viewID
|
||||
|
||||
for i := len(zones) - 1; i > -1; i-- {
|
||||
zone, err := c.GetEntityByName(parentViewID, zones[i], ZoneType)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("could not find zone named %s: %w", name, err)
|
||||
}
|
||||
|
||||
if zone == nil || zone.ID == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
name = strings.Join(zones[0:i], ".")
|
||||
}
|
||||
|
||||
parentViewID = zone.ID
|
||||
}
|
||||
|
||||
return parentViewID, name, nil
|
||||
}
|
||||
|
||||
// Send a REST request, using query parameters specified.
|
||||
// The Authorization header will be set if we have an active auth token.
|
||||
func (c *Client) sendRequest(method, resource string, payload interface{}, queryParams map[string]string) (*http.Response, error) {
|
||||
url := fmt.Sprintf("%s/Services/REST/v1/%s", c.baseURL, resource)
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if c.token != "" {
|
||||
req.Header.Set("Authorization", c.token)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
for k, v := range queryParams {
|
||||
q.Set(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
return c.HTTPClient.Do(req)
|
||||
}
|
41
providers/dns/bluecat/internal/client_test.go
Normal file
41
providers/dns/bluecat/internal/client_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient_LookupParentZoneID(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := NewClient(server.URL)
|
||||
|
||||
mux.HandleFunc("/Services/REST/v1/getEntityByName", func(rw http.ResponseWriter, req *http.Request) {
|
||||
query := req.URL.Query()
|
||||
|
||||
if query.Get("name") == "com" {
|
||||
_ = json.NewEncoder(rw).Encode(EntityResponse{
|
||||
ID: 2,
|
||||
Name: "com",
|
||||
Type: ZoneType,
|
||||
Properties: "test",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(rw, "{}", http.StatusOK)
|
||||
})
|
||||
|
||||
parentID, name, err := client.LookupParentZoneID(2, "foo.example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 2, parentID)
|
||||
assert.Equal(t, "foo.example", name)
|
||||
}
|
29
providers/dns/bluecat/internal/types.go
Normal file
29
providers/dns/bluecat/internal/types.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package internal
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Entity JSON body for Bluecat entity requests.
|
||||
type Entity struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
// EntityResponse JSON body for Bluecat entity responses.
|
||||
type EntityResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Properties string `json:"properties"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Resource string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (a APIError) Error() string {
|
||||
return fmt.Sprintf("resource: %s, status code: %d, message: %s", a.Resource, a.StatusCode, a.Message)
|
||||
}
|
Loading…
Reference in a new issue