nodeset

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

go-nodeset

Go Reference CI

ClusterShell node sets for Go. Parse, fold and expand ranges and steps (exe[0001-0010/2]), several numeric dimensions (rack[1-2]node[01-04]), the set operators , ! & ^, and @group references through a resolver of your own. Print sets folded, or as host lists that Slurm accepts. The standard library is its only dependency.

API reference on pkg.go.dev →

Status

The engine was written as the nodeset package of clusterctl, a command-line tool for administering HPC clusters, and moved here with its history once it had shipped in a clusterctl release. Why it is an engine of its own rather than an existing Go library is decision 5.

Usage

import "github.com/GSI-HPC/go-nodeset"

ns, err := nodeset.Parse("rack[1-2]node[01-04]!rack1node02")
if err != nil {
	return err
}
fmt.Println(ns)            // rack2node[01-04],rack1node[01,03-04]
fmt.Println(ns.Len())      // 7
fmt.Println(ns.Hostlist()) // rack1node[01,03-04],rack2node[01-04], for Slurm

// @group references go through a resolver of your own.
groups := nodeset.NewMapResolver("site", map[string]string{"gpu": "rack2node[03-04]"})
cpu, err := nodeset.ParseWith("rack[1-2]node[01-04]!@gpu", groups)
if err != nil {
	return err
}
fmt.Println(cpu) // rack1node[01-04],rack2node[01-02]

The language, and where it differs from ClusterShell, is described in doc/language.md; the API, with more examples, on pkg.go.dev.

Install

$ go get github.com/GSI-HPC/go-nodeset

It needs Go 1.26 or newer: the module requires the oldest Go release the Go project still supports, and follows it up after each Go release (decision 2).

Versions

Releases are signed tags vX.Y.Z, and each has release notes. The module is stable from v1.0.0: a minor or patch release breaks no API, changes no expression's hosts and changes no folded output (decision 10).

Contributing

The repository carries a mise.toml, so the toolchain comes from mise if you use it:

$ mise install    # Go and golangci-lint, at the versions CI uses
$ make lint       # golangci-lint
$ make test       # tests under the race detector
$ make cover      # every file at 100% coverage
$ make help       # every other check CI runs

Commits are Conventional Commits. How the module is built and why is in doc/, and a change that takes a decision adds it to doc/decisions.md. Instructions for coding agents are in AGENTS.md. Report vulnerabilities as SECURITY.md says.

AI disclosure

This project is developed with the help of AI coding tools. Changes written by Anthropic's Claude Code agent are committed as Claude <noreply@anthropic.com> and/or carry a Co-Authored-By: Claude … trailer.

Licence

Copyright 2026 GSI Helmholtz Centre for Heavy Ion Research GmbH http://www.gsi.de

Apache-2.0. See LICENSE.

Documentation

Overview

Package nodeset parses, folds and expands ClusterShell style node sets, such as exe[0001-0010/2] or rack[1-2]node[01-04]!@drained.

A node set expression names a set of hosts. Numeric parts of a host name may be written as a bracketed range, and several expressions may be combined with set operators:

exe[0001-0010]            a padded range
exe[1-10/2]               a range with a step
rack[1-2]node[01-04]      two independent numeric dimensions
@compute                  a group, resolved by a Resolver
exe[1-10],sub[1-2]        union
exe[1-10]!exe5            difference
exe[1-10]&@idle           intersection
exe[1-10]^@drained        symmetric difference

Operators have no precedence; an expression is evaluated strictly from left to right. Whitespace acts as a union operator, so the arguments of a command line may be joined with a space and parsed in one call. The operators !, & and ^ need an operand on each side: "exe[1-10]&" is an error, not exe[1-10].

Node names

Every maximal run of digits in a name is a numeric dimension, whether or not it was written in brackets. "exe0001" and "exe[0001]" parse identically, and "10.0.1.7" has four dimensions. A dimension holding a single value is rendered without brackets, so folding is stable: parsing the output of String and folding it again yields the same string. A name may not begin with "-".

Zero padding is not part of a node's identity: "exe1" and "exe01" are the same host, so Parse("exe1,exe01") has Len 1 and Contains("exe01") is true for a set holding exe1. Each host keeps the spelling it was first given, and a set never shows a host under a name it was not given: "exe[01-02],exe3" prints as exe[01-02,3], and Canonical("exe1") on a set holding exe0001 answers "exe0001". In a range the padding of the first bound applies to the whole range, and a last bound padded to another width, such as exe[1-010], is an error.

A set does not report that it was given one host under two spellings; the second is simply the same member again. A program for which such a pair is an error, such as two machines in an inventory, checks its names itself: before it adds a name, Canonical on the set built so far answers the spelling already held for that host, if there is one.

These rules, and each place where this package differs from ClusterShell, are set out in the language reference.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Lister

type Lister interface {
	// List returns the group names a source offers. An empty source means
	// the default one.
	List(source string) ([]string, error)
	// DefaultSource names the source used when a reference names none.
	DefaultSource() string
}

Lister is implemented by a Resolver that can also say which groups it offers, for a program that lists them or completes a group name. Parsing never needs it.

type MapResolver

type MapResolver struct {
	// Groups maps a source name to its groups.
	Groups map[string]map[string]string
	// Default names the source used when a reference names none.
	Default string
}

MapResolver resolves groups from an in-memory table, such as groups a program reads from its configuration, or those of a test.

func NewMapResolver

func NewMapResolver(source string, groups map[string]string) *MapResolver

NewMapResolver builds a resolver for a single source, which is also the default one.

func (*MapResolver) All

func (m *MapResolver) All(source string) (string, error)

All implements Resolver by unioning every group of the source.

func (*MapResolver) DefaultSource

func (m *MapResolver) DefaultSource() string

DefaultSource implements Lister.

func (*MapResolver) List

func (m *MapResolver) List(source string) ([]string, error)

List implements Lister. An empty source means the default one.

func (*MapResolver) Resolve

func (m *MapResolver) Resolve(source, group string) (string, error)

Resolve implements Resolver. An empty source means the default one. A group the source does not hold names no hosts, as it does in a static source of ClusterShell; a source the table does not hold is an error.

type NodeSet

type NodeSet struct {
	// contains filtered or unexported fields
}

NodeSet is an unordered set of host names that renders in folded form. The zero value is not usable; call New or Parse.

Padding is not part of a host's identity: exe1 and exe01 are one host. Each host keeps the spelling it was first given, and when sets are combined the spelling already held wins, so a set never shows a host under a name it was not given.

A set may be read from several goroutines at once. Add changes it, and needs the set to itself.

func MustParse

func MustParse(expr string, opts ...Option) *NodeSet

MustParse is Parse for expressions fixed at compile time; it panics on a parse error.

func New

func New(opts ...Option) *NodeSet

New returns an empty set.

func Parse

func Parse(expr string, opts ...Option) (*NodeSet, error)

Parse evaluates a node set expression without resolving groups. An expression containing a group reference is rejected.

Example
package main

import (
	"fmt"

	"github.com/GSI-HPC/go-nodeset"
)

func main() {
	ns, err := nodeset.Parse("exe[0001-0010]!exe0003")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(ns, ns.Len())

	// Operators have no precedence: the expression is read from left to
	// right.
	ns, err = nodeset.Parse("exe[1-10]!exe[1-5]&exe[1-7]")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(ns)

	_, err = nodeset.Parse("exe[1-10]&")
	fmt.Println(err)
}
Output:
exe[0001-0002,0004-0010] 9
exe[6-7]
in "exe[1-10]&": the & operator has no right operand

func ParseWith

func ParseWith(expr string, res Resolver, opts ...Option) (*NodeSet, error)

ParseWith evaluates a node set expression, resolving group references through res. A nil resolver rejects every group reference.

Example
package main

import (
	"fmt"

	"github.com/GSI-HPC/go-nodeset"
)

func main() {
	// A group may refer to other groups; the resolver's answer is parsed in
	// turn.
	res := nodeset.NewMapResolver("site", map[string]string{
		"compute": "exe[0001-0100]",
		"gpu":     "exe[0097-0100]",
		"cpu":     "@compute!@gpu",
	})

	ns, err := nodeset.ParseWith("@cpu&exe[0090-0200]", res)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(ns)

	// @* asks the resolver for every host of its default source.
	ns, err = nodeset.ParseWith("@*", res)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(ns.Len())

	// A group the table does not hold names no hosts, as in ClusterShell; a
	// source it does not hold is an error.
	ns, err = nodeset.ParseWith("@login,exe0001", res)
	fmt.Println(ns, err)
	_, err = nodeset.ParseWith("@slurm:idle", res)
	fmt.Println(err)
}
Output:
exe[0090-0096]
100
exe0001 <nil>
group @slurm:idle: unknown group source "slurm"

func (*NodeSet) Add

func (ns *NodeSet) Add(expr string) error

Add parses an expression, without resolving groups, and adds the hosts it names to the set.

func (*NodeSet) Canonical

func (ns *NodeSet) Canonical(name string) (string, bool)

Canonical returns the name this set holds for a host, which is always a name the set was given.

It differs from the name asked about when the two were written with different padding: a set holding exe0001 answers "exe0001" when asked about "exe1", because padding is not part of a host's identity and both name one host. A set holding exe0001 and exe11 answers "exe11" for exe11.

func (*NodeSet) Clone

func (ns *NodeSet) Clone() *NodeSet

Clone returns an independent copy.

func (*NodeSet) Contains

func (ns *NodeSet) Contains(name string) bool

Contains reports whether name is a member. Padding is ignored, so "exe01" and "exe1" name the same host.

func (*NodeSet) Difference

func (ns *NodeSet) Difference(other *NodeSet) *NodeSet

Difference returns the hosts of ns that are not in other.

func (*NodeSet) Expand

func (ns *NodeSet) Expand() []string

Expand returns the host names in the order ClusterShell lists them: by pattern first; within a pattern with one number, in numeric order; within one with several, vector by vector as String folds them, each with its last number varying fastest.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/GSI-HPC/go-nodeset"
)

func main() {
	// Hosts with one number come in order of their numbers, not of their
	// names as strings.
	ns := nodeset.MustParse("exe[9-11],rack[1-2]node[1-2]")
	fmt.Println(strings.Join(ns.Expand(), " "))
}
Output:
exe9 exe10 exe11 rack1node1 rack1node2 rack2node1 rack2node2

func (*NodeSet) Hostlist

func (ns *NodeSet) Hostlist() string

Hostlist renders the set in the syntax Slurm and FreeIPMI accept: one bracketed range per name at most, and no steps.

Each pattern is folded along the one dimension that gives the fewest names, so rack[1-2]node[001-100] becomes rack1node[001-100],rack2node[001-100] rather than two hundred names, and the argument stays short. Both parsers read several bracketed dimensions in one name as well, but this form is the one every version of them reads.

Example
package main

import (
	"fmt"

	"github.com/GSI-HPC/go-nodeset"
)

func main() {
	// For Slurm and FreeIPMI: one bracketed range per name at most, and no
	// steps.
	ns := nodeset.MustParse("rack[1-2]node[001-100],exe[1-9/2]")
	fmt.Println(ns)
	fmt.Println(ns.Hostlist())
}
Output:
exe[1,3,5,7,9],rack[1-2]node[001-100]
exe[1,3,5,7,9],rack1node[001-100],rack2node[001-100]

func (*NodeSet) Intersection

func (ns *NodeSet) Intersection(other *NodeSet) *NodeSet

Intersection returns the hosts in both sets.

func (*NodeSet) IsEmpty

func (ns *NodeSet) IsEmpty() bool

IsEmpty reports whether the set names no host.

func (*NodeSet) Len

func (ns *NodeSet) Len() int

Len reports the number of hosts in the set.

func (*NodeSet) Split

func (ns *NodeSet) Split(n int) []*NodeSet

Split partitions the set into at most n chunks of near equal size, in expansion order. It returns nil for n below one.

func (*NodeSet) String

func (ns *NodeSet) String() string

String renders the set in folded form, which Parse reads back as the same set, every host spelled as before. An empty set renders as the empty string.

Example
package main

import (
	"fmt"

	"github.com/GSI-HPC/go-nodeset"
)

func main() {
	// Hosts written one by one fold into ranges. exe3 is exe03 again, since
	// padding is not part of a host's identity, and a host keeps the spelling
	// it was first given.
	ns := nodeset.MustParse("exe01,exe02,exe03,exe3 exe05 login")
	fmt.Println(ns)

	// Names with several numbers fold as ClusterShell folds them.
	fmt.Println(nodeset.MustParse("rack1node01,rack1node02,rack2node01,rack2node02"))

	// Steps are written only when asked for.
	fmt.Println(nodeset.MustParse("exe[1-9/2]"))
	fmt.Println(nodeset.MustParse("exe[1-9/2]", nodeset.WithAutostep(3)))
}
Output:
exe[01-03,05],login
rack[1-2]node[01-02]
exe[1,3,5,7,9]
exe[1-9/2]

func (*NodeSet) SymmetricDifference

func (ns *NodeSet) SymmetricDifference(other *NodeSet) *NodeSet

SymmetricDifference returns the hosts in exactly one of the two sets.

func (*NodeSet) Union

func (ns *NodeSet) Union(other *NodeSet) *NodeSet

Union returns the hosts in either set.

type Option

type Option func(*NodeSet)

Option configures a set.

func WithAutostep

func WithAutostep(n int) Option

WithAutostep folds arithmetic progressions of at least n elements into "first-last/step" form, taking the values from left to right as ClusterShell's autostep does. ClusterShell disables this by default, and so does this package; n below 2 keeps it disabled.

type Resolver

type Resolver interface {
	// Resolve returns the expression a group names; an empty group returns
	// an empty expression. What an unknown group means is the resolver's to
	// decide: an error, for a program that must not select nothing by
	// mistake, or no hosts, as ClusterShell's static sources and MapResolver
	// answer. The name is empty for a reference written @ or @source:.
	Resolve(source, group string) (string, error)
	// All returns the expression naming every host a source knows. It
	// answers the reference @source:*, or @* for an empty source.
	All(source string) (string, error)
}

Resolver turns a group reference into a node set expression.

A reference is written @group, or @source:group when several group sources are configured. The expression a resolver returns is parsed in turn, so a group may refer to other groups.

The source is passed on as written, and is empty for a bare @group: which source that means, the default one or a search of several, is the resolver's to decide. A bare @group inside a group of a named source is passed on with that source, as ClusterShell resolves it.

Jump to

Keyboard shortcuts

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