Skip to content

Commit

Permalink
Major updates and new features
Browse files Browse the repository at this point in the history
- includes support for CSRF token
- includes new modules for L3 configuration
- includes new modules for FullConfig operations
- refactors modules to use Client object
- improves error handling and reporting
  • Loading branch information
tchiapuziowong committed Jun 18, 2024
1 parent 28f54e4 commit 946f51b
Show file tree
Hide file tree
Showing 9 changed files with 1,545 additions and 54 deletions.
31 changes: 25 additions & 6 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package aoscxgo

import (
"crypto/tls"
"errors"
"fmt"
"log"
"net/http"
Expand All @@ -15,6 +16,7 @@ type Client struct {
Version string `json:"version"`
// Generated after Connect
Cookie *http.Cookie `json:"cookie"`
Csrf string `json:"Csrf"`
// HTTP transport options. Note that the VerifyCertificate setting is
// only used if you do not specify a HTTP transport yourself.
VerifyCertificate bool `json:"verify_certificate"`
Expand All @@ -37,39 +39,56 @@ func Connect(c *Client) (*Client, error) {
c.Transport = tr
}

cookie, err := login(c.Transport, c.Hostname, c.Version, c.Username, c.Password)
cookie, csrf, err := login(c.Transport, c.Hostname, c.Version, c.Username, c.Password)

if err != nil {
return nil, err
}
c.Cookie = cookie

c.Csrf = csrf
return c, err
}

// Logout calls the logout endpoint to clear the session.
func (c *Client) Logout() error {
if c == nil {
return errors.New("nil value to Logout")
}
url := fmt.Sprintf("https://%s/rest/%s/logout", c.Hostname, c.Version)
resp := logout(c.Transport, c.Cookie, c.Csrf, url)
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
}
return nil
}

// login performs POST to create a cookie for authentication to the given IP with the provided credentials.
func login(http_transport *http.Transport, ip string, rest_version string, username string, password string) (*http.Cookie, error) {
func login(http_transport *http.Transport, ip string, rest_version string, username string, password string) (*http.Cookie, string, error) {
url := fmt.Sprintf("https://%s/rest/%s/login?username=%s&password=%s", ip, rest_version, username, password)
req, _ := http.NewRequest("POST", url, nil)
req.Header.Set("accept", "*/*")
req.Header.Set("x-use-csrf-token", "true")
req.Close = false

res, err := http_transport.RoundTrip(req)
if res.Status != "200 OK" {
log.Fatalf("Got error while connecting to switch %s Error %s", res.Status, err)
return nil, err
return nil, "", err
}

fmt.Println("Login Successful")

csrf := res.Header["X-Csrf-Token"][0]
cookie := res.Cookies()[0]

return cookie, err
return cookie, csrf, err
}

// logout performs POST to logout using a cookie from the given URL.
func logout(http_transport *http.Transport, cookie *http.Cookie, url string) *http.Response {
func logout(http_transport *http.Transport, cookie *http.Cookie, csrf string, url string) *http.Response {
req, _ := http.NewRequest("POST", url, nil)
req.Header.Set("accept", "*/*")
req.Header.Set("x-csrf-token", csrf)
req.Close = false

req.AddCookie(cookie)
Expand Down
251 changes: 251 additions & 0 deletions full_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package aoscxgo

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"

"github.com/google/go-cmp/cmp"
)

type FullConfig struct {

// Connection properties.
FileName string `json:"filename"`
//Hash hash.Hash `json:"hash"`
Config string `json:"config"`
uri string `json:"uri"`
}

// Create performs POST to create VLAN configuration on the given Client object.
func (fc *FullConfig) Create(c *Client) (*http.Response, error) {
if fc.FileName == "" {
return nil, &RequestError{
StatusCode: "Missing FileName",
Err: errors.New("Create Error"),
}
}

config_str, err := fc.ReadConfigFile(fc.FileName)

if err != nil {
return nil, &RequestError{
StatusCode: "Error in reading config file",
Err: err,
}
}

res, body := fc.ValidateConfig(c, config_str)

if body == nil {
return res, &RequestError{
StatusCode: res.Status,
Err: errors.New("Validation Error"),
}

} else if body["state"] == "success" {
res2, body2 := fc.ApplyConfig(c, config_str)
if body2["state"] != "success" {
errors_dict := body2["errors"].([]interface{})
error_str := convert_errors(errors_dict)

return res2, &RequestError{
StatusCode: "Error in applying config error : \n" + error_str,
Err: errors.New("Apply Error"),
}
} else {
log.Println("New Config Applied Successfully")
fc.Get(c)
return res2, nil
}
} else if res != nil && body != nil {
errors_dict := body["errors"].([]interface{})
errors_dict = errors_dict
error_str := convert_errors(errors_dict)

return res, &RequestError{
StatusCode: "Error in validating config error : \n" + error_str,
Err: errors.New("Apply Error"),
}

}

return res, &RequestError{
StatusCode: res.Status,
Err: errors.New("Validation Error"),
}
}

// Get performs GET to retrieve Running configuration for the given Client object.
func (fc *FullConfig) Get(c *Client) error {
base_uri := "configs/running-config"
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
res, _ := get_accept_text(c, url)

if res.Status != "200 OK" {
return &RequestError{
StatusCode: res.Status + url,
Err: errors.New("Retrieval Error"),
}
}

// Read the content
var bodyBytes []byte
if res.Body != nil {
bodyBytes, _ = ioutil.ReadAll(res.Body)
}
// Restore the io.ReadCloser to its original state
res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
// Use the content
bodyString := string(bodyBytes)

// config := bytes.NewBuffer(nil)
// config, _ = ioutil.ReadAll(res.Body)
fc.Config = bodyString

return nil
}

func (fc *FullConfig) ReadConfigFile(filename string) (string, error) {
config_contents, err := ioutil.ReadFile(filename)

if err != nil {
err_str := "Unable to read file " + filename
return "", &RequestError{
StatusCode: err_str,
Err: err,
}
}
config_str := string(config_contents)
return config_str, nil

}

// Formats the errors provided by dryrun for user
func convert_errors(errors_list []interface{}) string {
errors_str := ""
for _, error_dict := range errors_list {
tmp_dict := error_dict.(map[string]interface{})
line_num := tmp_dict["line"]
line_float := line_num.(float64)
line_int := int(line_float)
errors_str += "line "
line_num_str := fmt.Sprintf("%d", line_int)
errors_str += line_num_str
errors_str += " | "
errors_str += tmp_dict["message"].(string)
errors_str += "\n"
}
return errors_str
}

// Sets Config Attribute
func (fc *FullConfig) SetConfig(config string) error {
fc.Config = config

return nil
}

// Compares supplied string Config to stored Object
func (fc *FullConfig) CompareConfig(new_config string) string {

return cmp.Diff(fc.Config, new_config)
}

// Compares supplied string Config to stored Object
func (fc *FullConfig) DownloadConfig(c *Client, filename string) error {
base_uri := "configs/running-config"
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
res, _ := get_accept_text(c, url)

if res.Status != "200 OK" {
return &RequestError{
StatusCode: res.Status + url,
Err: errors.New("Retrieval Error"),
}
}

// Read the content
var bodyBytes []byte
if res.Body != nil {
bodyBytes, _ = ioutil.ReadAll(res.Body)
}
// Restore the io.ReadCloser to its original state
res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
// Use the content
bodyString := string(bodyBytes)

err := ioutil.WriteFile(filename, []byte(bodyString), 0644)
if err != nil {
panic(err)
}

return err
}

// Validates supplied CLI configuration as string using dryrun
func (fc *FullConfig) ValidateConfig(c *Client, config string) (*http.Response, map[string]interface{}) {
base_uri := "configs/running-config"
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
dryrun_url := url + "?dryrun=validate"

json_body := bytes.NewBufferString(config)

res := post(c, dryrun_url, json_body)

if res.Status != "200 OK" && res.Status != "202 Accepted" {
return res, nil
}

dryrun_url = url + "?dryrun"

res2, body := get(c, dryrun_url)

iterations := 10

for iterations > 0 {
iterations -= 1
if body["state"] == "success" || body["state"] == "error" {
break
}
time.Sleep(2 * time.Second)
res2, body = get(c, dryrun_url)
}

return res2, body
}

func (fc *FullConfig) ApplyConfig(c *Client, config string) (*http.Response, map[string]interface{}) {
base_uri := "configs/running-config"
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
dryrun_url := url + "?dryrun=apply"

json_body := bytes.NewBufferString(config)

res := post(c, dryrun_url, json_body)

if res.Status != "200 OK" && res.Status != "202 Accepted" {
return res, nil
}

dryrun_url = url + "?dryrun"

res2, body := get(c, dryrun_url)

iterations := 10

for iterations > 0 {
iterations -= 1
if body["state"] == "success" || body["state"] == "error" {
break
}
time.Sleep(2 * time.Second)
res2, body = get(c, dryrun_url)
}

return res2, body
}
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/aruba/aoscxgo

go 1.17
go 1.18

require golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
17 changes: 11 additions & 6 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Interface struct {
AdminState string `json:"admin"`
InterfaceDetails map[string]interface{} `json:"details"`
materialized bool `json:"materialized"`
uri string `json:"uri"`
}

// checkName validates if interface Name is valid or not
Expand Down Expand Up @@ -53,7 +54,11 @@ func (i *Interface) checkValues() error {
// Create performs POST to create Interface configuration on the given Client object.
func (i *Interface) Create(c *Client) error {
base_uri := "system/interfaces"
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
url_str := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri

int_str := url.PathEscape(i.Name)

i.uri = "/rest/" + c.Version + "/" + base_uri + "/" + int_str

err := i.checkValues()
if err != nil {
Expand All @@ -79,7 +84,7 @@ func (i *Interface) Create(c *Client) error {

json_body := bytes.NewBuffer(postBody)

res := post(c.Transport, c.Cookie, url, json_body)
res := post(c, url_str, json_body)

if res.Status != "201 Created" {
return &RequestError{
Expand Down Expand Up @@ -124,7 +129,7 @@ func (i *Interface) Update(c *Client) error {

json_body := bytes.NewBuffer(patchBody)

res := patch(c.Transport, c.Cookie, url, json_body)
res := patch(c, url, json_body)

if res.Status != "204 No Content" {
return &RequestError{
Expand All @@ -148,11 +153,11 @@ func (i *Interface) Delete(c *Client) error {
json_body := bytes.NewBuffer(putBody)

url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri + "/" + int_str
//res := delete(c.Transport, c.Cookie, url)
//res := delete(c, url)

//need logic for handling interfaces between platforms

res := put(c.Transport, c.Cookie, url, json_body)
res := put(c, url, json_body)

if res.Status != "204 No Content" && res.Status != "200 OK" {
return &RequestError{
Expand All @@ -171,7 +176,7 @@ func (i *Interface) Get(c *Client) error {

url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri + "/" + int_str + ""

res, body := get(c.Transport, c.Cookie, url)
res, body := get(c, url)

if res.Status != "200 OK" {
i.materialized = false
Expand Down
Loading

0 comments on commit 946f51b

Please sign in to comment.