This particular requirement is how I started my Go journey. I’d taken an initial look with other languages – it looked doable but not that straight forward. Plus, I ideally wanted a command line tool that I could give to other people. Python and Node just weren’t going to cut it. I was keen to take a look at Go and thought I would see if it could meet my needs. As it turned out, it was super simple and definitely the right choice.
Firstly, I guess I should explain why I would want to do such a thing. Well, not too long ago, Webex didn’t have any mechanism for managing Webex recordings. However, more recently, they added “Recording Management” to the menu which is definitely a useful addition.
On top of that, the ability to bulk manage and archive recordings on a scheduled basis programmatically was also a requirement, in order to manage our “Recording Storage Allocation”.
To build the tool, I used the following elements:
- Cobra : a library for creating powerful modern CLI applications
- Webex Meetings API : API documentation for interacting with Webex.
The help file for the resulting command currently looks like this:
$ wt
WebEx Tool (wt) is a tool for IT Administrators to manage their WebEx Tenant.
Usage:
wt [command]
Available Commands:
delete Delete WebEx Recordings
download Download WebEx Recordings
help Help about any command
list List WebEx Recordings
user User based commands
version Print the version number of wt
Flags:
-h, --help help for wt
Use "wt [command] --help" for more information about a command.
For the purposes of this post, I’ll focus specifically on the interaction with Webex. There are plenty of resources on using Cobra, but I’m happy to provide more information in another post if it’s something people would like.
In addition, on the Webex side, to keep the post relatively short, I’ll just concentrate on obtaining the recording itself with the “download” command. I won’t go into detail on the other API calls that are used, such as:
- LstRecording : Allows users to see a list of all recordings within a certain date range. Site Admin users can return the recording information for all users on the site.
- GetRecordingInfo : allows users to retrieve information about a recording.
- DelRecording : Allows the user to delete an NBR (Network Based Recording) file.
- GetUser : Retrieves detailed information about the specified user.
- SetUser : Allows site administrators to update the information of an existing user, for example to unlock their account.
The API for downloading recordings is slightly separate from the other APIs mentioned above in that it falls under the NBR Web Services API. Specifically, it is the getNBRStorageFile
API call outlined under the API Functions.
The Webex API for this is currently SOAP based and as such we have to pass it XML. I use the encoding/xml library to decode various responses, and strictly speaking we should maybe use that to generate the body, but since it is pretty basic, I decided to just use a formatted string:
xmlBody := fmt.Sprintf("
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<soapenv:Envelope
xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">
<soapenv:Body>
<ns1:downloadNBRStorageFile soapenv:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:ns1=\"NBRStorageService\">
<siteId xsi:type=\"xsd:long\">%s</siteId>
<recordId xsi:type=\"xsd:long\">%s</recordId>
<ticket xsi:type=\"xsd:string\">%s</ticket>
</ns1:downloadNBRStorageFile>
</soapenv:Body>
</soapenv:Envelope>", siteID, recordingID, token)
The values passed into this function and used by the string formatting are:
siteID
: This is a six digit number that you can get from the Site Information on your Webex Administration pagerecordingID
: This is obtained from listing the recordingstoken
: this is obtained using another function after prompting the user for their username and password
We then need to create a connection to Webex and send the XML body. In addition, we need to specify the Accept
header as application/soap+xml, application/dime, multipart/related, text/*
and an empty SOAPAction
:
client := &http.Client{}
req, _ := http.NewRequest("POST", URL, strings.NewReader(xmlBody))
req.Header.Set("Content-Type", "application/xml; charset=utf-8")
req.Header.Set("SOAPAction", "''")
req.Header.Set("Accept", "application/soap+xml, application/dime, multipart/related, text/*")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
The URL
referred to is http://[data center specific domain]/nbr/services/nbrXMLService
where [data center specific domain]
is something you have to obtain from Cisco and would be different for different tenants.
At this point, we’ve made the request and now have a response body we can process. The response is a multipart body consisting of three parts: the XML file response; a file telling us the size and name; and the binary recording.
To process this I used the “mime” and “mime/multipart” libraries which is what makes this much easier than other languages I tried. Essentially we just loop through the three parts and when we get to the binary file, we create a new local file and write the bytes straight into it.
mediaType, params, error := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if error != nil {
return "", error
}
if strings.HasPrefix(mediaType, "multipart/") {
mr := multipart.NewReader(resp.Body, params["boundary"])
embeddedfilename := ""
filename := recording.HostWebExID + "_" + recording.RecordingID + "_"
// Since we always get three parts:
for i := 0; i < 3; i++ {
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
// XML File
if i == 0 {
//do nothing since we don't need the xml file
}
// File name and Size and Encryption
if i == 1 {
slurp, err := ioutil.ReadAll(p)
if err != nil {
return "", err
}
embeddedfilename = strings.Split(string(slurp), "\n")[0]
filename += embeddedfilename
fmt.Println("Downloading " + filename)
}
// The Recording
if i == 2 && embeddedfilename != "" {
fo, err := os.Create(filename)
if err != nil {
return "", err
}
defer fo.Close()
w := bufio.NewWriter(fo)
buf := make([]byte, 1024)
for {
// Reading
n, err := p.Read(buf)
if err != nil && err != io.EOF {
return "", err
}
if n == 0 {
break
}
// Write to file
if _, err := w.Write(buf[:n]); err != nil {
return "", err
}
}
}
}
}
The output from running the list command followed by the download command is as follows:
$ wt list -u dparkinson
Enter Password:
Host WebExID RecordingID Size CreateTime Name
------------ ----------- ---- ---------- ----
dparkinson 66768892 460.049 05/04/2019 16:44:48 Test Meeting
$ wt download -r 66768892
Enter Username: dparkinson
Enter Password:
Downloading Test Meeting.mp4
Clearly whilst looking at any of this code, bear in mind that this is the first Go code I had ever written. In this instance it was a simple function, but if I were to build this again, I’d almost certainly use methods on a type and allow the caller to pass in their own http client.
The good news is that a lot of this will get easier with the release of the new “RESTful API” which is coming soon. Apparently it will be similar to the Webex Teams API so will be more clearly documented and a whole lot simpler to use – watch this space.