Contents


Take command of the SoftLayer API from the Go programming language

Use the SoftLayer-Go library to start making the most of the SoftLayer cloud platform

Comments

Want to learn the ins and outs of using the SoftLayer Cloud API from the Go programming language? This tutorial shows you how to use the services, navigate and use the data structures, and establish object masks and filters, as well as how to order VMs and more in a concise and idiomatic way.

SoftLayer-Go is a SoftLayer API client library for the Go programming language. It is automatically generated based on the SoftLayer API metadata that describes the different service endpoints and data types.

softlayer-go has been fashioned in the same way the actual SoftLayer API is exported to users. That is, it has a set of structs that mirror the data types in SoftLayer. Another set of structs mirror the services in SoftLayer, and each one has all the expected methods attached. The services have a few additional methods (to be covered later) that allow you to specify the context for a method.

Here's a breakdown of all the Go packages in the library:

  • datatypes provide access to all of the structs that are returned by the API and you would need to pass to the API, depending on the service method being used.
  • services is for all the services (in the form of structs) with their corresponding methods.
  • session contains all the transport logic used to communicate with the API endpoint; it supports both REST and XML-RPC endpoints.
  • sl provides convenient functions for getting and setting values to and from the data type structs.
  • filter is used for creating an object filter that specifies which fields you want to get back from the API.
  • helpers do more elaborate things that involve multiple calls to the API or common tasks like getting the ID for a data center name.

We will talk more about these packages in the sections that follow.

Installing softlayer-go

Before you install the library, you will need to do the following:

  • Install Go
  • Obtain a GOPATH environment variable

You should also add GOPATH/bin to your PATH environment variable for convenience.

To install the library, enter the following from a command line:

go get github.com/softlayer/softlayer-go/...

Finally, you should also have some experience reading and writing Go code.

Quickstart example

Let’s take a look at a short example that uses the softlayer-go library:

package main

import (
    "fmt"
    "os"

    "github.com/softlayer/softlayer-go/session"
    "github.com/softlayer/softlayer-go/services"
)

const (
    USERNAME = "your user name"
    APIKEY = "your api key"
)

func main() {
    sess := session.New(USERNAME, APIKEY)
    accountService := services.GetAccountService(sess)
    account, err := accountService.GetObject()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    fmt.Println(*account.Id, *account.CompanyName)
}

To summarize the steps taken in the code above:

  1. Import the required softlayer-go packages.
  2. Create a new session by passing your SoftLayer username and API key.
  3. Acquire a handle for the SoftLayer Account service. There is a Get method for all SoftLayer services listed here. The method names are always prefixed with Get and suffixed with Service, as in services.GetServiceNameHereService(...). This ignores any underscores that appear in the name of the service, as shown in the API documentation.
  4. Invoke a method on the account service; basically, you're getting the account information. This can return an error that you check for.
  5. Print out the account ID and account name.

Create a session

Let's take a closer look at the line that creates a new session:

sess := session.New(USERNAME, APIKEY)

session.New() can take a variable list of (up to four) string arguments, in this order:

  1. Username
  2. API key
  3. API endpoint
  4. HTTP timeout (in seconds)

The API endpoint and the HTTP timeout have default values of https://api.softlayer.com/rest/v3 and 120, respectively. You can override them in any way you wish by passing the third and fourth parameters.

The session values can also be read from the environment. The environment variables read from are (respectively):

  1. SL_USERNAME
  2. SL_API_KEY
  3. SL_ENDPOINT_URL
  4. SL_TIMEOUT

If you have the username and API key set in the environment, then you can just create a new session without parameters in the code:

sess := session.New()

In addition, the session values can also be read from a local configuration file. The library will look for the file at ~/.softlayer (%USERPROFILE%/.softlayer on Windows). The format should look like this:

[softlayer]
username = <your username>
api_key = <your api key>
endpoint_url = <optional>
timeout = <optional>

Passing the arguments to New() will always override the corresponding argument in the environment or local configuration file. The environment has precedence over the local configuration file.

Now that you have a session reference, you can get a handle for any of the SoftLayer services. This always requires passing the session reference:

accountService := services.GetAccountService(sess)

Instantiate a virtual guest

Let's look at another example. This time, a virtual guest is created on SoftLayer:

package main

import (
    "fmt"
    "os"

    "github.com/softlayer/softlayer-go/datatypes"
    "github.com/softlayer/softlayer-go/services"
    "github.com/softlayer/softlayer-go/session"
    "github.com/softlayer/softlayer-go/sl"
)

func main() {
    sess := session.New()
    service := services.GetVirtualGuestService(sess)

    guestTpl := datatypes.Virtual_Guest{
        Hostname: sl.String("sample"),
        Domain: sl.String("example.com"),
        MaxMemory: sl.Int(2048),
        StartCpus: sl.Int(1),
        Datacenter: &datatypes.Location{Name: sl.String("sjc01")},
        OperatingSystemReferenceCode: sl.String("UBUNTU_LATEST"),
        LocalDiskFlag: sl.Bool(true),
    }

    guest, err := service.Mask("id;domain").CreateObject(&guestTpl)
    if err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }

    fmt.Printf("New Virtual Guest created with ID %d\n", *guest.Id)
    fmt.Printf("Domain: %s\n", *guest.Domain)
}

To create a virtual guest instance, you need to use a virtual guest struct that allows you to specify certain required properties. This becomes your guest template. Once you have initialized this template as shown above, you can invoke the CreateObject() method of the virtual guest service to create the instance on the cloud by passing it the template.

Note the helper methods from the sl package that you use to set the values in the struct. These are used to provide pointers to the literal values. Also, note the use of the chained method Mask("id;domain"), which is used to set object masks on the underlying API call. I'll talk more about helpers and masks in the following sections.

Service methods

As previously mentioned, the services package has one struct for each service in SoftLayer. Each one of these has all the expected methods for that service.

Let's take a look at one of the method signatures in the Virtual_Guest service:

func (r Virtual_Guest) GetCpuMetricDataByDate(
    startDateTime *datatypes.Time,
    endDateTime *datatypes.Time,
    cpuIndexes []int) (resp []datatypes.Metric_Tracking_Object_Data, err error)

In softlayer-go, every method parameter is expected to be a pointer, except for slice parameters. In the case of a slice parameter, you would just pass that directly, since slices are implicit pointers in Go.

Every method will also return an error. All errors will be wrapped with sl.Error (more on this in the following sections). If the method has other return values, they will also be returned along with the error, as in the example above.

This is what calling the above method would look like:

sess := session.New()
service := services.GetVirtualGuestService(sess)

start := time.Date(2010, 12, 31, 0, 0, 0, 0, time.FixedZone("Atlantic", -60*60*4))
end := time.Now()

resp, err := service.GetCpuMetricDataByDate(
    &datatypes.Time{start}, &datatypes.Time{end},
    []int{1, 2, 3},
)

When dealing with time values, the library handles them as its own datatypes.Time types. This is not just true of method arguments that have to do with time, but also of any datatype field that has to do with time. This allows the library to decode/encode time values appropriately from/to the SoftLayer API endpoint, and to treat that type of value consistently throughout.

As a result, when needing to pass time values as method arguments, users will have to wrap them within a datatypes.Time struct. Note that we pass a reference to these structs, while the array of integers is passed as is.

Service context modifiers

When using the SoftLayer API, you can refer to specific instances of a resource or object. In truth, some methods require this. For example, when requesting information for a particular user, you have to specify the ID of the user. However, this ID is not handled as a method argument by the API, so the library handles it as a contextual parameter instead:

service := services.GetUserCustomerService(sess)
user, err := service.Id(6786566).GetObject()

Every service has a set of functions you can use to pass these contextual parameters, like the ID of the resource you want to get data about as shown above. Here is the complete list of these types of functions:

  • Id(id int)—The ID of the resource that the operation will act on.
  • Limit(limit int)—For methods that return a collection, sets the maximum number of results to return.
  • Offset(offset int)—For methods that return a collection, sets how many results to skip over before marking the start of the returned collection.
  • Mask(id string)—Used to get relational properties when fetching objects from the API; also enables you to specify which fields you are interested in so the API can return only that data.
  • Filter(filter string)—For methods that return a collection, used to limit the number of results returned.

The following code snippet will return a list with a single public image, since you have explicitly specified the global identifier of the image in the filter. In addition, only the fields that are set in the object mask are returned by the API.

service := services.GetVirtualGuestBlockDeviceTemplateGroupService(sess)

publicImages, err := service.
    Mask("id;name;note;summary").
    Filter(`{"globalIdentifier":{"operation":"2e61f677-752b-4020-a447-b138f5daa387"}}`).GetPublicImages()

The field names you use in object masks and filters are not capitalized. They must match the API datatype documentation. However, the corresponding data structures in Go will have those field names capitalized for visibility reasons. For example, the public image's summary, which you specified in your object mask, is accessed with *publicImages[0].Summary. See the object masks documentation to learn more.

Above, the mask and filter are applied to that request only. That is, if you use the same service reference again, the mask and filter will be blank. This is true for the other contextual parameters listed above, as well. If you would like to retain the set mask and filter for future method invocations, save the service handle that's returned by the contextual parameter function:

serviceWithMaskAndFilter := service.
    Mask("id;name;note;summary").
    Filter(`{"globalIdentifier":{"operation":"2e61f677-752b-4020-a447-b138f5daa387"}}`)

publicImages, err := serviceWithMaskAndFilter.GetPublicImages()

Object filters

Filters are specified as a string. Given how complex object filters can become, the library provides a filter builder to help make this more painless. For example, here is how you can specify the same filter as in the previous snippet using the filter builder:

service.Filter(
    filter.Path("globalIdentifier").
        Eq("2e61f677-752b-4020-a447-b138f5daa387").Build(),
).GetPublicImages()

Filters can also be created in advance, in bulk, and with other kinds of conditions:

filters := filter.New(
    filter.Path("virtualGuests.hostname").StartsWith("demo"),
    filter.Path("virtualGuests.id").NotEq(1234),
)

Later, you can append another filter to that group before using it:

filters = append(filters, filter.Path("virtualGuests.domain").EndsWith("example.com"))

guests, err := accountService.Filter(filters.Build()).GetVirtualGuests()

filter.Path() can be used to specify a condition on a nested relational property of an object in SoftLayer. For example:

filter.Path("datacenter.locationStatus.status").Eq("ACTIVE")

Convenience functions in sl packages

As with method arguments, every field of every datatype struct expects a pointer (except those fields that are slices). This allows the library to omit unused fields in a datatype struct when posting them to the API if the values are nil, since only pointers (and the interface{} type) can be nil in Go.

The library has a helper package to help deal with this when getting and setting values from SoftLayer structs. Take another look at one of the previous examples:

guestTpl := datatypes.Virtual_Guest{
    Hostname: sl.String("sample"),
    Domain: sl.String("example.com"),
    MaxMemory: sl.Int(2048),
    StartCpus: sl.Int(1),
    Datacenter: &datatypes.Location{Name: sl.String("sjc01")},
    OperatingSystemReferenceCode: sl.String("UBUNTU_LATEST"),
    LocalDiskFlag: sl.Bool(true),
}

The sl. package has pointer helpers for native types like strings, integers, unsigned integers, and booleans. They merely return a pointer reference to the value you pass. Without the helpers, you would have to rewrite the above code like this:

hostname := "sample"
domain := "example.com"
maxMemory := 2048
startCpus := 1
datacenterName := "sjc01"
osName := "UBUNTU_LATEST"
localDiskFlag := true

guestTpl := datatypes.Virtual_Guest{
    Hostname: &hostname,
    Domain: &domain,
    MaxMemory: &maxMemory,
    StartCpus: &startCpus,
    Datacenter: &datatypes.Location{Name: &datacenterName},
    OperatingSystemReferenceCode: &osName,
    LocalDiskFlag: &localDiskFlag,
}

This can be convenient in some cases, especially when you want to use the values as constants throughout. In other cases, it is unnecessarily verbose. Use helpers to make your code more concise whenever possible.

These helpers are useful when you need to set values in a struct, but there are also helpers that get values out of a SoftLayer struct. For example, consider this code snippet where you need to get the post install script URI of a virtual guest:

// Don't assume the post install script uri is non-nil.
var scriptURI string
if guest.PostInstallScriptUri != nil {
    scriptURI = *guest.PostInstallScriptUri
}

With the sl.Get helper, you can abbreviate this with the following (with some help from Go type assertion):

scriptURI := sl.Get(guest.PostInstallScriptUri).(string)

The sl package getter helpers are:

  • Get(p interface{}, d ...interface{}) interface{} returns the value of p. If p is a pointer, it will dereference it for you and return that value. You can also pass in an optional default value as a second argument. If p is nil, then d is returned. Otherwise if p is nil and there is no default value, then you will get the zero-value of p (meaning, if p is a string or a pointer to a string, you will get the blank string. If it is an integer or a pointer to an integer, you will get a zero, etc.).
  • GetOk(p interface{}) (interface{}, bool) is almost the same as Get(), but it will return an additional boolean value indicating whether p is a non-nil pointer. A default value option is not necessary here since the point is to have your own logic in case there is no value—which can include determining the appropriate default value.

There may be times when you need to get a deeply nested value from a SoftLayer structure. Since fields can be pointers, the safest way to avoid unexpected Go panics is to test for nil at each structure level:

var vlanId int
if guest.PrimaryNetworkComponent != nil {
    if guest.PrimaryNetworkComponent.NetworkVlan != nil {
        if guest.PrimaryNetworkComponent.NetworkVlan.Id != nil {
            vlanId = *guest.PrimaryNetworkComponent.NetworkVlan.Id
        }
    }
}

Another helper is available that can help obtain values from nested places such as that one and will eliminate a lot of that boilerplate code:

vlanId := sl.Grab(guest, "PrimaryNetworkComponent.NetworkVlan.Id").(int)

Here are the sl package grab helpers:

  • Grab(s interface{}, path string, d ...interface{}) interface{} returns the value specified by the given path using s as the starting point. path is a dot-delimited set of fields that traverse from s to get the value being grabbed. If at any point in the path traversal you get to a nil pointer value, a type-appropriate zero-value is returned.
  • GrabOk(s interface{}, path string) (interface{}, bool) is similar to Grab(), except it returns another boolean value that indicates whether it successfully found a value or not (in which case it has to create a zero-value for you).

GrabOk can be useful when no value is available and you want to handle the situation in a special way:

if vlanId, ok := sl.GrabOk(guest, "PrimaryNetworkComponent.NetworkVlan.Id").(int); !ok {
    log.Println("No VLAN ID found!")
}

Handling errors

All service methods can return an error. The error shown below, in addition to implementing Go's Error interface, is a SoftLayer type that you can inspect to get more information about it.

type Error struct {
    StatusCode int
    Exception  string
    Message    string
    Wrapped    error
}

This is helpful in case you want, for example, a precise way to determine whether an error is due to a resource not existing, or some other transport or API error.

_, err := service.Id(0).      // invalid object ID
    GetObject()

if err != nil {
    // Note: type assertion is only necessary for inspecting individual fields
    apiErr, ok := err.(sl.Error)
    if apiError.StatusCode == 404 {
        // handle not found error here
    }
    // more generic error handling here
}

The original error can always be found in .Wrapped if you need to do further unwrapping.

if err != nil {
    // Note: type assertion is only necessary for inspecting individual fields
    apiErr, ok := err.(sl.Error)
    if apiError.StatusCode == 404 {
        // handle not found error here
    } else if netError, ok := apiError.Wrapped.(net.Error); ok && netError.Timeout() {
        // handle transport timeout error here
    }
    // more generic error handling here
}

XML-RPC and password-based authentication

The SoftLayer API supports both REST and XML-RPC endpoints. Functionality between the two is nearly identical, except for one area I know of that only works over XML-RPC—specifically, when you need to authenticate to the API using your password instead of an API key.

In order to do this, you need to specify the XML-RPC endpoint in the session and do password authentication as shown here:

func main() {
    // Create a session specifying an XML-RPC endpoint url.
    sess := &session.Session{
        Endpoint: "https://api.softlayer.com/xmlrpc/v3",
    }

    // Get a token from the api using your username and password
    userService := services.GetUserCustomerService(sess)
    token, err := userService.GetPortalLoginToken(USERNAME, PASSWORD, nil, nil)
    if err != nil {
        log.Fatal(err)
    }

    // Add user id and token to the session.
    sess.UserId = *token.UserId
    sess.AuthToken = *token.Hash
    // You have a complete authenticated session now.
    // Call any api from this point on as normal...

    keys, err := userService.Id(sess.UserId).GetApiAuthenticationKeys()
    if err != nil {
        log.Fatal(err)
    }

    log.Println("API Key:", *keys[0].AuthenticationKey)
}

Debugging

To see logs of API requests and responses, simply turn on the session debug flag like this:

sess := session.New()
sess.Debug = true
// use the session reference throughout
accountService := services.GetAccountService(sess)
// ...

This can be very useful when experimenting with the library and the SoftLayer API.

Conclusion

Now you have learned how to start taking control of the SoftLayer API from the Go programming language. You have explored session initialization, obtaining handles to services, method invocation, data type structures and how to better initialize and navigate them, how to instantiate virtual servers, set object masks, use the filter builder, handle errors, and a few other things. Well done!

Of course, this only scratches the surface of what you can do with SoftLayer, a powerful and feature-rich cloud platform. I encourage you to follow the links in the Related topics section below to find more advanced examples.

Acknowledgements: Many thanks to Michael Rieth for coming up with the first version of the library and instructions.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Cloud computing
ArticleID=1047196
ArticleTitle=Take command of the SoftLayer API from the Go programming language
publish-date=07062017