gocmlclient

package module
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2026 License: MIT Imports: 3 Imported by: 0

README

Go Reference CodeQL Go Coverage Status Go Report Card

gocmlclient

A comprehensive Go client library for Cisco Modeling Labs (CML) 2.x, providing modern service-based APIs.

Table of Contents

Features

  • 🚀 Modern Service Architecture: Clean, modular design with dedicated services for each resource type
  • 🔄 Focused API: Modern, service-based client (not a drop-in replacement for older gocmlclient versions)
  • 🔐 Flexible Authentication: Support for username/password, tokens, and custom providers
  • 🛡️ Production Ready: Comprehensive error handling, retries, and connection management
  • 📊 Built-in Monitoring: Request/response statistics and health checks
  • 🧪 Well Tested: High test coverage with race detection and integration tests
  • 📚 Rich Documentation: Comprehensive examples and API documentation

Installation

go get github.com/rschmied/gocmlclient

Requirements:

  • Go 1.25 or later
  • Access to a CML 2.x controller (version 2.9.0+ recommended; 2.9/2.10 tested)

Quick Start

More examples:

  • Runnable programs: examples/
  • API docs and examples: pkg.go.dev (click the Go Reference badge)
package main

import (
    "context"
    "log"
    "github.com/rschmied/gocmlclient"
)

func main() {
    // Create client with authentication
    client, err := gocmlclient.New("https://cml-controller.example.com",
        gocmlclient.WithUsernamePassword("admin", "password"))
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()

    // List lab IDs (use show_all=true)
    labs, err := client.Lab.Labs(ctx, true)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Found %d labs", len(labs))
}

Configuration

System Readiness Check

By default, the client automatically performs a system readiness check during initialization to ensure the CML server is compatible and ready. This check:

  • Verifies the server is running and accessible
  • Validates version compatibility (>=2.9.0, <3.0.0)
  • Caches version information for subsequent operations
  • Checks for named configuration support (>=2.7.0)

If you need to skip this check (e.g., for testing or when working with servers that don't support the system_information endpoint) or when working with older versions (might or might not work, untested):

client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.SkipReadyCheck())
Authentication

The client supports multiple authentication methods:

// Using username/password
client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithUsernamePassword("username", "password"))

// Using token
client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithToken("your-token"))

// Skip TLS verification (for development)
client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithInsecureTLS())

// Combine options
client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithUsernamePassword("username", "password"),
    gocmlclient.WithTokenStorageFile("/tmp/cml_tokens.json"),
    gocmlclient.WithInsecureTLS(),
    gocmlclient.SkipReadyCheck())

// Add a static proxy/auth header to every outbound request
client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithStaticToken("your-token"),
    gocmlclient.WithRequestHeader("X-Proxy-Token", os.Getenv("CML_PROXY_TOKEN")))

// Add a Bearer token in Proxy-Authorization, similar to IAP-style proxy auth
proxyToken := os.Getenv("CML_PROXY_TOKEN")
client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithStaticToken("your-token"),
    gocmlclient.WithRequestHeader("Proxy-Authorization", "Bearer "+proxyToken))

WithRequestHeader and WithRequestHeaders apply to all outbound HTTP calls, including authentication bootstrap requests such as /api/v0/auth_extended. This makes them suitable for proxies or gateways that require additional static headers. Empty header values are ignored.

If your proxy expects an IAP-style bearer token in Proxy-Authorization, load the token in caller code and pass "Bearer "+token as the header value. See examples/auth-proxy-header/main.go for a runnable example.

Token Persistence

By default, tokens are cached in memory for the lifetime of the client. To reuse tokens across process restarts (e.g., Terraform runs), configure file-based token storage:

client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithUsernamePassword("username", "password"),
    gocmlclient.WithTokenStorageFile("/tmp/cml_tokens.json"))

Note: the token file can contain a valid bearer token; secure and clean it up per your environment.

API Reference

Note: In the examples below, UUIDs are represented as short strings like models.UUID("lab-uuid") for brevity. In production, these would be actual UUIDs (e.g., models.UUID("123e4567-e89b-12d3-a456-426614174000")).

Labs

Manage CML labs including creation, configuration, and lifecycle operations.

// Get all labs
labs, err := client.Lab.Labs(ctx, true) // true sets show_all=true

// Get labs with topology tile data (fast endpoint)
tiles, err := client.Lab.LabsWithData(ctx) // GET /populate_lab_tiles

// Get lab by ID
lab, err := client.Lab.GetByID(ctx, models.UUID("lab-uuid"), true)

// Get lab by title
lab, err := client.Lab.GetByTitle(ctx, "My Lab", true)

// Create a new lab
newLab := models.LabCreateRequest{
    Title:       "New Lab",
    Description: "A new CML lab",
    Notes:       "Created via API",
}
createdLab, err := client.Lab.Create(ctx, newLab)

// Update lab metadata
updateReq := models.LabUpdateRequest{
    Title:       "Updated Title",
    Description: "Updated description",
}
updatedLab, err := client.Lab.Update(ctx, models.UUID("lab-uuid"), updateReq)

// Node staging (CML 2.10+; same request shape as used by the UI)
_, err = client.Lab.Update(ctx, models.UUID("lab-uuid"), models.LabUpdateRequest{
     NodeStaging: &models.NodeStaging{Enabled: false, StartRemaining: true, AbortOnFailure: false},
})

// Control lab lifecycle
err = client.Lab.Start(ctx, models.UUID("lab-uuid"))
err = client.Lab.Stop(ctx, models.UUID("lab-uuid"))
err = client.Lab.Wipe(ctx, models.UUID("lab-uuid"))
err = client.Lab.Delete(ctx, models.UUID("lab-uuid"))

// Import lab from topology
lab, err := client.Lab.Import(ctx, topologyYAML)

// Check convergence
converged, err := client.Lab.HasConverged(ctx, models.UUID("lab-uuid"))
Nodes

Manage individual nodes within labs.

// Get nodes for a lab
nodes, err := client.Node.GetNodesForLab(ctx, models.UUID("lab-uuid"))

// Get specific node
node, err := client.Node.GetByID(ctx, models.UUID("lab-uuid"), models.UUID("node-uuid"))

// Create a new node
ram := 512
img := "vios-adventerprisek9-m"
newNode := models.Node{
    LabID:           models.UUID("lab-uuid"),
    Label:           "Router1",
    NodeDefinition:  "iosv",
    ImageDefinition: &img,
    CPUs:            1,
    RAM:             &ram,
    X:               100,
    Y:               100,
}
createdNode, err := client.Node.Create(ctx, newNode)

// Update node configuration
updatedNode, err := client.Node.Update(ctx, existingNode)

// Set node configuration
err = client.Node.SetConfig(ctx, &node, "interface GigabitEthernet0/0\n ip address 192.168.1.1 255.255.255.0")

// Set named configurations
configs := []models.NodeConfig{
    {Name: "startup", Content: "hostname R1\ninterface GigabitEthernet0/0\n ip address 192.168.1.1 255.255.255.0"},
}
err = client.Node.SetNamedConfigs(ctx, &node, configs)

// Control node lifecycle
err = client.Node.Start(ctx, models.UUID("lab-uuid"), models.UUID("node-uuid"))
err = client.Node.Stop(ctx, models.UUID("lab-uuid"), models.UUID("node-uuid"))
err = client.Node.Wipe(ctx, models.UUID("lab-uuid"), models.UUID("node-uuid"))
err = client.Node.Delete(ctx, models.UUID("lab-uuid"), models.UUID("node-uuid"))
Users

Manage CML user accounts and authentication.

// Get all users
users, err := client.User.Users(ctx)

// Get user by ID
user, err := client.User.GetByID(ctx, models.UUID("user-uuid"))

// Get user by name
user, err := client.User.GetByName(ctx, "username")

// Create a new user
newUser := models.UserCreateRequest{
    UserBase: models.UserBase{
        Username: "newuser",
        Fullname: "New User",
        Email:    "user@example.com",
        IsAdmin:  false,
    },
    Password: "securepassword",
}
createdUser, err := client.User.Create(ctx, newUser)

// Update user
updateReq := models.UserUpdateRequest{
    UserBase: models.UserBase{
        Username: "updateduser",
        Fullname: "Updated Name",
        Email:    "updated@example.com",
    },
}
updatedUser, err := client.User.Update(ctx, models.UUID("user-uuid"), updateReq)

// Delete user
err = client.User.Delete(ctx, models.UUID("user-uuid"))

// Get user's groups
groups, err := client.User.Groups(ctx, models.UUID("user-uuid"))
Groups

Manage user groups and permissions.

// Get all groups
groups, err := client.Group.Groups(ctx)

// Get group by ID
group, err := client.Group.GetByID(ctx, models.UUID("group-uuid"))

// Get group by name
group, err := client.Group.ByName(ctx, "groupname")

// Create a new group
newGroup := models.Group{
    Name:        "Students",
    Description: "Student group",
    Members:     []string{"user1-uuid", "user2-uuid"},
}
createdGroup, err := client.Group.Create(ctx, newGroup)

// Update group
updatedGroup, err := client.Group.Update(ctx, existingGroup)

// Delete group
err = client.Group.Delete(ctx, models.UUID("group-uuid"))

Manage network links between nodes.

// Get links for a lab
links, err := client.Link.GetLinksForLab(ctx, models.UUID("lab-uuid"))

// Get specific link
link, err := client.Link.GetByID(ctx, models.UUID("lab-uuid"), models.UUID("link-uuid"))

// Create a new link
newLink := models.Link{
    LabID:  models.UUID("lab-uuid"),
    SrcNode: models.UUID("node1-uuid"),
    DstNode: models.UUID("node2-uuid"),
    SrcSlot: 0,
    DstSlot: 1,
}
createdLink, err := client.Link.Create(ctx, newLink)

// Delete link
err = client.Link.Delete(ctx, models.UUID("lab-uuid"), models.UUID("link-uuid"))

// Link conditions (if supported)
condition, err := client.Link.GetCondition(ctx, models.UUID("lab-uuid"), models.UUID("link-uuid"))
err = client.Link.SetCondition(ctx, models.UUID("lab-uuid"), models.UUID("link-uuid"), conditionConfig)
err = client.Link.DeleteCondition(ctx, models.UUID("lab-uuid"), models.UUID("link-uuid"))
Interfaces

Manage network interfaces on nodes.

// Get interfaces for a node
interfaces, err := client.Interface.GetInterfacesForNode(ctx, models.UUID("lab-uuid"), models.UUID("node-uuid"))

// Get specific interface
iface, err := client.Interface.GetByID(ctx, models.UUID("lab-uuid"), models.UUID("interface-uuid"))

// Create a new interface
newInterface, err := client.Interface.Create(ctx, models.UUID("lab-uuid"), models.UUID("node-uuid"), 0) // slot 0
Annotations

Manage classic annotations (text/rectangle/ellipse/line) and smart annotations.

// Create a text annotation
create := models.AnnotationCreate{
 Type: models.AnnotationTypeText,
 Text: &models.TextAnnotation{
  Type:        models.AnnotationTypeText,
  BorderColor: "#000000",
  BorderStyle: "",
  Color:       "#ffffff",
  Thickness:   1,
  X1:          10,
  Y1:          10,
  ZIndex:      0,
  Rotation:    0,
  TextBold:    false,
  TextContent: "hello",
  TextFont:    "sans",
  TextItalic:  false,
  TextSize:    12,
  TextUnit:    "px",
 },
}
ann, err := client.Annotation.Create(ctx, models.UUID("lab-uuid"), create)

 // List annotations
anns, err := client.Annotation.List(ctx, models.UUID("lab-uuid"))

 // Patch an annotation (OpenAPI requires `type`)
 updated := "hello-updated"
 upd := models.AnnotationUpdate{Type: models.AnnotationTypeText, Text: &models.TextAnnotationPartial{Type: models.AnnotationTypeText, TextContent: &updated}}
ann, err = client.Annotation.Update(ctx, models.UUID("lab-uuid"), ann.Text.ID, upd)

 // Line annotations: line_start/line_end are required but may be null.
 // On PATCH, gocmlclient always includes these keys so callers can send explicit nulls.
 arrow := models.LineStyleArrow
 lineCreate := models.AnnotationCreate{Type: models.AnnotationTypeLine, Line: &models.LineAnnotation{Type: models.AnnotationTypeLine, BorderColor: "#000000", BorderStyle: "", Color: "#ffffff", Thickness: 1, X1: 10, Y1: 10, X2: 100, Y2: 10, ZIndex: 0, LineStart: &arrow, LineEnd: &arrow}}
line, err := client.Annotation.Create(ctx, models.UUID("lab-uuid"), lineCreate)
 if line.Line != nil {
  // Clear both line ends (explicit JSON null)
		_, err = client.Annotation.Update(ctx, models.UUID("lab-uuid"), line.Line.ID, models.AnnotationUpdate{Type: models.AnnotationTypeLine, Line: &models.LineAnnotationPartial{Type: models.AnnotationTypeLine, LineStart: nil, LineEnd: nil}})
 }

 // Delete
err = client.Annotation.Delete(ctx, models.UUID("lab-uuid"), ann.Text.ID)

// Smart annotations
smart, err := client.SmartAnnotation.List(ctx, models.UUID("lab-uuid"))
if len(smart) > 0 {
	_, _ = client.SmartAnnotation.Get(ctx, models.UUID("lab-uuid"), smart[0].ID)
}
System

Access system-level information and configuration.

// Get system version
version := client.System.Version()

// Check version compatibility
compatible, err := client.System.VersionCheck(ctx, ">=2.9.0")

// Check system readiness
err = client.System.Ready(ctx)

// Enable named configurations (if supported)
client.System.UseNamedConfigs()
Image Definitions

Retrieve image definitions available on the controller.

images, err := client.ImageDefinition.ImageDefinitions(ctx) // GET /image_definitions
Node Definitions

Retrieve simplified node definitions available on the controller.

defs, err := client.NodeDefinition.NodeDefinitions(ctx) // GET /simplified_node_definitions
External Connectors

List or fetch external connectors configured on the system.

exts, err := client.ExtConn.List(ctx) // GET /system/external_connectors
ext, err := client.ExtConn.Get(ctx, models.UUID("extconn-uuid")) // GET /system/external_connectors/{id}

Error Handling

The gocmlclient provides comprehensive error handling with specific error types and detailed error messages.

import (
    "errors"
    cmlerror "github.com/rschmied/gocmlclient/pkg/errors"
    "github.com/rschmied/gocmlclient/pkg/models"
)

lab, err := client.Lab.GetByID(ctx, models.UUID("nonexistent-id"), false)
if err != nil {
    if errors.Is(err, cmlerror.ErrElementNotFound) {
        log.Println("Lab not found")
    } else if errors.Is(err, cmlerror.ErrSystemNotReady) {
        log.Println("CML system is not ready")
    } else {
        log.Printf("Unexpected error: %v", err)
    }
}
Common Error Types
  • ErrElementNotFound: Resource not found
  • ErrSystemNotReady: CML system is not accessible or ready
  • ErrAuthenticationFailed: Authentication failed
  • ErrPermissionDenied: Insufficient permissions
  • ErrInvalidRequest: Invalid request parameters

Advanced Usage

Custom HTTP Client
import (
    "net/http"
    "time"
    "github.com/rschmied/gocmlclient"
)

customClient := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    },
}

client, err := gocmlclient.New("https://cml-controller.example.com",
    gocmlclient.WithHTTPClient(customClient),
    gocmlclient.WithUsernamePassword("admin", "password"))
Request Statistics
// Get request statistics
stats := client.Stats()
log.Printf("Total requests: %d", stats.TotalRequests)
log.Printf("Failed requests: %d", stats.FailedRequests)
log.Printf("Average response time: %v", stats.AverageResponseTime)
Concurrent Operations
import (
    "sync"
    "golang.org/x/sync/errgroup"
)

func processLabsConcurrently(ctx context.Context, client *gocmlclient.Client, labIDs []string) error {
    g, gctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // Limit concurrent operations

    for _, labID := range labIDs {
        labID := labID // Capture loop variable
        g.Go(func() error {
            lab, err := client.Lab.GetByID(gctx, labID, false)
            if err != nil {
                return err
            }
            log.Printf("Processed lab: %s", lab.Title)
            return nil
        })
    }

    return g.Wait()
}

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup
# Clone the repository
git clone https://github.com/rschmied/gocmlclient.git
cd gocmlclient

# Install dependencies
go mod download

# Run tests
go test ./...

# Run with race detection
go test -race ./...

# Run linting
make lint

# Or, without make:
go vet ./...
golangci-lint run
Code Style
  • Follow standard Go conventions
  • Use gofmt for formatting
  • Add tests for new functionality
  • Update documentation for API changes

License

Copyright (c) Ralph Schmieder 2022-2026

Licensed under the MIT License. See LICENSE for details.

Documentation

Overview

Package gocmlclient provides a client for Cisco Modeling Labs

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	Conditional                   = client.Conditional
	SkipReadyCheck                = client.SkipReadyCheck
	WithCACertPEM                 = client.WithCACertPEM
	WithHTTPClient                = client.WithHTTPClient
	WithInsecureTLS               = client.WithInsecureTLS
	WithLogLevel                  = client.WithLogLevel
	WithLogger                    = client.WithLogger
	WithNodeExcludeConfigurations = client.WithNodeExcludeConfigurations
	WithRequestHeader             = client.WithRequestHeader
	WithRequestHeaders            = client.WithRequestHeaders
	WithStaticToken               = client.WithStaticToken
	WithToken                     = client.WithToken
	WithTokenStorageFile          = client.WithTokenStorageFile
	WithUsernamePassword          = client.WithUsernamePassword
	WithoutNamedConfigs           = client.WithoutNamedConfigs
)

Re-export common options for convenience.

Functions

func New added in v0.0.14

func New(baseURL string, opts ...client.Option) (*client.Client, error)

New creates a new CML client - convenience constructor

Example

ExampleNew shows the convenience constructor in the root package.

package main

import (
	gocml "github.com/rschmied/gocmlclient"
)

func main() {
	c, err := gocml.New("https://example.invalid",
		gocml.WithStaticToken("bearer-token"),
		gocml.SkipReadyCheck(),
	)
	if err != nil {
		return
	}
	_ = c
}
Example (WithUsernamePassword)

ExampleNew_withUsernamePassword shows username/password auth.

package main

import (
	gocml "github.com/rschmied/gocmlclient"
)

func main() {
	c, err := gocml.New("https://example.invalid",
		gocml.WithUsernamePassword("user", "pass"),
		gocml.SkipReadyCheck(),
	)
	if err != nil {
		return
	}
	_ = c
}

Types

type Lab

type Lab = models.Lab

Lab is a CML lab.

type Node

type Node = models.Node

Node is a CML node.

type Option added in v0.2.3

type Option = client.Option

Option is re-exported for convenience.

type Stats added in v0.2.0

type Stats = api.Stats

Stats represents API client statistics.

Directories

Path Synopsis
cmd
cml-version command
dtest command
examples
auth-token command
auth-userpass command
errors-is command
labs-list command
Package integration contains integration tests.
Package integration contains integration tests.
internal
api
Package api provides the api client
Package api provides the api client
auth
Package auth provides auth service
Package auth provides auth service
httputil
Package httputil provides shared HTTP request building utilities
Package httputil provides shared HTTP request building utilities
logging
Package logging provides an internal logger that can be configured by the client.
Package logging provides an internal logger that can be configured by the client.
services
Package services, annotation specific
Package services, annotation specific
testutil
Package testutil provides some common test functions
Package testutil provides some common test functions
version
Package version provides a version string
Package version provides a version string
pkg
client
Package client provides a Cisco Modeling Labs API client
Package client provides a Cisco Modeling Labs API client
errors
Package errors provides error related functions and types
Package errors provides error related functions and types
models
Package models provides the models for Cisco Modeling Labs here: annotation related types
Package models provides the models for Cisco Modeling Labs here: annotation related types

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL