Skip to content

Commit 946f51b

Browse files
Major updates and new features
- 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
1 parent 28f54e4 commit 946f51b

9 files changed

+1545
-54
lines changed

client.go

+25-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package aoscxgo
22

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

40-
cookie, err := login(c.Transport, c.Hostname, c.Version, c.Username, c.Password)
42+
cookie, csrf, err := login(c.Transport, c.Hostname, c.Version, c.Username, c.Password)
4143

4244
if err != nil {
4345
return nil, err
4446
}
4547
c.Cookie = cookie
46-
48+
c.Csrf = csrf
4749
return c, err
4850
}
4951

52+
// Logout calls the logout endpoint to clear the session.
53+
func (c *Client) Logout() error {
54+
if c == nil {
55+
return errors.New("nil value to Logout")
56+
}
57+
url := fmt.Sprintf("https://%s/rest/%s/logout", c.Hostname, c.Version)
58+
resp := logout(c.Transport, c.Cookie, c.Csrf, url)
59+
if resp.StatusCode != http.StatusOK {
60+
return errors.New(resp.Status)
61+
}
62+
return nil
63+
}
64+
5065
// login performs POST to create a cookie for authentication to the given IP with the provided credentials.
51-
func login(http_transport *http.Transport, ip string, rest_version string, username string, password string) (*http.Cookie, error) {
66+
func login(http_transport *http.Transport, ip string, rest_version string, username string, password string) (*http.Cookie, string, error) {
5267
url := fmt.Sprintf("https://%s/rest/%s/login?username=%s&password=%s", ip, rest_version, username, password)
5368
req, _ := http.NewRequest("POST", url, nil)
5469
req.Header.Set("accept", "*/*")
70+
req.Header.Set("x-use-csrf-token", "true")
5571
req.Close = false
5672

5773
res, err := http_transport.RoundTrip(req)
5874
if res.Status != "200 OK" {
5975
log.Fatalf("Got error while connecting to switch %s Error %s", res.Status, err)
60-
return nil, err
76+
return nil, "", err
6177
}
6278

6379
fmt.Println("Login Successful")
80+
81+
csrf := res.Header["X-Csrf-Token"][0]
6482
cookie := res.Cookies()[0]
6583

66-
return cookie, err
84+
return cookie, csrf, err
6785
}
6886

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

7594
req.AddCookie(cookie)

full_config.go

+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package aoscxgo
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"io/ioutil"
8+
"log"
9+
"net/http"
10+
"time"
11+
12+
"github.com/google/go-cmp/cmp"
13+
)
14+
15+
type FullConfig struct {
16+
17+
// Connection properties.
18+
FileName string `json:"filename"`
19+
//Hash hash.Hash `json:"hash"`
20+
Config string `json:"config"`
21+
uri string `json:"uri"`
22+
}
23+
24+
// Create performs POST to create VLAN configuration on the given Client object.
25+
func (fc *FullConfig) Create(c *Client) (*http.Response, error) {
26+
if fc.FileName == "" {
27+
return nil, &RequestError{
28+
StatusCode: "Missing FileName",
29+
Err: errors.New("Create Error"),
30+
}
31+
}
32+
33+
config_str, err := fc.ReadConfigFile(fc.FileName)
34+
35+
if err != nil {
36+
return nil, &RequestError{
37+
StatusCode: "Error in reading config file",
38+
Err: err,
39+
}
40+
}
41+
42+
res, body := fc.ValidateConfig(c, config_str)
43+
44+
if body == nil {
45+
return res, &RequestError{
46+
StatusCode: res.Status,
47+
Err: errors.New("Validation Error"),
48+
}
49+
50+
} else if body["state"] == "success" {
51+
res2, body2 := fc.ApplyConfig(c, config_str)
52+
if body2["state"] != "success" {
53+
errors_dict := body2["errors"].([]interface{})
54+
error_str := convert_errors(errors_dict)
55+
56+
return res2, &RequestError{
57+
StatusCode: "Error in applying config error : \n" + error_str,
58+
Err: errors.New("Apply Error"),
59+
}
60+
} else {
61+
log.Println("New Config Applied Successfully")
62+
fc.Get(c)
63+
return res2, nil
64+
}
65+
} else if res != nil && body != nil {
66+
errors_dict := body["errors"].([]interface{})
67+
errors_dict = errors_dict
68+
error_str := convert_errors(errors_dict)
69+
70+
return res, &RequestError{
71+
StatusCode: "Error in validating config error : \n" + error_str,
72+
Err: errors.New("Apply Error"),
73+
}
74+
75+
}
76+
77+
return res, &RequestError{
78+
StatusCode: res.Status,
79+
Err: errors.New("Validation Error"),
80+
}
81+
}
82+
83+
// Get performs GET to retrieve Running configuration for the given Client object.
84+
func (fc *FullConfig) Get(c *Client) error {
85+
base_uri := "configs/running-config"
86+
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
87+
res, _ := get_accept_text(c, url)
88+
89+
if res.Status != "200 OK" {
90+
return &RequestError{
91+
StatusCode: res.Status + url,
92+
Err: errors.New("Retrieval Error"),
93+
}
94+
}
95+
96+
// Read the content
97+
var bodyBytes []byte
98+
if res.Body != nil {
99+
bodyBytes, _ = ioutil.ReadAll(res.Body)
100+
}
101+
// Restore the io.ReadCloser to its original state
102+
res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
103+
// Use the content
104+
bodyString := string(bodyBytes)
105+
106+
// config := bytes.NewBuffer(nil)
107+
// config, _ = ioutil.ReadAll(res.Body)
108+
fc.Config = bodyString
109+
110+
return nil
111+
}
112+
113+
func (fc *FullConfig) ReadConfigFile(filename string) (string, error) {
114+
config_contents, err := ioutil.ReadFile(filename)
115+
116+
if err != nil {
117+
err_str := "Unable to read file " + filename
118+
return "", &RequestError{
119+
StatusCode: err_str,
120+
Err: err,
121+
}
122+
}
123+
config_str := string(config_contents)
124+
return config_str, nil
125+
126+
}
127+
128+
// Formats the errors provided by dryrun for user
129+
func convert_errors(errors_list []interface{}) string {
130+
errors_str := ""
131+
for _, error_dict := range errors_list {
132+
tmp_dict := error_dict.(map[string]interface{})
133+
line_num := tmp_dict["line"]
134+
line_float := line_num.(float64)
135+
line_int := int(line_float)
136+
errors_str += "line "
137+
line_num_str := fmt.Sprintf("%d", line_int)
138+
errors_str += line_num_str
139+
errors_str += " | "
140+
errors_str += tmp_dict["message"].(string)
141+
errors_str += "\n"
142+
}
143+
return errors_str
144+
}
145+
146+
// Sets Config Attribute
147+
func (fc *FullConfig) SetConfig(config string) error {
148+
fc.Config = config
149+
150+
return nil
151+
}
152+
153+
// Compares supplied string Config to stored Object
154+
func (fc *FullConfig) CompareConfig(new_config string) string {
155+
156+
return cmp.Diff(fc.Config, new_config)
157+
}
158+
159+
// Compares supplied string Config to stored Object
160+
func (fc *FullConfig) DownloadConfig(c *Client, filename string) error {
161+
base_uri := "configs/running-config"
162+
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
163+
res, _ := get_accept_text(c, url)
164+
165+
if res.Status != "200 OK" {
166+
return &RequestError{
167+
StatusCode: res.Status + url,
168+
Err: errors.New("Retrieval Error"),
169+
}
170+
}
171+
172+
// Read the content
173+
var bodyBytes []byte
174+
if res.Body != nil {
175+
bodyBytes, _ = ioutil.ReadAll(res.Body)
176+
}
177+
// Restore the io.ReadCloser to its original state
178+
res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
179+
// Use the content
180+
bodyString := string(bodyBytes)
181+
182+
err := ioutil.WriteFile(filename, []byte(bodyString), 0644)
183+
if err != nil {
184+
panic(err)
185+
}
186+
187+
return err
188+
}
189+
190+
// Validates supplied CLI configuration as string using dryrun
191+
func (fc *FullConfig) ValidateConfig(c *Client, config string) (*http.Response, map[string]interface{}) {
192+
base_uri := "configs/running-config"
193+
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
194+
dryrun_url := url + "?dryrun=validate"
195+
196+
json_body := bytes.NewBufferString(config)
197+
198+
res := post(c, dryrun_url, json_body)
199+
200+
if res.Status != "200 OK" && res.Status != "202 Accepted" {
201+
return res, nil
202+
}
203+
204+
dryrun_url = url + "?dryrun"
205+
206+
res2, body := get(c, dryrun_url)
207+
208+
iterations := 10
209+
210+
for iterations > 0 {
211+
iterations -= 1
212+
if body["state"] == "success" || body["state"] == "error" {
213+
break
214+
}
215+
time.Sleep(2 * time.Second)
216+
res2, body = get(c, dryrun_url)
217+
}
218+
219+
return res2, body
220+
}
221+
222+
func (fc *FullConfig) ApplyConfig(c *Client, config string) (*http.Response, map[string]interface{}) {
223+
base_uri := "configs/running-config"
224+
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
225+
dryrun_url := url + "?dryrun=apply"
226+
227+
json_body := bytes.NewBufferString(config)
228+
229+
res := post(c, dryrun_url, json_body)
230+
231+
if res.Status != "200 OK" && res.Status != "202 Accepted" {
232+
return res, nil
233+
}
234+
235+
dryrun_url = url + "?dryrun"
236+
237+
res2, body := get(c, dryrun_url)
238+
239+
iterations := 10
240+
241+
for iterations > 0 {
242+
iterations -= 1
243+
if body["state"] == "success" || body["state"] == "error" {
244+
break
245+
}
246+
time.Sleep(2 * time.Second)
247+
res2, body = get(c, dryrun_url)
248+
}
249+
250+
return res2, body
251+
}

go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/aruba/aoscxgo
22

3-
go 1.17
3+
go 1.18
4+
5+
require golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect

interface.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Interface struct {
1616
AdminState string `json:"admin"`
1717
InterfaceDetails map[string]interface{} `json:"details"`
1818
materialized bool `json:"materialized"`
19+
uri string `json:"uri"`
1920
}
2021

2122
// checkName validates if interface Name is valid or not
@@ -53,7 +54,11 @@ func (i *Interface) checkValues() error {
5354
// Create performs POST to create Interface configuration on the given Client object.
5455
func (i *Interface) Create(c *Client) error {
5556
base_uri := "system/interfaces"
56-
url := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
57+
url_str := "https://" + c.Hostname + "/rest/" + c.Version + "/" + base_uri
58+
59+
int_str := url.PathEscape(i.Name)
60+
61+
i.uri = "/rest/" + c.Version + "/" + base_uri + "/" + int_str
5762

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

8085
json_body := bytes.NewBuffer(postBody)
8186

82-
res := post(c.Transport, c.Cookie, url, json_body)
87+
res := post(c, url_str, json_body)
8388

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

125130
json_body := bytes.NewBuffer(patchBody)
126131

127-
res := patch(c.Transport, c.Cookie, url, json_body)
132+
res := patch(c, url, json_body)
128133

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

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

153158
//need logic for handling interfaces between platforms
154159

155-
res := put(c.Transport, c.Cookie, url, json_body)
160+
res := put(c, url, json_body)
156161

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

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

174-
res, body := get(c.Transport, c.Cookie, url)
179+
res, body := get(c, url)
175180

176181
if res.Status != "200 OK" {
177182
i.materialized = false

0 commit comments

Comments
 (0)