diff --git a/Dockerfile b/Dockerfile index e10495a..eff36c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ RUN apk --update --no-cache add ipmitool COPY --from=builder /go/src/github.com/vapor-ware/synse-ipmi-plugin/build/plugin ./plugin COPY config.yml . -COPY config/proto /etc/synse/plugin/config/proto EXPOSE 5001 diff --git a/Gopkg.lock b/Gopkg.lock index a070de3..02e8e2b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -8,10 +8,10 @@ version = "v1.0.5" [[projects]] - name = "github.com/fsnotify/fsnotify" + name = "github.com/creasty/defaults" packages = ["."] - revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" - version = "v1.4.7" + revision = "a7e48e2230891fc7456c8ab918c424c5b06c3192" + version = "v1.2.1" [[projects]] name = "github.com/golang/protobuf" @@ -22,37 +22,14 @@ "ptypes/duration", "ptypes/timestamp" ] - revision = "925541529c1fa6821df4e44ce2723319eb2be768" - version = "v1.0.0" - -[[projects]] - branch = "master" - name = "github.com/hashicorp/hcl" - packages = [ - ".", - "hcl/ast", - "hcl/parser", - "hcl/printer", - "hcl/scanner", - "hcl/strconv", - "hcl/token", - "json/parser", - "json/scanner", - "json/token" - ] - revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" - -[[projects]] - name = "github.com/magiconair/properties" - packages = ["."] - revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" - version = "v1.7.6" + revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" + version = "v1.1.0" [[projects]] branch = "master" name = "github.com/mitchellh/mapstructure" packages = ["."] - revision = "00c29f56e2386353d58c599509e8dc3801b0d716" + revision = "bb74f1db0675b241733089d5a1faa5dd8b0ef57b" [[projects]] name = "github.com/patrickmn/go-cache" @@ -60,51 +37,12 @@ revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" version = "v2.1.0" -[[projects]] - name = "github.com/pelletier/go-toml" - packages = ["."] - revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" - version = "v1.1.0" - [[projects]] name = "github.com/rs/xid" packages = ["."] - revision = "02dd45c33376f85d1064355dc790dcc4850596b1" - version = "v1.1" - -[[projects]] - name = "github.com/spf13/afero" - packages = [ - ".", - "mem" - ] - revision = "63644898a8da0bc22138abf860edaf5277b6102e" - version = "v1.1.0" - -[[projects]] - name = "github.com/spf13/cast" - packages = ["."] - revision = "8965335b8c7107321228e3e3702cab9832751bac" + revision = "2c7e97ce663ff82c49656bca3048df0fdd83c5f9" version = "v1.2.0" -[[projects]] - branch = "master" - name = "github.com/spf13/jwalterweatherman" - packages = ["."] - revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" - -[[projects]] - name = "github.com/spf13/pflag" - packages = ["."] - revision = "583c0c0531f06d5278b7d917446061adc344b5cd" - version = "v1.0.1" - -[[projects]] - name = "github.com/spf13/viper" - packages = ["."] - revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" - version = "v1.0.2" - [[projects]] branch = "master" name = "github.com/vapor-ware/goipmi" @@ -112,26 +50,27 @@ revision = "0bfe430a1c3594194822863d09a1a0244ae8f54f" [[projects]] - branch = "master" name = "github.com/vapor-ware/synse-sdk" packages = [ "sdk", - "sdk/config", - "sdk/logger" + "sdk/errors", + "sdk/health", + "sdk/policies" ] - revision = "7d589e9c3a38a24c885377b2aec343ca334a960f" + revision = "a7ed632f114763592399c68aba6bf31b1ca239c5" + version = "1.0.1" [[projects]] - branch = "master" name = "github.com/vapor-ware/synse-server-grpc" packages = ["go"] - revision = "978e8f5af7b012c9d8212616133d8b683945db4b" + revision = "e0c079f1b85081c6a536fb7c5c63cf9b9370c22e" + version = "1.0.0" [[projects]] branch = "master" name = "golang.org/x/crypto" packages = ["ssh/terminal"] - revision = "2b6c08872f4b66da917bb4ce98df4f0307330f78" + revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602" [[projects]] branch = "master" @@ -143,10 +82,9 @@ "http2/hpack", "idna", "internal/timeseries", - "lex/httplex", "trace" ] - revision = "5f9ae10d9af5b1c89ae6904293b14b064d4ada23" + revision = "4cb1c02c05b0e749b0365f61ae859a8e0cfceed9" [[projects]] branch = "master" @@ -155,7 +93,7 @@ "unix", "windows" ] - revision = "79b0c6888797020a994db17c8510466c72fe75d9" + revision = "7138fd3d9dc8335c567ca206f4333fb75eb05d56" [[projects]] name = "golang.org/x/text" @@ -188,7 +126,7 @@ branch = "master" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] - revision = "7fd901a49ba6a7f87732eb344f6e3c5b19d1b200" + revision = "ff3583edef7de132f219f0efc00e097cabcc0ec0" [[projects]] name = "google.golang.org/grpc" @@ -202,9 +140,11 @@ "credentials", "encoding", "encoding/proto", - "grpclb/grpc_lb_v1/messages", "grpclog", "internal", + "internal/backoff", + "internal/channelz", + "internal/grpcrand", "keepalive", "metadata", "naming", @@ -217,8 +157,8 @@ "tap", "transport" ] - revision = "d11072e7ca9811b1100b80ca0269ac831f06d024" - version = "v1.11.3" + revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8" + version = "v1.13.0" [[projects]] name = "gopkg.in/yaml.v2" @@ -229,6 +169,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "47e9af15a871ee9c6070d863095662aa2f0027dcbcb763b52ce4c129eed80f7e" + inputs-digest = "b8adb82bacc7a8ad53775ae3c3d249e5446f2dddfc112c33d3531889164f4356" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 79f23e0..40d5800 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -27,7 +27,15 @@ [[constraint]] branch = "master" + name = "github.com/mitchellh/mapstructure" + +[[constraint]] + branch = "master" + name = "github.com/vapor-ware/goipmi" + +[[constraint]] name = "github.com/vapor-ware/synse-sdk" + version = "1.0.1" [prune] go-tests = true diff --git a/Makefile b/Makefile index b10ab4d..b791fee 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # PLUGIN_NAME := ipmi -PLUGIN_VERSION := 0.1.1-alpha +PLUGIN_VERSION := 0.2.0-alpha IMAGE_NAME := vaporio/ipmi-plugin GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2> /dev/null || true) @@ -11,13 +11,13 @@ GIT_TAG ?= $(shell git describe --tags 2> /dev/null || true) BUILD_DATE := $(shell date -u +%Y-%m-%dT%T 2> /dev/null) GO_VERSION := $(shell go version | awk '{ print $$3 }') -PKG_CTX := main +PKG_CTX := github.com/vapor-ware/synse-ipmi-plugin/vendor/github.com/vapor-ware/synse-sdk/sdk LDFLAGS := -w \ -X ${PKG_CTX}.BuildDate=${BUILD_DATE} \ -X ${PKG_CTX}.GitCommit=${GIT_COMMIT} \ -X ${PKG_CTX}.GitTag=${GIT_TAG} \ -X ${PKG_CTX}.GoVersion=${GO_VERSION} \ - -X ${PKG_CTX}.VersionString=${PLUGIN_VERSION} + -X ${PKG_CTX}.PluginVersion=${PLUGIN_VERSION} HAS_LINT := $(shell which gometalinter) @@ -78,7 +78,7 @@ endif --disable=gotype --disable=gocyclo \ --tests \ --vendor \ - --sort=severity \ + --sort=path --sort=line \ --aggregate \ --deadline=5m diff --git a/README.md b/README.md index 669d565..dd82be3 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,58 @@ +[![CircleCI](https://circleci.com/gh/vapor-ware/synse-ipmi-plugin.svg?style=shield)](https://circleci.com/gh/vapor-ware/synse-ipmi-plugin) + # Synse IPMI Plugin A general-purpose IPMI plugin for [Synse Server][synse-server]. -> **NOTE**: This plugin is still in development and is considered to be -> in an alpha state. While the plugin should be functional, it requires -> updates in the underlying SDK to function better. See the [Caveats](#caveats) -> section, below. +## Plugin Support +### Outputs +Outputs should be referenced by name. A single device can have more than one instance +of an output type. A value of `-` in the table below indicates that there is no value +set for that field. + +| Name | Description | Unit | Precision | Scaling Factor | +| ---- | ----------- | ---- | --------- | -------------- | +| chassis.power.state | The power state (on/off) of the chassis. | - | - | - | +| chassis.led.state | The chassis' LED identify state. | - | - | - | +| chassis.boot.target | The boot target set for the chassis. | - | - | - | + + +### Device Handlers +Device Handlers should be referenced by name. + +| Name | Description | Read | Write | Bulk Read | +| ---- | ----------- | ---- | ----- | --------- | +| boot_target | A handler for chassis boot target. | ✓ | ✓ | ✗ | +| chassis.led | A handler for chassis identify, commonly an LED. | ✓ | ✓ | ✗ | +| chassis.power | A handler for chassis power. | ✓ | ✓ | ✗ | + -This plugin provides read support for: -- Current power status (on/off) -- Current boot device -- Current identify state (IPMI 2.0, if supported by BMC) +### Write Values +This plugin supports the following values when writing to a device via a handler. -And write support for: -- BMC Power control *(on/off/reset/cycle)* -- BMC Boot target *(none/pxe/disk/safe/diag/cdrom/bios/floppy/rfloppy/rprimary/rcdrom/rdisk)* -- BMC Identify *(on - 15s/off)* +| Handler | Write Action | Write Data | +| ------- | ------------ | ---------- | +| boot_target | `target` | `none`, `pxe`, `disk`, `safe`, `diag`, `cdrom`, `bios`, `rfloppy`, `rprimary`, `rcdrom`, `rdisk`, `floppy` | +| chassis.led | `state` | `on`, `off` | +| chassis.power | `state` | `on`, `off`, `reset`, `cycle` | ## Getting Started ### Getting the Plugin -You can get the Synse IPMI plugin either by cloning this repo and running one of: -```console -# Build the IPMI plugin binary locally -$ make setup build +You can get the Synse IPMI plugin either by cloning this repo, setting up the project dependencies, +and building the binary or docker image: +```bash +# Setup the project +$ make setup + +# Build the binary +$ make build -# Build the IPMI plugin Docker image locally +# Build the docker image $ make docker ``` You can also use a pre-built docker image from [DockerHub][plugin-dockerhub] -```console +```bash $ docker pull vaporio/ipmi-plugin ``` @@ -37,14 +60,14 @@ Or a pre-built binary from the latest [release][plugin-release]. ### Running the Plugin If you are using the plugin binary: -```console +```bash # The name of the plugin binary may differ depending on whether it is built # locally or a pre-built binary is used. $ ./plugin ``` If you are using the Docker image: -```console +```bash $ docker run vaporio/ipmi-plugin ``` @@ -54,8 +77,7 @@ See the next section for how to configure your plugin. The [Example Deployment]( section describes how to run a functional end-to-end example, right out of the box. ## Configuring the Plugin for your deployment -Plugins have three different types of configurations - these are all described in detail -in the [SDK Configuration Documentation][sdk-config-docs]. +Plugin and device configuration are described in detail in the [SDK Configuration Documentation][sdk-config-docs]. For your own deployment, you will need to provide your own plugin config, `config.yml`. The IPMI plugin dynamically generates the device configuration records from BMC data, so @@ -64,20 +86,21 @@ there is no need to specify device instance configuration files here. ### plugin config After reading through the docs linked above for the plugin config, take a look at the [example plugin config](example/config.yml). This can be used as a reference. To specify your own BMCs, you -will need to list them under the `auto_enumerate` field, e.g. +will need to list them under the `dynamicRegistration` field, e.g. ```yaml -auto_enumerate: - - hostname: 10.1.2.3 - port: 623 - username: ADMIN - password: ADMIN - interface: lanplus - - hostname: 10.1.2.4 - port: 623 - username: ADMIN - password: ADMIN - interface: lanplus +dynamicRegistration: + config: + - hostname: 10.1.2.3 + port: 623 + username: ADMIN + password: ADMIN + interface: lanplus + - hostname: 10.1.2.4 + port: 623 + username: ADMIN + password: ADMIN + interface: lanplus ``` Once you have your own plugin config, you can either mount it into the container at `/plugin/config.yml`, @@ -127,15 +150,9 @@ and add to the list. ## Caveats As the Plugin SDK is still being actively developed and improved, there are some under-developed areas that affect this plugin. -- Dynamic `DeviceConfig` generation is a relatively new feature. As it is used, it will - become more developed and easier to use. - `Location` information needs to be manually specified for each dynamically generated device. It may not always be clear what values to use for the `rack` and `board` ids. This plugin uses `ipmi` as the rack id, and the BMC host as the `board`. -- Each of the devices (`bmc-boot-target`, `bmc-power`, `bmc-chassis-led`) are defined fairly - statically - they require their current type/model to stay the same. If you wish to supply - your own configs (e.g. to correctly identify the manufacturer and other meta-info), you - will need to keep the same type/model. ## Feedback Feedback for this plugin, or any component of the Synse ecosystem, is greatly appreciated! @@ -146,6 +163,7 @@ feedback you may have. ## Contributing We welcome contributions to the project. The project maintainers actively manage the issues and pull requests. If you choose to contribute, we ask that you either comment on an existing +issue or open a new one. The Synse IPMI Plugin, and all other components of the Synse ecosystem, is released under the [GPL-3.0](LICENSE) license. diff --git a/config.yml b/config.yml index ab230bb..0b80f4a 100644 --- a/config.yml +++ b/config.yml @@ -10,8 +10,7 @@ # supply their own configuration. # version: 1.0 -name: ipmi - +debug: true network: type: tcp address: ":5001" diff --git a/config/proto/boot_target.yml b/config/proto/boot_target.yml deleted file mode 100644 index 07fb206..0000000 --- a/config/proto/boot_target.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 1.0 -prototypes: - - type: boot_target - model: bmc-boot-target - manufacturer: Vapor IO - protocol: ipmi - output: - - type: target diff --git a/config/proto/chassis_led.yml b/config/proto/chassis_led.yml deleted file mode 100644 index 88a0d91..0000000 --- a/config/proto/chassis_led.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 1.0 -prototypes: - - type: led - model: bmc-chassis-led - manufacturer: Vapor IO - protocol: ipmi - output: - - type: state diff --git a/config/proto/power.yml b/config/proto/power.yml deleted file mode 100644 index c8518c7..0000000 --- a/config/proto/power.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 1.0 -prototypes: - - type: power - model: bmc-power - manufacturer: Vapor IO - protocol: ipmi - output: - - type: state diff --git a/deploy/docker/deploy.yml b/deploy/docker/deploy.yml index b346f53..72789c7 100644 --- a/deploy/docker/deploy.yml +++ b/deploy/docker/deploy.yml @@ -12,7 +12,7 @@ services: ports: - 5000:5000 environment: - SYNSE_PLUGIN_TCP_IPMI: ipmi-plugin:5001 + SYNSE_PLUGIN_TCP: ipmi-plugin:5001 links: - ipmi-plugin @@ -22,10 +22,8 @@ services: ports: - 5001:5001 volumes: - - ./devices:/tmp/devices - ../../example:/tmp/plugin environment: - PLUGIN_DEVICE_PATH: /tmp/devices PLUGIN_CONFIG: /tmp/plugin links: - ipmisim diff --git a/enumerate/enumerate.go b/enumerate/enumerate.go deleted file mode 100644 index 5bc017d..0000000 --- a/enumerate/enumerate.go +++ /dev/null @@ -1,114 +0,0 @@ -package enumerate - -import ( - "strconv" - - "github.com/mitchellh/mapstructure" - "github.com/vapor-ware/goipmi" - "github.com/vapor-ware/synse-sdk/sdk/config" - "github.com/vapor-ware/synse-sdk/sdk/logger" -) - -// FIXME (etd): the SDK needs some improvements to how dynamic device -// creation happens. While this implementation works, it is liable to change -// moving forward. - -// DeviceEnumerator is the enumeration handler for the IPMI Plugin. It generates -// device instance configurations at runtime for the configured BMCs. -// -// Currently, this will not scan the SDR or otherwise search for devices exposed -// by the BMC. Instead, it will just create a few higher-level devices for chassis -// control for each configured BMC. -func DeviceEnumerator(data map[string]interface{}) ([]*config.DeviceConfig, error) { - var devices []*config.DeviceConfig - - // FIXME (etd): creating the connection and the client here are not totally - // necessary right now other than to validate that the Connection info is - // generally correct. We will need this later, e.g. when scanning the SDR - // for devices. - conn := &ipmi.Connection{} - err := mapstructure.Decode(data, conn) - if err != nil { - return nil, err - } - - logger.Debugf("Connection: %+v", conn) - - // FIXME (etd): see the FIXME above - we do not currently use this. - _, err = ipmi.NewClient(conn) - if err != nil { - return nil, err - } - - // Make new power device for the BMC. This device would be akin to - // `ipmitool [options] chassis power ...` commands. - power := config.DeviceConfig{ - Version: "1", - Type: "power", - Model: "bmc-power", - // FIXME (etd): how do we determine the location for dynamic devices? - // These values are semi-sane placeholders until we figure this out. - Location: config.Location{ - Rack: "ipmi", - Board: conn.Hostname, - }, - // We can put the connection info here.. sorta (needs to be string->interface{}, - // or interface{}->interface{} - Data: map[string]string{ - "id": "1", // FIXME (etd): is there anything unique that we can use instead of hardcoding? if not, find a better way than manually specifying ids... - "path": conn.Path, - "hostname": conn.Hostname, - "port": strconv.Itoa(conn.Port), - "username": conn.Username, - "password": conn.Password, - "interface": conn.Interface, - }, - } - devices = append(devices, &power) - - // Make new boot target device for the BMC. This device would be akin to - // `ipmitool [options] chassis bootdev ...` commands. - bootTarget := config.DeviceConfig{ - Version: "1", - Type: "boot_target", - Model: "bmc-boot-target", - Location: config.Location{ - Rack: "ipmi", - Board: conn.Hostname, - }, - Data: map[string]string{ - "id": "2", // FIXME (etd): see above - "path": conn.Path, - "hostname": conn.Hostname, - "port": strconv.Itoa(conn.Port), - "username": conn.Username, - "password": conn.Password, - "interface": conn.Interface, - }, - } - devices = append(devices, &bootTarget) - - // Make new identify device for the BMC. This device would be akin to - // `ipmitool [options] chassis identify ...` commands. - identifyLed := config.DeviceConfig{ - Version: "1", - Type: "led", - Model: "bmc-chassis-led", - Location: config.Location{ - Rack: "ipmi", - Board: conn.Hostname, - }, - Data: map[string]string{ - "id": "3", // FIXME (etd): see above - "path": conn.Path, - "hostname": conn.Hostname, - "port": strconv.Itoa(conn.Port), - "username": conn.Username, - "password": conn.Password, - "interface": conn.Interface, - }, - } - devices = append(devices, &identifyLed) - - return devices, nil -} diff --git a/example/config.yml b/example/config.yml index 6a65fd1..f61ed7c 100644 --- a/example/config.yml +++ b/example/config.yml @@ -9,7 +9,6 @@ # deployments. # version: 1.0 -name: ipmi debug: true network: type: tcp @@ -20,9 +19,10 @@ settings: interval: 3s write: interval: 1s -auto_enumerate: - - hostname: ipmisim - port: 623 - username: ADMIN - password: ADMIN - interface: lanplus +dynamicRegistration: + config: + - hostname: ipmisim + port: 623 + username: ADMIN + password: ADMIN + interface: lanplus diff --git a/main.go b/main.go new file mode 100644 index 0000000..1e8eb5f --- /dev/null +++ b/main.go @@ -0,0 +1,32 @@ +package main + +import ( + log "github.com/Sirupsen/logrus" + + "github.com/vapor-ware/synse-ipmi-plugin/pkg" + "github.com/vapor-ware/synse-sdk/sdk" +) + +const ( + pluginName = "ipmi" + pluginMaintainer = "vaporio" + pluginDesc = "A general-purpose IPMI plugin" + pluginVcs = "https://github.com/vapor-ware/synse-ipmi-plugin" +) + +func main() { + // Set the plugin metadata + sdk.SetPluginMeta( + pluginName, + pluginMaintainer, + pluginDesc, + pluginVcs, + ) + + plugin := pkg.MakePlugin() + + // Run the plugin + if err := plugin.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/devices/boot_target.go b/pkg/devices/boot_target.go similarity index 66% rename from devices/boot_target.go rename to pkg/devices/boot_target.go index 19af8dc..b0de882 100644 --- a/devices/boot_target.go +++ b/pkg/devices/boot_target.go @@ -4,17 +4,16 @@ import ( "fmt" "strings" + log "github.com/Sirupsen/logrus" + "github.com/vapor-ware/goipmi" - "github.com/vapor-ware/synse-ipmi-plugin/protocol" + "github.com/vapor-ware/synse-ipmi-plugin/pkg/protocol" "github.com/vapor-ware/synse-sdk/sdk" - "github.com/vapor-ware/synse-sdk/sdk/logger" ) -// BmcBootTarget is the handler for the bmc-boot-target device. -var BmcBootTarget = sdk.DeviceHandler{ - Type: "boot_target", - Model: "bmc-boot-target", - +// ChassisBootTarget is the handler for the bmc-boot-target device. +var ChassisBootTarget = sdk.DeviceHandler{ + Name: "boot_target", Read: bmcBootTargetRead, Write: bmcBootTargetWrite, } @@ -26,16 +25,20 @@ func bmcBootTargetRead(device *sdk.Device) ([]*sdk.Reading, error) { return nil, err } - ret := []*sdk.Reading{ - sdk.NewReading("target", target), + bootTarget, err := device.GetOutput("chassis.boot.target").MakeReading(target) + if err != nil { + return nil, err } - return ret, nil + + return []*sdk.Reading{ + bootTarget, + }, nil } // bmcBootTargetWrite is the write handler function for bmc-boot-target devices. func bmcBootTargetWrite(device *sdk.Device, data *sdk.WriteData) error { action := data.Action - raw := data.Raw + raw := data.Data // When writing to a BMC boot target device, we always expect there to be // raw data specified. If there isn't, we return an error. @@ -44,46 +47,46 @@ func bmcBootTargetWrite(device *sdk.Device, data *sdk.WriteData) error { } if action == "target" { - cmd := string(raw[0]) + cmd := string(raw) var target ipmi.BootDevice switch strings.ToLower(cmd) { case "none": - logger.Info("Setting Boot Target -> NONE") + log.Info("Setting Boot Target -> NONE") target = ipmi.BootDeviceNone case "pxe": - logger.Info("Setting Boot Target -> PXE") + log.Info("Setting Boot Target -> PXE") target = ipmi.BootDevicePxe case "disk": - logger.Info("Setting Boot Target -> DISK") + log.Info("Setting Boot Target -> DISK") target = ipmi.BootDeviceDisk case "safe": - logger.Info("Setting Boot Target -> SAFE") + log.Info("Setting Boot Target -> SAFE") target = ipmi.BootDeviceSafe case "diag": - logger.Info("Setting Boot Target -> DIAG") + log.Info("Setting Boot Target -> DIAG") target = ipmi.BootDeviceDiag case "cdrom": - logger.Info("Setting Boot Target -> CDROM") + log.Info("Setting Boot Target -> CDROM") target = ipmi.BootDeviceCdrom case "bios": - logger.Info("Setting Boot Target -> BIOS") + log.Info("Setting Boot Target -> BIOS") target = ipmi.BootDeviceBios case "rfloppy": - logger.Info("Setting Boot Target -> RFLOPPY") + log.Info("Setting Boot Target -> RFLOPPY") target = ipmi.BootDeviceRemoteFloppy case "rprimary": - logger.Info("Setting Boot Target -> RPRIMARY") + log.Info("Setting Boot Target -> RPRIMARY") target = ipmi.BootDeviceRemotePrimary case "rcdrom": - logger.Info("Setting Boot Target -> RCDROM") + log.Info("Setting Boot Target -> RCDROM") target = ipmi.BootDeviceRemoteCdrom case "rdisk": - logger.Info("Setting Boot Target -> RDISK") + log.Info("Setting Boot Target -> RDISK") target = ipmi.BootDeviceRemoteDisk case "floppy": - logger.Info("Setting Boot Target -> FLOPPY") + log.Info("Setting Boot Target -> FLOPPY") target = ipmi.BootDeviceFloppy default: return fmt.Errorf("unsupported command for bmc boot target 'target' action: %s", cmd) diff --git a/devices/chassis_led.go b/pkg/devices/chassis_led.go similarity index 84% rename from devices/chassis_led.go rename to pkg/devices/chassis_led.go index 5323ac0..b567899 100644 --- a/devices/chassis_led.go +++ b/pkg/devices/chassis_led.go @@ -4,11 +4,11 @@ import ( "fmt" "strings" - "github.com/vapor-ware/synse-ipmi-plugin/protocol" + "github.com/vapor-ware/synse-ipmi-plugin/pkg/protocol" "github.com/vapor-ware/synse-sdk/sdk" ) -// BmcChassisLed is the handler for the bmc-boot-target device. +// ChassisLed is the handler for the bmc-boot-target device. // // This is really chassis identify, which according to the IPMI spec: // @@ -18,10 +18,8 @@ import ( // // This was considered LED in Synse 1.4 so we will continue to consider it // an LED device, even though it may not be. -var BmcChassisLed = sdk.DeviceHandler{ - Type: "led", - Model: "bmc-chassis-led", - +var ChassisLed = sdk.DeviceHandler{ + Name: "chassis.led", Read: bmcChassisLedRead, Write: bmcChassisLedWrite, } @@ -33,16 +31,20 @@ func bmcChassisLedRead(device *sdk.Device) ([]*sdk.Reading, error) { return nil, err } - ret := []*sdk.Reading{ - sdk.NewReading("state", state), + chassisIdentify, err := device.GetOutput("chassis.led.state").MakeReading(state) + if err != nil { + return nil, err } - return ret, nil + + return []*sdk.Reading{ + chassisIdentify, + }, nil } // bmcChassisLedWrite is the write handler function for bmc-chassis-led devices. func bmcChassisLedWrite(device *sdk.Device, data *sdk.WriteData) error { action := data.Action - raw := data.Raw + raw := data.Data // When writing to a BMC LED (identify) device, we always expect there to be // raw data specified. If there isn't, we return an error. @@ -51,7 +53,7 @@ func bmcChassisLedWrite(device *sdk.Device, data *sdk.WriteData) error { } if action == "state" { - cmd := string(raw[0]) + cmd := string(raw) var state protocol.IdentifyState // TODO (etd): figure out if we want to support intervals. if so, how? could be diff --git a/devices/power.go b/pkg/devices/power.go similarity index 79% rename from devices/power.go rename to pkg/devices/power.go index ce5d1e3..d69de10 100644 --- a/devices/power.go +++ b/pkg/devices/power.go @@ -5,15 +5,13 @@ import ( "strings" "github.com/vapor-ware/goipmi" - "github.com/vapor-ware/synse-ipmi-plugin/protocol" + "github.com/vapor-ware/synse-ipmi-plugin/pkg/protocol" "github.com/vapor-ware/synse-sdk/sdk" ) -// BmcPower is the handler for the bmc-power device. -var BmcPower = sdk.DeviceHandler{ - Type: "power", - Model: "bmc-power", - +// ChassisPower is the handler for the bmc-power device. +var ChassisPower = sdk.DeviceHandler{ + Name: "chassis.power", Read: bmcPowerRead, Write: bmcPowerWrite, } @@ -25,16 +23,20 @@ func bmcPowerRead(device *sdk.Device) ([]*sdk.Reading, error) { return nil, err } - readings := []*sdk.Reading{ - sdk.NewReading("state", state), + powerState, err := device.GetOutput("chassis.power.state").MakeReading(state) + if err != nil { + return nil, err } - return readings, nil + + return []*sdk.Reading{ + powerState, + }, nil } // bmcPowerWrite is the write handler function for bmc-power devices. func bmcPowerWrite(device *sdk.Device, data *sdk.WriteData) error { action := data.Action - raw := data.Raw + raw := data.Data // When writing to a BMC Power device, we always expect there to be // raw data specified. If there isn't, we return an error. @@ -43,7 +45,7 @@ func bmcPowerWrite(device *sdk.Device, data *sdk.WriteData) error { } if action == "state" { - cmd := string(raw[0]) + cmd := string(raw) var state ipmi.ChassisControl switch strings.ToLower(cmd) { diff --git a/pkg/options.go b/pkg/options.go new file mode 100644 index 0000000..414459b --- /dev/null +++ b/pkg/options.go @@ -0,0 +1,128 @@ +package pkg + +import ( + "fmt" + + log "github.com/Sirupsen/logrus" + "github.com/mitchellh/mapstructure" + "github.com/vapor-ware/goipmi" + "github.com/vapor-ware/synse-sdk/sdk" +) + +// deviceIdentifier defines the IPMI-specific way of uniquely identifying a device +// through its device configuration. +// +// Since the currently supported devices do not have a unique identifier beyond the +// BMC (they are really just interfaces for the BMC chassis), we supply our own unique +// ID in the "id" field. This is liable to change in the future. +func deviceIdentifier(data map[string]interface{}) string { + return fmt.Sprint(data["id"]) +} + +// dynamicDeviceConfig is the custom override option to enable the IPMI plugin to +// dynamically register device configs at runtime for the configured BMCs. +// +// Currently, this will not scan the SDR or otherwise search for devices exposed +// by the BMC. Instead, it will just create a few higher-level devices for chassis +// control for each configured BMC. +func dynamicDeviceConfig(data map[string]interface{}) ([]*sdk.DeviceConfig, error) { + + // FIXME (etd): creating the connection and the client here are not totally + // necessary right now other than to validate that the Connection info is + // generally correct. We will need this later, e.g. when scanning the SDR + // for devices. + conn := &ipmi.Connection{} + err := mapstructure.Decode(data, conn) + if err != nil { + return nil, err + } + + log.Debugf("Connection: %+v", conn) + + // FIXME (etd): see the FIXME above - we do not currently use this. + _, err = ipmi.NewClient(conn) + if err != nil { + return nil, err + } + + // Make new power device for the BMC. This device would be akin to + // `ipmitool [options] chassis power ...` commands. + cfg := sdk.DeviceConfig{ + SchemeVersion: sdk.SchemeVersion{Version: "1.0"}, + Locations: []*sdk.LocationConfig{ + { + Name: "ipmi", + Rack: &sdk.LocationData{Name: "ipmi"}, + Board: &sdk.LocationData{Name: conn.Hostname}, + }, + }, + Devices: []*sdk.DeviceKind{ + { + Name: "chassis.power", + Outputs: []*sdk.DeviceOutput{ + {Type: "chassis.power.state"}, + }, + Instances: []*sdk.DeviceInstance{ + { + Info: "BMC chassis power", + Location: "ipmi", + Data: map[string]interface{}{ + // FIXME (etd): is there anything unique that we can use instead of hardcoding? + // if not, find a better way than manually specifying ids... + "path": conn.Path, + "hostname": conn.Hostname, + "port": conn.Port, + "username": conn.Username, + "password": conn.Password, + "interface": conn.Interface, + }, + }, + }, + }, + { + Name: "boot_target", + Outputs: []*sdk.DeviceOutput{ + {Type: "chassis.boot.target"}, + }, + Instances: []*sdk.DeviceInstance{ + { + Info: "BMC chassis boot target", + Location: "ipmi", + Data: map[string]interface{}{ + "id": "2", // FIXME (etd): see above + "path": conn.Path, + "hostname": conn.Hostname, + "port": conn.Port, + "username": conn.Username, + "password": conn.Password, + "interface": conn.Interface, + }, + }, + }, + }, + { + Name: "chassis.led", + Outputs: []*sdk.DeviceOutput{ + {Type: "chassis.led.state"}, + }, + Instances: []*sdk.DeviceInstance{ + { + Info: "BMC chassis identify LED", + Location: "ipmi", + Data: map[string]interface{}{ + "id": "3", // FIXME (etd): see above + "path": conn.Path, + "hostname": conn.Hostname, + "port": conn.Port, + "username": conn.Username, + "password": conn.Password, + "interface": conn.Interface, + }, + }, + }, + }, + }, + } + + return []*sdk.DeviceConfig{&cfg}, nil +} diff --git a/pkg/outputs/outputs.go b/pkg/outputs/outputs.go new file mode 100644 index 0000000..ae98d9f --- /dev/null +++ b/pkg/outputs/outputs.go @@ -0,0 +1,20 @@ +package outputs + +import "github.com/vapor-ware/synse-sdk/sdk" + +var ( + // ChassisPowerState is the output type for chassis power (on/off). + ChassisPowerState = sdk.OutputType{ + Name: "chassis.power.state", + } + + // ChassisLedState is the output type for chassis identify LED state (on/off). + ChassisLedState = sdk.OutputType{ + Name: "chassis.led.state", + } + + // ChassisBootTarget is the output type for chassis boot target settings. + ChassisBootTarget = sdk.OutputType{ + Name: "chassis.boot.target", + } +) diff --git a/pkg/plugin.go b/pkg/plugin.go new file mode 100644 index 0000000..51e8d2d --- /dev/null +++ b/pkg/plugin.go @@ -0,0 +1,37 @@ +package pkg + +import ( + log "github.com/Sirupsen/logrus" + "github.com/vapor-ware/synse-ipmi-plugin/pkg/devices" + "github.com/vapor-ware/synse-ipmi-plugin/pkg/outputs" + "github.com/vapor-ware/synse-sdk/sdk" + "github.com/vapor-ware/synse-sdk/sdk/policies" +) + +// MakePlugin creates a new instance of the IPMI plugin. +func MakePlugin() *sdk.Plugin { + plugin := sdk.NewPlugin( + sdk.CustomDeviceIdentifier(deviceIdentifier), + sdk.CustomDynamicDeviceConfigRegistration(dynamicDeviceConfig), + ) + + policies.Add(policies.DeviceConfigDynamicRequired) + policies.Add(policies.DeviceConfigFileOptional) + + err := plugin.RegisterOutputTypes( + &outputs.ChassisBootTarget, + &outputs.ChassisLedState, + &outputs.ChassisPowerState, + ) + if err != nil { + log.Fatal(err) + } + + plugin.RegisterDeviceHandlers( + &devices.ChassisBootTarget, + &devices.ChassisLed, + &devices.ChassisPower, + ) + + return plugin +} diff --git a/protocol/boot_target.go b/pkg/protocol/boot_target.go similarity index 79% rename from protocol/boot_target.go rename to pkg/protocol/boot_target.go index d15199f..f81e7ad 100644 --- a/protocol/boot_target.go +++ b/pkg/protocol/boot_target.go @@ -1,12 +1,13 @@ package protocol import ( + log "github.com/Sirupsen/logrus" + "github.com/vapor-ware/goipmi" - "github.com/vapor-ware/synse-sdk/sdk/logger" ) // GetChassisBootTarget gets the current boot device for the chassis. -func GetChassisBootTarget(config map[string]string) (string, error) { +func GetChassisBootTarget(config map[string]interface{}) (string, error) { client, err := newClientFromConfig(config) if err != nil { return "", err @@ -34,11 +35,11 @@ func GetChassisBootTarget(config map[string]string) (string, error) { } // SetChassisBootTarget sets the boot device for the chassis. -func SetChassisBootTarget(config map[string]string, target ipmi.BootDevice) error { +func SetChassisBootTarget(config map[string]interface{}, target ipmi.BootDevice) error { client, err := newClientFromConfig(config) if err != nil { return err } - logger.Debugf("Setting boot target to: %s", target.String()) + log.Debugf("Setting boot target to: %s", target.String()) return client.SetBootDevice(target) } diff --git a/protocol/identify.go b/pkg/protocol/identify.go similarity index 93% rename from protocol/identify.go rename to pkg/protocol/identify.go index ce1c415..fe511b4 100644 --- a/protocol/identify.go +++ b/pkg/protocol/identify.go @@ -3,8 +3,9 @@ package protocol import ( "fmt" + log "github.com/Sirupsen/logrus" + "github.com/vapor-ware/goipmi" - "github.com/vapor-ware/synse-sdk/sdk/logger" ) // FIXME - now that we have a fork of goipmi, some of this stuff should @@ -83,7 +84,7 @@ func Identify(c *ipmi.Client, time int) error { } // GetChassisIdentify gets the current identify state from the chassis. -func GetChassisIdentify(config map[string]string) (string, error) { +func GetChassisIdentify(config map[string]interface{}) (string, error) { client, err := newClientFromConfig(config) if err != nil { return "", err @@ -125,7 +126,7 @@ func GetChassisIdentify(config map[string]string) (string, error) { } // SetChassisIdentify sets the identify state of the chassis -func SetChassisIdentify(config map[string]string, state IdentifyState) error { +func SetChassisIdentify(config map[string]interface{}, state IdentifyState) error { client, err := newClientFromConfig(config) if err != nil { return err @@ -141,7 +142,7 @@ func SetChassisIdentify(config map[string]string, state IdentifyState) error { return fmt.Errorf("identify state unsupported for setting: %v", state) } - logger.Debugf("Setting chassis to identify for: %d seconds", time) + log.Debugf("Setting chassis to identify for: %d seconds", time) return Identify( client, time, diff --git a/protocol/power.go b/pkg/protocol/power.go similarity index 80% rename from protocol/power.go rename to pkg/protocol/power.go index ec2d537..79ec444 100644 --- a/protocol/power.go +++ b/pkg/protocol/power.go @@ -2,7 +2,8 @@ package protocol import ( "github.com/vapor-ware/goipmi" - "github.com/vapor-ware/synse-sdk/sdk/logger" + + log "github.com/Sirupsen/logrus" ) const ( @@ -10,7 +11,7 @@ const ( ) // GetChassisPowerState gets the current state (on/off) of the chassis. -func GetChassisPowerState(config map[string]string) (string, error) { +func GetChassisPowerState(config map[string]interface{}) (string, error) { client, err := newClientFromConfig(config) if err != nil { return "", err @@ -44,12 +45,12 @@ func GetChassisPowerState(config map[string]string) (string, error) { } // SetChassisPowerState sets the state of the chassis. -func SetChassisPowerState(config map[string]string, control ipmi.ChassisControl) error { +func SetChassisPowerState(config map[string]interface{}, control ipmi.ChassisControl) error { client, err := newClientFromConfig(config) if err != nil { return err } - logger.Debugf("Setting power state to: %s", control.String()) + log.Debugf("Setting power state to: %s", control.String()) return client.Control(control) } diff --git a/protocol/utils.go b/pkg/protocol/utils.go similarity index 52% rename from protocol/utils.go rename to pkg/protocol/utils.go index 20b3f22..3c65e06 100644 --- a/protocol/utils.go +++ b/pkg/protocol/utils.go @@ -1,15 +1,13 @@ package protocol import ( - "strconv" - "github.com/mitchellh/mapstructure" "github.com/vapor-ware/goipmi" ) // newClientFromConfig is a utility function to create a new IPMI client // using the configuration specified in a Device's Data field. -func newClientFromConfig(config map[string]string) (*ipmi.Client, error) { +func newClientFromConfig(config map[string]interface{}) (*ipmi.Client, error) { conn, err := makeConnection(config) if err != nil { return nil, err @@ -19,25 +17,11 @@ func newClientFromConfig(config map[string]string) (*ipmi.Client, error) { // makeConnection is a utility function to initialize the ipmi.Connection // used for an ipmi.Client by parsing the Device's Data map. -func makeConnection(data map[string]string) (*ipmi.Connection, error) { - // FIXME (etd): need to do some type casting because the device - // data is a map[string]string, but we need some values as ints - var tmp = make(map[string]interface{}) - for k, v := range data { - tmp[k] = v - } - - port, err := strconv.Atoi(data["port"]) - if err != nil { - return nil, err - } - tmp["port"] = port - +func makeConnection(data map[string]interface{}) (*ipmi.Connection, error) { conn := &ipmi.Connection{} - err = mapstructure.Decode(tmp, conn) + err := mapstructure.Decode(data, conn) if err != nil { return nil, err } - return conn, nil } diff --git a/plugin.go b/plugin.go deleted file mode 100644 index 0898c48..0000000 --- a/plugin.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "log" - - "github.com/vapor-ware/synse-ipmi-plugin/devices" - "github.com/vapor-ware/synse-ipmi-plugin/enumerate" - "github.com/vapor-ware/synse-sdk/sdk" -) - -// Build time variables for setting the version info of a Plugin. -var ( - BuildDate string - GitCommit string - GitTag string - GoVersion string - VersionString string -) - -// DeviceIdentifier defines the IPMI-specific way of uniquely identifying a device -// through its device configuration. -// -// Since the currently supported devices do not have a unique identifier beyond the -// BMC (they are really just interfaces for the BMC chassis), we supply our own unique -// ID in the "id" field. This is liable to change in the future. -func DeviceIdentifier(data map[string]string) string { - return data["id"] -} - -func main() { - - // Create the handlers for the IPMI plugin. The DeviceEnumerator will - // create the device instance configurations at runtime. - handlers, err := sdk.NewHandlers(DeviceIdentifier, enumerate.DeviceEnumerator) - if err != nil { - log.Fatal(err) - } - - plugin, err := sdk.NewPlugin(handlers, nil) - if err != nil { - log.Fatal(err) - } - - plugin.RegisterDeviceHandlers( - // BMC Chassis Power, e.g. ipmitool [options] chassis power ... - &devices.BmcPower, - - // BMC Chassis Boot Target, e.g. ipmitool [options] chassis bootdev ... - &devices.BmcBootTarget, - - // BMC Chassis LED (identify), e.g. ipmitool [options] chassis identify ... - &devices.BmcChassisLed, - ) - - // Set build-time version info. - plugin.SetVersion(sdk.VersionInfo{ - BuildDate: BuildDate, - GitCommit: GitCommit, - GitTag: GitTag, - GoVersion: GoVersion, - VersionString: VersionString, - }) - - // Run the plugin. - err = plugin.Run() - if err != nil { - log.Fatal(err) - } -}