test build
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2023-09-28 11:55:15 +10:00
parent 46f151f599
commit 95a48a89a6
25 changed files with 2358 additions and 12 deletions

326
local/go-ucs/api/api.go Executable file
View File

@@ -0,0 +1,326 @@
package api
import (
"encoding/xml"
"errors"
"fmt"
"github.com/dnaeon/go-ucs/version"
)
// The remote API endpoint to which we POST requests.
const apiEndpoint = "nuova"
// The UserAgent that we use for our requests.
const userAgent = "go-ucs/" + version.Version
// BaseResponse contains the base attributes as returned in a response from a
// Cisco UCS API endpoint.
type BaseResponse struct {
Cookie string `xml:"cookie,attr"`
Response string `xml:"response,attr"`
ErrorCode string `xml:"errorCode,attr,omitempty"`
InvocationResult string `xml:"invocationResult,attr,omitempty"`
ErrorDescription string `xml:"errorDescr,attr,omitempty"`
}
// IsError returns a boolean indicating whether the response contains errors.
func (b *BaseResponse) IsError() bool {
return b.ErrorCode != ""
}
// Error implements the error interface.
func (b *BaseResponse) Error() string {
return fmt.Sprintf("%s: %s (code %s)", b.ErrorDescription, b.InvocationResult, b.ErrorCode)
}
// ToError creates a new error.Error from the error response fields.
func (b *BaseResponse) ToError() error {
return errors.New(b.Error())
}
// AaaLoginRequest is the type which is sent during initial login
// in order to obtain authentication cookie.
type AaaLoginRequest struct {
XMLName xml.Name `xml:"aaaLogin"`
InName string `xml:"inName,attr"`
InPassword string `xml:"inPassword,attr"`
}
// AaaLoginResponse represents the response after a successful login to UCS manager.
type AaaLoginResponse struct {
BaseResponse
XMLName xml.Name `xml:"aaaLogin"`
OutCookie string `xml:"outCookie,attr,omitempty"`
OutRefreshPeriod int `xml:"outRefreshPeriod,attr,omitempty"`
OutPriv string `xml:"outPriv,attr,omitempty"`
OutDomains string `xml:"outDomains,attr,omitempty"`
OutChannel string `xml:"outChannel,attr,omitempty"`
OutEvtChannel string `xml:"outEvtChannel,attr,omitempty"`
OutName string `xml:"outName,attr,omitempty"`
OutVersion string `xml:"outVersion,attr,omitempty"`
OutSessionId string `xml:"outSessionId,attr,omitempty"`
}
// AaaRefreshRequest type is used for sending a request to the remote API endpoint for
// refreshing a session using the 47-character cookie obtained from a previous refresh
// or during the initial authentication as returned in a AaaLoginResponse.
type AaaRefreshRequest struct {
XMLName xml.Name `xml:"aaaRefresh"`
InName string `xml:"inName,attr"`
InPassword string `xml:"inPassword,attr"`
InCookie string `xml:"inCookie,attr"`
}
// AaaRefreshResponse is the response associated to a AaaRefreshRequest.
type AaaRefreshResponse struct {
XMLName xml.Name `xml:"aaaRefresh"`
AaaLoginResponse
}
// AaaLogoutRequest type is used for sending a request to invalidate an existing
// authentication cookie.
type AaaLogoutRequest struct {
XMLName xml.Name `xml:"aaaLogout"`
InCookie string `xml:"inCookie,attr"`
}
// AaaLogoutResponse represents the type that is returned after a call to aaaLogout method.
type AaaLogoutResponse struct {
BaseResponse
XMLName xml.Name `xml:"aaaLogout"`
OutStatus string `xml:"outStatus,attr,omitempty"`
}
// AaaKeepAliveRequest type is used for sending a request for keeping a session active
// until the default session time expires.
type AaaKeepAliveRequest struct {
XMLName xml.Name `xml:"aaaKeepAlive"`
Cookie string `xml:"cookie,attr"`
}
// AaaKeepAliveResponse is the response type associated with a AaaKeepAliveRequest.
type AaaKeepAliveResponse struct {
BaseResponse
XMLName xml.Name `xml:"aaaKeepAlive"`
Cookie string `xml:"cookie,attr"`
}
// ConfigResolveDnRequest type is used for constructing requests that retrieve a
// single managed object with the given DN.
type ConfigResolveDnRequest struct {
XMLName xml.Name `xml:"configResolveDn"`
Cookie string `xml:"cookie,attr"`
Dn string `xml:"dn,attr"`
InHierarchical string `xml:"inHierarchical,attr,omitempty"`
}
// ConfigResolveDnResponse is the type associated with a ConfigResolveDnRequest type.
// Specific classes contained within OutConfig should be xml.Unmarshal'ed first.
type ConfigResolveDnResponse struct {
BaseResponse
XMLName xml.Name `xml:"configResolveDn"`
Dn string `xml:"dn,attr"`
OutConfig InnerXml `xml:"outConfig"`
}
// Dn represents a single managed object DN.
type Dn struct {
XMLName xml.Name `xml:"dn"`
Value string `xml:"value,attr"`
}
// NewDn creates a new DN value.
func NewDn(value string) Dn {
dn := Dn{
Value: value,
}
return dn
}
// ConfigResolveDnsRequest type is used for constructing requests that retrieve
// managed objects for a list of given DNs.
type ConfigResolveDnsRequest struct {
XMLName xml.Name `xml:"configResolveDns"`
Cookie string `xml:"cookie,attr"`
InHierarchical string `xml:"inHierarchical,attr,omitempty"`
InDns []Dn `xml:"inDns>dn"`
}
// ConfigResolveDnsResponse is the response type associated with a ConfigResolveDnsRequest.
// The managed objects within OutConfigs field should be xml.Unmarshal'ed.
type ConfigResolveDnsResponse struct {
BaseResponse
XMLName xml.Name `xml:"configResolveDns"`
OutUnresolved []Dn `xml:"outUnresolved>dn"`
OutConfigs InnerXml `xml:"outConfigs"`
}
// InnerXml represents a generic configuration retrieved by the various query methods.
// After a successful result from a query method a client should unmarshal the data
// contained within an InnerXml to the specific managed object.
type InnerXml struct {
XMLName xml.Name
Inner []byte `xml:",innerxml"`
}
// ConfigResolveClassRequest type is used for constructing requests that retrieve
// managed objects of a given class.
type ConfigResolveClassRequest struct {
XMLName xml.Name `xml:"configResolveClass"`
Cookie string `xml:"cookie,attr"`
ClassId string `xml:"classId,attr"`
InHierarchical string `xml:"inHierarchical,attr,omitempty"`
InFilter FilterAny `xml:"inFilter>any,omitempty"`
}
// ConfigResolveClassResponse is the type associated with a ConfigResolveClassRequest.
// Specific classes contained within OutConfigs should be xml.Unmarshal'ed first.
type ConfigResolveClassResponse struct {
BaseResponse
XMLName xml.Name `xml:"configResolveClass"`
OutConfigs InnerXml `xml:"outConfigs"`
}
// Id represents an ID of a class.
type Id struct {
XMLName xml.Name `xml:"Id"`
Value string `xml:"value,attr"`
}
// NewId creates a new class id.
func NewId(value string) Id {
id := Id{
Value: value,
}
return id
}
// ConfigResolveClassesRequest type is used for constructing requests that retrieve managed
// objects in several classes.
type ConfigResolveClassesRequest struct {
XMLName xml.Name `xml:"configResolveClasses"`
Cookie string `xml:"cookie,attr"`
InHierarchical string `xml:"inHierarchical,attr,omitempty"`
InIds []Id `xml:"inIds>Id"`
}
// ConfigResolveClassesResponse is the response type associated with a ConfigResolveClassesRequest.
type ConfigResolveClassesResponse struct {
BaseResponse
XMLName xml.Name `xml:"configResolveClasses"`
OutConfigs InnerXml `xml:"outConfigs"`
}
// ConfigResolveChildren type is used for constructing requests that retrieve
// children of managed objects under a specified DN. A filter can be used to
// reduce the number of children being returned.
type ConfigResolveChildrenRequest struct {
XMLName xml.Name `xml:"configResolveChildren"`
Cookie string `xml:"cookie,attr"`
ClassId string `xml:"classId,attr"`
InDn string `xml:"inDn,attr"`
InHierarchical string `xml:"inHierarchical,attr"`
InFilter FilterAny `xml:"inFilter>any,omitempty"`
}
// ConfigResolveChildrenResponse is the response type associated with a ConfigResolveChildrenRequest.
type ConfigResolveChildrenResponse struct {
BaseResponse
XMLName xml.Name `xml:"configResolveChildren"`
OutConfigs InnerXml `xml:"outConfigs"`
}
// FilterAny represents any valid filter.
type FilterAny interface{}
// FilterProperty represents a Property Filter.
type FilterProperty struct {
Class string `xml:"class,attr"`
Property string `xml:"property,attr"`
Value string `xml:"value,attr"`
}
// FilterEq represents an Equality Filter.
type FilterEq struct {
XMLName xml.Name `xml:"eq"`
FilterProperty
}
// FilterNe represents a Not Equal Filter.
type FilterNe struct {
XMLName xml.Name `xml:"ne"`
FilterProperty
}
// FilterGt represents a Greater Than Filter.
type FilterGt struct {
XMLName xml.Name `xml:"gt"`
FilterProperty
}
// FilterGe represents a Greater Than Or Equal To Filter.
type FilterGe struct {
XMLName xml.Name `xml:"ge"`
FilterProperty
}
// FilterLt represents a Less Than Filter.
type FilterLt struct {
XMLName xml.Name `xml:"lt"`
FilterProperty
}
// FilterLe represents a Less Than Or Equal To Filter.
type FilterLe struct {
XMLName xml.Name `xml:"le"`
FilterProperty
}
// FilterWildcard represents a Wildcard Filter.
// The wildcard filter uses standard regular expression syntax.
type FilterWildcard struct {
XMLName xml.Name `xml:"wcard"`
FilterProperty
}
// FilterAnyBits represents an Any Bits Filter.
type FilterAnyBits struct {
XMLName xml.Name `xml:"anybit"`
FilterProperty
}
// FilterAllBits represents an All Bits Filter.
type FilterAllBits struct {
XMLName xml.Name `xml:"allbits"`
FilterProperty
}
// FilterAnd represents a composite AND Filter.
type FilterAnd struct {
XMLName xml.Name `xml:"and"`
Filters []FilterAny
}
// FilterOr represents a composite OR Filter.
type FilterOr struct {
XMLName xml.Name `xml:"or"`
Filters []FilterAny
}
// FilterNot represents a NOT Modifier Filter.
type FilterNot struct {
XMLName xml.Name `xml:"not"`
Filters []FilterAny
}
// FilterBetween represents a Between Filter.
type FilterBetween struct {
XMLName xml.Name `xml:"bw"`
Class string `xml:"class,attr"`
Property string `xml:"property,attr"`
FirstVault string `xml:"firstValue,attr"`
SecondValue string `xml:"secondValue,attr"`
}

341
local/go-ucs/api/client.go Executable file
View File

@@ -0,0 +1,341 @@
package api
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/dnaeon/go-ucs/mo"
"golang.org/x/time/rate"
)
// RateLimit limits the number of requests per second that a Client
// can send to a remote Cisco UCS API endpoint using a rate.Limiter
// token bucket configured with the provided requests per seconds and
// burst. A request will wait for up to the given wait time.
type RateLimit struct {
// RequestsPerSecond defines the allowed number of requests per second.
RequestsPerSecond float64
// Burst is the maximum burst size.
Burst int
// Wait defines the maximum time a request will wait for a token to be consumed.
Wait time.Duration
}
// Config type contains the setting used by the Client.
type Config struct {
// HttpClient is the HTTP client to use for sending requests.
// If nil then we use http.DefaultClient for all requests.
HttpClient *http.Client
// Endpoint is the base URL to the remote Cisco UCS Manager endpoint.
Endpoint string
// Username to use when authenticating to the remote endpoint.
Username string
// Password to use when authenticating to the remote endpoint.
Password string
// RateLimit is used for limiting the number of requests per second
// against the remote Cisco UCS API endpoint using a token bucket.
RateLimit *RateLimit
}
// Client is used for interfacing with the remote Cisco UCS API endpoint.
type Client struct {
config *Config
apiUrl *url.URL
limiter *rate.Limiter
// Cookie is the authentication cookie currently in use.
// It's value is set by the AaaLogin and AaaRefresh methods.
Cookie string
}
// NewClient creates a new API client from the given config.
func NewClient(config Config) (*Client, error) {
if config.HttpClient == nil {
config.HttpClient = http.DefaultClient
}
baseUrl, err := url.Parse(config.Endpoint)
if err != nil {
return nil, err
}
apiUrl, err := url.Parse(apiEndpoint)
if err != nil {
return nil, err
}
var limiter *rate.Limiter
if config.RateLimit != nil {
rps := rate.Limit(config.RateLimit.RequestsPerSecond)
limiter = rate.NewLimiter(rps, config.RateLimit.Burst)
}
client := &Client{
config: &config,
apiUrl: baseUrl.ResolveReference(apiUrl),
limiter: limiter,
}
return client, nil
}
// Hostname returns the host portion of the remote UCS API endpoint without any port number.
func (c *Client) Hostname() string {
return c.apiUrl.Host
}
// AaaLogin performs the initial authentication to the remote Cisco UCS API endpoint.
func (c *Client) AaaLogin(ctx context.Context) (*AaaLoginResponse, error) {
req := AaaLoginRequest{
InName: c.config.Username,
InPassword: c.config.Password,
}
var resp AaaLoginResponse
if err := c.Request(ctx, req, &resp); err != nil {
return nil, err
}
if resp.IsError() {
return nil, resp.ToError()
}
// Set authentication cookie for future re-use when needed.
c.Cookie = resp.OutCookie
return &resp, nil
}
// AaaRefresh refreshes the current session by requesting a new authentication cookie.
func (c *Client) AaaRefresh(ctx context.Context) (*AaaRefreshResponse, error) {
req := AaaRefreshRequest{
InName: c.config.Username,
InPassword: c.config.Password,
InCookie: c.Cookie,
}
var resp AaaRefreshResponse
if err := c.Request(ctx, req, &resp); err != nil {
return nil, err
}
if resp.IsError() {
return nil, resp.ToError()
}
// Set new authentication cookie
c.Cookie = resp.OutCookie
return &resp, nil
}
// AaaKeepAlive sends a request to keep the current session active using the same cookie.
func (c *Client) AaaKeepAlive(ctx context.Context) (*AaaKeepAliveResponse, error) {
req := AaaKeepAliveRequest{
Cookie: c.Cookie,
}
var resp AaaKeepAliveResponse
if err := c.Request(ctx, req, &resp); err != nil {
return nil, err
}
if resp.IsError() {
return nil, resp.ToError()
}
return &resp, nil
}
// AaaLogout invalidates the current client session.
func (c *Client) AaaLogout(ctx context.Context) (*AaaLogoutResponse, error) {
req := AaaLogoutRequest{
InCookie: c.Cookie,
}
var resp AaaLogoutResponse
if err := c.Request(ctx, req, &resp); err != nil {
return nil, err
}
if resp.IsError() {
return nil, resp.ToError()
}
c.Cookie = ""
return &resp, nil
}
// doRequest sends a request to the remote Cisco UCS API endpoint.
func (c *Client) doRequest(ctx context.Context, in, out interface{}) error {
data, err := xmlMarshalWithSelfClosingTags(in)
if err != nil {
return err
}
fmt.Println("doRequest sending following XML request:")
fmt.Println(string(data[:]))
r, err := http.NewRequest("POST", c.apiUrl.String(), bytes.NewBuffer(data))
if err != nil {
return err
}
req := r.WithContext(ctx)
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.config.HttpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Println("doRequest received following response:")
fmt.Println(string(body[:]))
return xml.Unmarshal(body, &out)
}
// Request sends a POST request to the remote Cisco UCS API endpoint.
func (c *Client) Request(ctx context.Context, in, out interface{}) error {
// Rate limit requests if we are using a limiter
if c.limiter != nil {
ctxWithTimeout, cancel := context.WithTimeout(ctx, c.config.RateLimit.Wait)
defer cancel()
if err := c.limiter.Wait(ctxWithTimeout); err != nil {
return err
}
}
return c.doRequest(ctx, in, out)
}
// RequestNow sends a POST request to the remote Cisco UCS API endpoint immediately.
// This bypasses any rate limiter configuration that may be used and is
// meant to be used for priority requests, e.g. refreshing a token, logging out, etc.
func (c *Client) RequestNow(ctx context.Context, in, out interface{}) error {
return c.doRequest(ctx, in, out)
}
// ConfigResolveDn retrieves a single managed object for a specified DN.
func (c *Client) ConfigResolveDn(ctx context.Context, in ConfigResolveDnRequest, out mo.Any) error {
var resp ConfigResolveDnResponse
if err := c.Request(ctx, in, &resp); err != nil {
return err
}
if resp.IsError() {
return resp.ToError()
}
// The requested managed object is contained within the inner XML document,
// which we need to unmarshal first into the given concrete type.
return xml.Unmarshal(resp.OutConfig.Inner, &out)
}
// ConfigResolveDns retrieves managed objects for a specified list of DNs.
func (c *Client) ConfigResolveDns(ctx context.Context, in ConfigResolveDnsRequest, out mo.Any) (*ConfigResolveDnsResponse, error) {
var resp ConfigResolveDnsResponse
if err := c.Request(ctx, in, &resp); err != nil {
return nil, err
}
if resp.IsError() {
return nil, resp.ToError()
}
inner, err := xml.Marshal(resp.OutConfigs)
if err != nil {
return nil, err
}
if err := xml.Unmarshal(inner, &out); err != nil {
return nil, err
}
return &resp, nil
}
// ConfigResolveClass retrieves managed objects of the specified class.
func (c *Client) ConfigResolveClass(ctx context.Context, in ConfigResolveClassRequest, out mo.Any) error {
var resp ConfigResolveClassResponse
if err := c.Request(ctx, in, &resp); err != nil {
return err
}
if resp.IsError() {
return resp.ToError()
}
inner, err := xml.Marshal(resp.OutConfigs)
if err != nil {
return err
}
// The requested managed objects are contained within the inner XML document,
// which we need to unmarshal first into the given concrete type.
return xml.Unmarshal(inner, &out)
}
// ConfigResolveClasses retrieves managed objects from the specified list of classes.
func (c *Client) ConfigResolveClasses(ctx context.Context, in ConfigResolveClassesRequest, out mo.Any) error {
var resp ConfigResolveClassesResponse
if err := c.Request(ctx, in, &resp); err != nil {
return err
}
if resp.IsError() {
return resp.ToError()
}
inner, err := xml.Marshal(resp.OutConfigs)
if err != nil {
return err
}
// The requested managed objects are contained within the inner XML document,
// which we need to unmarshal first into the given concrete type.
return xml.Unmarshal(inner, &out)
}
// ConfigResolveChildren retrieves children of managed objects under a specified DN.
func (c *Client) ConfigResolveChildren(ctx context.Context, in ConfigResolveChildrenRequest, out mo.Any) error {
var resp ConfigResolveChildrenResponse
if err := c.Request(ctx, in, &resp); err != nil {
return err
}
if resp.IsError() {
return resp.ToError()
}
inner, err := xml.Marshal(resp.OutConfigs)
if err != nil {
return err
}
// The requested managed objects are contained within the inner XML document,
// which we need to unmarshal first into the given concrete type.
return xml.Unmarshal(inner, &out)
}

2
local/go-ucs/api/doc.go Executable file
View File

@@ -0,0 +1,2 @@
// Package api provides types and methods for interfacing with a Cisco UCS API endpoint.
package api

View File

@@ -0,0 +1,73 @@
package api_test
import (
"context"
"crypto/tls"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/dnaeon/go-ucs/api"
)
func Example_aaaKeepAlive() {
// The following example shows how to keep a session alive.
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
// Authenticate to the remote API endpoint and obtain authentication cookie
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to authenticate: %s", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
// Channel on which the shutdown signal is sent
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Send a KeepAlive request every minute
ticker := time.NewTicker(1 * time.Minute)
L:
for {
select {
case <-quit:
log.Println("Keyboard interrupt detected, terminating.")
break L
case <-ticker.C:
log.Println("Sending KeepAlive request ...")
resp, err := client.AaaKeepAlive(ctx)
if err != nil {
log.Printf("Unable to keep session alive: %s\n", err)
break L
}
log.Printf("Got response with cookie: %s\n", resp.Cookie)
}
}
}

View File

@@ -0,0 +1,44 @@
package api_test
import (
"context"
"crypto/tls"
"log"
"net/http"
"github.com/dnaeon/go-ucs/api"
)
func Example_aaaLogin() {
// The following example shows how to login and logout from a Cisco UCS API endpoint
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s\n", err)
}
// Authenticate to the remote API endpoint and obtain authentication cookie
log.Printf("Logging in to %s\n", config.Endpoint)
ctx := context.Background()
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to authenticate: %s", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
}

View File

@@ -0,0 +1,51 @@
package api_test
import (
"context"
"crypto/tls"
"log"
"net/http"
"github.com/dnaeon/go-ucs/api"
)
func Example_aaaRefresh() {
// The following example shows how to refresh an existing session.
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
// Authenticate to the remote API endpoint and obtain authentication cookie
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to authenticate: %s", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
log.Println("Refreshing session")
if _, err := client.AaaRefresh(ctx); err != nil {
log.Fatalf("Unable to refresh session: %s\n", err)
}
log.Printf("New authentication cookie is: %s\n", client.Cookie)
}

View File

@@ -0,0 +1,96 @@
package api_test
import (
"context"
"crypto/tls"
"encoding/xml"
"log"
"net/http"
"github.com/dnaeon/go-ucs/api"
"github.com/dnaeon/go-ucs/mo"
)
func Example_compositeFilter() {
// The following example shows how to use a composite AND property filter.
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to login: %s\n", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
// The type into which we unmarshal the result data
type blades struct {
XMLName xml.Name
Blades []mo.ComputeBlade `xml:"computeBlade"`
}
// Create a composite AND property filter, which will find all blades,
// which have total memory greater than or equal to 2048 MB and are in chassis 3.
filter := api.FilterAnd{
Filters: []api.FilterAny{
api.FilterGe{
FilterProperty: api.FilterProperty{
Class: "computeBlade",
Property: "totalMemory",
Value: "2048",
},
},
api.FilterEq{
FilterProperty: api.FilterProperty{
Class: "computeBlade",
Property: "chassisId",
Value: "3",
},
},
},
}
req := api.ConfigResolveClassRequest{
Cookie: client.Cookie,
InHierarchical: "false",
ClassId: "computeBlade",
InFilter: filter,
}
var out blades
log.Println("Retrieving managed objects with class `computeBlade`")
if err := client.ConfigResolveClass(ctx, req, &out); err != nil {
log.Fatalf("Unable to retrieve `computeBlade` managed object: %s", err)
}
log.Printf("Retrieved %d compute blades\n", len(out.Blades))
for _, blade := range out.Blades {
log.Printf("%s:\n", blade.Dn)
log.Printf("\tNumber of CPUs: %d\n", blade.NumOfCpus)
log.Printf("\tTotal Memory: %d\n", blade.TotalMemory)
log.Printf("\tModel: %s\n", blade.Model)
log.Printf("\tChassis ID: %d\n", blade.ChassisId)
log.Printf("\tVendor: %s\n", blade.Vendor)
}
}

View File

@@ -0,0 +1,73 @@
package api_test
import (
"context"
"crypto/tls"
"encoding/xml"
"log"
"net/http"
"github.com/dnaeon/go-ucs/api"
"github.com/dnaeon/go-ucs/mo"
)
func Example_configResolveClass() {
// The following example shows how to retrieve all compute blades.
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to login: %s\n", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
// The type into which we unmarshal the result data
type blades struct {
XMLName xml.Name
Blades []mo.ComputeBlade `xml:"computeBlade"`
}
req := api.ConfigResolveClassRequest{
Cookie: client.Cookie,
ClassId: "computeBlade",
InHierarchical: "false",
}
var out blades
log.Println("Retrieving managed objects with class `computeBlade`")
if err := client.ConfigResolveClass(ctx, req, &out); err != nil {
log.Fatalf("Unable to retrieve `computeBlade` managed object: %s", err)
}
log.Printf("Retrieved %d compute blades\n", len(out.Blades))
for _, blade := range out.Blades {
log.Printf("%s:\n", blade.Dn)
log.Printf("\tNumber of CPUs: %d\n", blade.NumOfCpus)
log.Printf("\tTotal Memory: %d\n", blade.TotalMemory)
log.Printf("\tModel: %s\n", blade.Model)
log.Printf("\tVendor: %s\n", blade.Vendor)
}
}

View File

@@ -0,0 +1,81 @@
package api_test
import (
"context"
"crypto/tls"
"log"
"net/http"
"github.com/dnaeon/go-ucs/api"
"github.com/dnaeon/go-ucs/mo"
)
func Example_configResolveClasses() {
// The following example shows how to retrieve managed objects from different classes.
// In the example below we will retrieve the managed objects of `computeBlade` and `computeRackUnit` classes.
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to login: %s\n", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
req := api.ConfigResolveClassesRequest{
Cookie: client.Cookie,
InHierarchical: "false",
InIds: []api.Id{
api.NewId("computeBlade"),
api.NewId("computeRackUnit"),
},
}
// ComputeItem is a container for all physical compute items.
var out mo.ComputeItem
log.Println("Retrieving managed objects with classes `computeBlade` and `computeRackUnit`")
if err := client.ConfigResolveClasses(ctx, req, &out); err != nil {
log.Fatalf("Unable to retrieve `computeBlade` and `computeRackUnit` managed object: %s", err)
}
log.Printf("Retrieved %d compute blades\n", len(out.Blades))
log.Printf("Retrieved %d compute rack units\n", len(out.RackUnits))
for _, blade := range out.Blades {
log.Printf("%s:\n", blade.Dn)
log.Printf("\tNumber of CPUs: %d\n", blade.NumOfCpus)
log.Printf("\tTotal Memory: %d\n", blade.TotalMemory)
log.Printf("\tModel: %s\n", blade.Model)
log.Printf("\tVendor: %s\n", blade.Vendor)
}
for _, blade := range out.RackUnits {
log.Printf("%s:\n", blade.Dn)
log.Printf("\tNumber of CPUs: %d\n", blade.NumOfCpus)
log.Printf("\tTotal Memory: %d\n", blade.TotalMemory)
log.Printf("\tModel: %s\n", blade.Model)
log.Printf("\tVendor: %s\n", blade.Vendor)
}
}

View File

@@ -0,0 +1,63 @@
package api_test
import (
"context"
"crypto/tls"
"log"
"net/http"
"github.com/dnaeon/go-ucs/api"
"github.com/dnaeon/go-ucs/mo"
)
func Example_configResolveDn() {
// The following example shows how to retrieve a single managed object for a specified DN.
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to login: %s\n", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
// Retrieve the `sys` DN, which is of type mo.TopSystem
log.Println("Retrieving `sys` managed object")
req := api.ConfigResolveDnRequest{
Cookie: client.Cookie,
Dn: "sys",
InHierarchical: "false",
}
var sys mo.TopSystem
if err := client.ConfigResolveDn(ctx, req, &sys); err != nil {
log.Fatalf("Unable to retrieve DN: %s", err)
}
log.Printf("Address: %s\n", sys.Address)
log.Printf("Current time: %s\n", sys.CurrentTime)
log.Printf("Dn: %s\n", sys.Dn)
log.Printf("Mode: %s\n", sys.Mode)
log.Printf("Uptime: %s\n", sys.SystemUptime)
}

View File

@@ -0,0 +1,80 @@
package api_test
import (
"context"
"crypto/tls"
"encoding/xml"
"log"
"net/http"
"strings"
"github.com/dnaeon/go-ucs/api"
"github.com/dnaeon/go-ucs/mo"
)
func Example_configResolveDns() {
// The following example shows how to retrieve a list of managed objects by specifying their DNs.
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to login: %s\n", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
// A type to contain the instances of the retrieved DNs.
type outConfigs struct {
XMLName xml.Name
Sys mo.TopSystem `xml:"topSystem"`
Version mo.VersionApplication `xml:"versionApplication"`
}
var out outConfigs
// Retrieve the list of DNs
log.Println("Retrieving managed objects using configResolveDns query method")
req := api.ConfigResolveDnsRequest{
Cookie: client.Cookie,
InHierarchical: "false",
InDns: []api.Dn{
api.NewDn("sys"),
api.NewDn("sys/version/application"),
api.NewDn("no/such/dn"),
},
}
resp, err := client.ConfigResolveDns(ctx, req, &out)
if err != nil {
log.Fatalf("Unable to retrieve DNs: %s", err)
}
log.Printf("%s is at version %s\n", out.Sys.Name, out.Version.Version)
unresolved := make([]string, 0)
for _, dn := range resp.OutUnresolved {
unresolved = append(unresolved, dn.Value)
}
log.Printf("Unresolved DNs: %s", strings.Join(unresolved, ", "))
}

View File

@@ -0,0 +1,88 @@
package api_test
import (
"context"
"crypto/tls"
"log"
"net/http"
"sync"
"time"
"github.com/dnaeon/go-ucs/api"
"github.com/dnaeon/go-ucs/mo"
)
func Example_rateLimiter() {
// The following example shows how to rate limit requests again the remote
// Cisco UCS API endpoint using a token bucket rate limiter.
// https://en.wikipedia.org/wiki/Token_bucket
// Skip SSL certificate verification of remote endpoint.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := &http.Client{Transport: tr}
// Create a new Cisco UCS API client.
// Set maximum allowed requests per second to 1 with a burst size of 1.
// A request will wait up to 1 minute for a token.
config := api.Config{
Endpoint: "https://ucs01.example.org/",
Username: "admin",
Password: "password",
HttpClient: httpClient,
RateLimit: &api.RateLimit{
RequestsPerSecond: 1.0,
Burst: 1,
Wait: time.Duration(1 * time.Minute),
},
}
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Unable to create API client: %s", err)
}
ctx := context.Background()
log.Printf("Logging in to %s\n", config.Endpoint)
if _, err := client.AaaLogin(ctx); err != nil {
log.Fatalf("Unable to login: %s\n", err)
}
defer client.AaaLogout(ctx)
log.Printf("Got authentication cookie: %s\n", client.Cookie)
// Start a few concurrent requests to the remote API endpoint.
// Requests will be executed one at a time, because of how the limiter is configured.
var wg sync.WaitGroup
for i := 1; i < 10; i++ {
wg.Add(1)
// Our worker function will retrieve the `sys` DN from the remote Cisco UCS API.
// We will start a few of these in separate goroutines.
worker := func(id int) {
defer wg.Done()
// Retrieve the `sys` DN, which is of type mo.TopSystem
log.Printf("Worker #%d: Retrieving `sys` managed object\n", id)
req := api.ConfigResolveDnRequest{
Cookie: client.Cookie,
Dn: "sys",
InHierarchical: "false",
}
var sys mo.TopSystem
if err := client.ConfigResolveDn(ctx, req, &sys); err != nil {
log.Printf("Worker #%d: Unable to retrieve DN: %s\n", id, err)
return
}
log.Printf("Worker #%d: successfully retrieved `sys` managed object\n", id)
}
go worker(i)
}
wg.Wait()
}

54
local/go-ucs/api/xml.go Executable file
View File

@@ -0,0 +1,54 @@
package api
import (
"encoding/xml"
"regexp"
)
// xmlMarshalWithSelfClosingTags post-processes results from xml.Marshal into XML
// document where empty XML elements use self-closing tags.
//
// The XML standard states that self-closing tags are permitted.
//
// https://www.w3.org/TR/REC-xml/#dt-empty
// https://www.w3.org/TR/REC-xml/#d0e2480
//
// According to the XML standard an empty element with a start and end tag is
// semantically the same as an empty element with a self-closing tag.
//
// Unfortunately not all XML parsers can understand both. Such is the
// case with Cisco UCS Manager API, which expects empty elements to use
// self-closing tags only.
//
// As of now XML marshaling in Go always uses start and end tags,
// which results in XML elements like the one below.
//
// <Person name="me"></Person>
//
// Above XML elements cannot be parsed by the remote Cisco UCS API endpoint,
// and such API calls result in parse error returned to the client.
//
// Currently the Go team is considering implementing XML self-closing tags,
// which progress can be tracked in the issue below.
//
// https://github.com/golang/go/issues/21399
//
// Until support for XML self-closing tags (if ever) becomes real in Go
// we need to ensure compatibility with the Cisco UCS API by doing that ourselves.
//
// In a separate thread @rsc also suggested a similar approach by using
// strings.Replace(), even though such thing is not ideal and should
// hopefully one day make into the language as a feature.
//
// https://groups.google.com/forum/#!topic/golang-nuts/guG6iOCRu08
func xmlMarshalWithSelfClosingTags(in interface{}) ([]byte, error) {
data, err := xml.Marshal(in)
if err != nil {
return nil, err
}
re := regexp.MustCompile(`<([^/][\w\s\"\=\-\/]*)>\s*<(\/\w*)>`)
newData := re.ReplaceAllString(string(data), "<$1/>")
return []byte(newData), nil
}

64
local/go-ucs/api/xml_test.go Executable file
View File

@@ -0,0 +1,64 @@
package api
import (
"encoding/xml"
"testing"
)
type Person struct {
XMLName xml.Name `xml:"person"`
Name string `xml:"name,attr,omitempty"`
Place *Location `xml:"place,omitempty"`
}
type Location struct {
Country string `xml:"country,omitempty"`
City string `xml:"city,omitempty"`
}
type PersonEmbedded struct {
XMLName xml.Name `xml:"personEmbedded"`
Person
Location
}
type NilStruct *Person
func TestXmlMarshalWithSelfClosingTags(t *testing.T) {
var tests = []struct {
value interface{}
expect string
}{
// Nil values
{value: nil, expect: ``},
{value: new(NilStruct), expect: ``},
// Values
{value: Person{}, expect: `<person/>`},
{value: Person{Name: "John Doe"}, expect: `<person name="John Doe"/>`},
{
value: Person{Name: "Jane Doe", Place: &Location{Country: "unknown", City: "unknown"}},
expect: `<person name="Jane Doe"><place><country>unknown</country><city>unknown</city></place></person>`,
},
{
value: &PersonEmbedded{Person: Person{Name: "John Doe"}, Location: Location{Country: "unknown"}},
expect: `<personEmbedded name="John Doe"><country>unknown</country></personEmbedded>`,
},
// Pointers to values
{value: &Person{}, expect: `<person/>`},
{value: &Person{Name: "John Doe"}, expect: `<person name="John Doe"/>`},
}
for _, test := range tests {
data, err := xmlMarshalWithSelfClosingTags(test.value)
if err != nil {
t.Fatalf("Cannot marshal %+v: %s", test.value, err)
}
got := string(data)
if got != test.expect {
t.Fatalf("Got XML '%s', expect '%s'", got, test.expect)
}
}
}