Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Test

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: gofmt
run: test -z "$(gofmt -l .)"
- run: go vet ./...
- run: go test -race ./...
145 changes: 78 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,108 +1,119 @@
# sm-cli

sm-cli is a command-line tool written in Go for managing server operations via Redfish API. It provides commands to interact with BMC (Baseboard Management Controller) systems such as powering on/off servers, managing virtual media, and setting boot options.
sm-cli is a command-line tool written in Go for managing Supermicro servers through their BMC (Baseboard Management Controller) using the Redfish API: power, boot overrides, virtual media, users, IP access control, and read-only diagnostics (system info, sensors, event log).

I started using this tool because it takes a lot of time to log in to all IPMIs and adjust certain things, especially when you are dealing with 100+ servers. This tool allows you (with a simple bash statement) to walk over all hosts and run tasks in batches.
I started using this tool because it takes a lot of time to log in to all IPMIs and adjust certain things, especially when you are dealing with 100+ servers. sm-cli works on many hosts at once, in parallel.

## Features
- Power Management: Control server power state (on, off, restart).
- Virtual Media: Insert and eject virtual media.
- Boot Options: Set boot order to CD or PXE once.
- Add new users
- Enable ACL and add rules

I am adding more features to this tool as I continue to work on it. Feel free to contribute or suggest new features!
- Power: status, on, off, restart, graceful shutdown.
- Diagnostics: system info (model, serial, BIOS/BMC firmware, health), sensors (temperatures, fans, PSUs, voltages), hardware event log.
- Virtual media: insert, eject, status.
- Boot: CD or PXE once (UEFI).
- Users: add an Administrator user.
- IP access control: enable, add rules.
- Hosts from an inventory file or from NetBox; select by name, rack, site, tag or role.
- Parallel execution, per-host results, text or JSON output, non-zero exit code when a host fails.
- Safety: `--dry-run` for changes, `--yes` required to change more than one host, passwords masked in verbose output.

## Installation
Clone the repository and build the binary:
Grab a pre-built binary from the [releases](https://github.com/Charlie-Root/smcli/releases) page, or build it:

```
git clone https://github.com/Charlie-Root/smcli.git
cd sm-cli
go build -o sm-cli main.go
cd smcli
go build -o sm-cli .
```
Make sure to have Go installed on your system.

Or grab one of the pre-built binaries from the [releases](https://github.com/Charlie-Root/smcli/releases) page.
## Hosts

## Usage
The sm-cli tool supports the following commands:
### Inventory file (default)
Configure your servers in `inventory.yaml` (or pass `--inventory <file>`, `-` reads it from stdin):

### Power Management
```
./sm-cli power [on|off|restart] <hostname>
```yaml
hosts:
- name: host1
bmc_address: x.x.x.x
username_password: ADMIN:xxxxxx
iso_image: http://some.server/debian.iso
```

#### Examples:
### NetBox
With `--source netbox` (or `SMCLI_SOURCE=netbox`), sm-cli looks hosts up in NetBox instead. Set `NETBOX_URL` and `NETBOX_TOKEN`. For each device it follows:

```
# Power on a server
./sm-cli power on host1
device → `oob_ip` → the interface that IP is assigned to → interface custom fields `ipmi_username` and `ipmi_password`.

# Power off a server
./sm-cli power off host2
Credentials are only held in memory. Devices without an OOB IP or without credentials are reported as failed for that host.

# Restart a server
./sm-cli power restart host3
```
Select hosts by name, or with `--rack`, `--site`, `--tag` and `--role` (NetBox slugs; filters combine):

### Virtual Media Management
```
./sm-cli media [insert|eject|status] <hostname>
sm-cli --source netbox power status --rack A.R10 --role server
```

#### Examples:

```
# Insert virtual media from ISO image
./sm-cli media insert host1
## Usage

# Eject virtual media
./sm-cli media eject host2
Hosts are given after the action. Every command accepts one or more hosts.

# Check virtual media status
./sm-cli media status host3
### Diagnostics (read-only)
```
sm-cli power status host1 host2
sm-cli info host1
sm-cli sensors host1 [--problems]
sm-cli sel host1 [--since 24h] [--limit 50] [--manager]
sm-cli media status host1
```
`sel --since 0` shows the whole log; `--manager` adds the BMC's own logs.

### Boot Options
### Power
```
./sm-cli boot [cd|pxe] <hostname>
sm-cli power [on|off|restart|shutdown] <host...>
```
`off` and `restart` are forced; `shutdown` asks the OS to shut down (ACPI).

#### Examples:

### Boot
```
sm-cli boot [cd|pxe] <host...>
```
# Set boot order to CD once
./sm-cli boot cd host1

# Set boot order to PXE once
./sm-cli boot pxe host2
```
### Virtual media
```
sm-cli media [insert|eject|status] <host...> [--iso http://some.server/debian.iso]
```

## Configuration
### Users and access control
```
sm-cli user <username> <password|-> <host...>
sm-cli acl enable <host...>
sm-cli acl add <address> <prefix> <policy> <host...>
```
Use `-` as password to read it from stdin, so it does not show up in the process list or shell history.

Configure your servers in inventory.yaml:
### Global flags
| Flag | Default | Meaning |
|---|---|---|
| `--source` | `file` (`SMCLI_SOURCE`) | `file` or `netbox` |
| `--inventory` | `inventory.yaml` | inventory file, `-` for stdin |
| `--rack`, `--site`, `--tag`, `--role` | | NetBox selection |
| `--parallel` | `5` | hosts at the same time |
| `--timeout` | `15s` | per request |
| `-o`, `--output` | `text` | `text` or `json` |
| `--dry-run` | | show changes without sending them |
| `-y`, `--yes` | | required to change more than one host |
| `-v`, `--verbose` | | log requests to stderr, passwords masked |

```yaml
hosts:
- name: host1
bmc_address: x.x.x.x
username_password: ADMIN:xxxxxx
iso_image: http://some.server/debian.iso
```
Ensure each server entry includes its hostname, BMC IP address, username/password for authentication, and ISO image URL for virtual media operations.
Exit code is 0 when every host succeeded, 1 otherwise.

## Additional Notes
## Notes

### SSL Verification:
By default, SSL verification is skipped for BMC connections. Modify main.go to change this behavior if required.
### TLS
Certificate verification is skipped for BMC connections (BMCs use self-signed certificates). NetBox connections are verified.

### Verbose Mode:
Use -v or --verbose flag to enable detailed output for requests.
### Compatibility
The command syntax of earlier versions still works (`sm-cli power on host1`, `sm-cli user <name> <password> <host>`, `sm-cli acl add ...`).

# Contributing
## Contributing

Contributions are welcome! Fork the repository, create a new branch, make your changes, and submit a pull request.
Contributions are welcome! Fork the repository, create a new branch, make your changes, run `go test ./...`, and submit a pull request.

# References
https://www.supermicro.com/manuals/other/redfish-ref-guide-html/Content/general-content/available-apis.htm
## References
https://www.supermicro.com/manuals/other/redfish-ref-guide-html/Content/general-content/available-apis.htm
47 changes: 19 additions & 28 deletions cmd/acl.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,40 @@
package cmd

import (
"fmt"
"strconv"

"github.com/Charlie-Root/smcli/host"
"github.com/spf13/cobra"
)

// Parent ACL command (no direct functionality)
var AclCmd = &cobra.Command{
Use: "acl",
Short: "Manage ACL settings",
Short: "Manage the BMC IP access control list (Supermicro)",
}

// Subcommand to enable ACL on the host
var EnableAclCmd = &cobra.Command{
Use: "enable <host>",
Short: "Enable ACL on the specified host",
Args: cobra.ExactArgs(1), // Expect exactly one argument for the host
Run: func(cmd *cobra.Command, args []string) {
hostName := args[0]
host.SelectHost(hostName)
host.ServerEnableACL() // Call to enable ACL on the host
Use: "enable [host...]",
Short: "Enable IP access control",
RunE: func(cmd *cobra.Command, args []string) error {
return run(args, true, func(c *host.Client) (host.Result, error) { return c.EnableACL() })
},
}

// Subcommand to add an ACL entry
var AddAclCmd = &cobra.Command{
Use: "add <address> <prefix> <policy> <host>",
Short: "Add new ACL entry",
Args: cobra.ExactArgs(4), // Expect address, prefix, policy, and host as arguments
Run: func(cmd *cobra.Command, args []string) {
address := args[0]
prefix := args[1]
policy := args[2]
hostName := args[3]

host.SelectHost(hostName)
host.ServerCreateACL(address, prefix, policy) // Call to add ACL entry
Use: "add <address> <prefix> <policy> [host...]",
Short: "Add an IP access control rule",
Args: cobra.MinimumNArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
address, policy, hosts := args[0], args[2], args[3:]
prefix, err := strconv.Atoi(args[1])
if err != nil || prefix < 0 || prefix > 128 {
return fmt.Errorf("invalid prefix length %q", args[1])
}
return run(hosts, true, func(c *host.Client) (host.Result, error) { return c.AddACL(address, prefix, policy) })
},
}

func init() {
// Register subcommands under the acl command
AclCmd.AddCommand(EnableAclCmd)
AclCmd.AddCommand(AddAclCmd)

// Register acl command to the root command (assuming RootCmd is the root cobra command)
RootCmd.AddCommand(AclCmd)
AclCmd.AddCommand(EnableAclCmd, AddAclCmd)
}
26 changes: 9 additions & 17 deletions cmd/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,15 @@ import (
)

var BootCmd = &cobra.Command{
Use: "boot [cd|pxe]",
Short: "Set boot order",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
action := args[0]
hostName := args[1]

host.SelectHost(hostName)
host.GetRedfishPath()

switch action {
case "cd":
host.ServerSetBootOnceCD()
case "pxe":
host.ServerSetBootOncePXE()
default:
fmt.Println("Unknown boot command:", action)
Use: "boot [cd|pxe] [host...]",
Short: "Boot from CD or PXE once (UEFI)",
Args: cobra.MinimumNArgs(1),
ValidArgs: []string{"cd", "pxe"},
RunE: func(cmd *cobra.Command, args []string) error {
target, hosts := args[0], args[1:]
if target != "cd" && target != "pxe" {
return fmt.Errorf("unknown boot target %q (want cd or pxe)", target)
}
return run(hosts, true, func(c *host.Client) (host.Result, error) { return c.BootOnce(target) })
},
}
47 changes: 47 additions & 0 deletions cmd/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cmd

import (
"time"

"github.com/Charlie-Root/smcli/host"
"github.com/spf13/cobra"
)

var InfoCmd = &cobra.Command{
Use: "info [host...]",
Short: "Show model, serial, BIOS/BMC firmware, power state and health",
RunE: func(cmd *cobra.Command, args []string) error {
return run(args, false, func(c *host.Client) (host.Result, error) { return c.Info() })
},
}

var sensorsProblemsOnly bool

var SensorsCmd = &cobra.Command{
Use: "sensors [host...]",
Short: "Show temperatures, fans, PSUs and voltages",
RunE: func(cmd *cobra.Command, args []string) error {
return run(args, false, func(c *host.Client) (host.Result, error) { return c.Sensors(sensorsProblemsOnly) })
},
}

var (
selSince time.Duration
selLimit int
selManager bool
)

var SelCmd = &cobra.Command{
Use: "sel [host...]",
Short: "Show the hardware event log, newest first",
RunE: func(cmd *cobra.Command, args []string) error {
return run(args, false, func(c *host.Client) (host.Result, error) { return c.SEL(selSince, selLimit, selManager) })
},
}

func init() {
SensorsCmd.Flags().BoolVar(&sensorsProblemsOnly, "problems", false, "only show sensors that are not OK")
SelCmd.Flags().DurationVar(&selSince, "since", 24*time.Hour, "only entries newer than this (0 = all)")
SelCmd.Flags().IntVar(&selLimit, "limit", 50, "maximum entries per host (0 = all)")
SelCmd.Flags().BoolVar(&selManager, "manager", false, "also read the BMC's own logs")
}
Loading
Loading