Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of APCUPSD Addon #372

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
59 changes: 59 additions & 0 deletions addons/apcupsd/addon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package apcupsd

import (
"fmt"

"github.com/rocket-pool/smartnode/shared/types/addons"
cfgtypes "github.com/rocket-pool/smartnode/shared/types/config"
)

const (
ContainerID_Apcupsd cfgtypes.ContainerID = "apcupsd"
ContainerID_ApcupsdExporter cfgtypes.ContainerID = "apcupsd_exporter"
ApcupsdContainerName string = "addon_apcupsd"
ApcupsdNetworkComposeTemplateName string = "addon_apcupsd.network"
ApcupsdContainerComposeTemplateName string = "addon_apcupsd.container"
ApcupsdConfigTemplateName string = "addon_apcupsd_config"
ApcupsdConfigName string = "addon_apcupsd.conf"
)

type Apcupsd struct {
cfg *ApcupsdConfig `yaml:"config,omitempty"`
}

func NewApcupsd() addons.SmartnodeAddon {
return &Apcupsd{
cfg: NewConfig(),
}
}

func (apcupsd *Apcupsd) GetName() string {
return "APCUPS Monitor"
}

func (apcupsd *Apcupsd) GetDescription() string {
return "This addon adds UPS monitoring to your node so you can monitor the status of your APCUPSD compatible UPS within grafana \n\nMade with love by killjoy.eth."
}

func (apcupsd *Apcupsd) GetConfig() cfgtypes.Config {
return apcupsd.cfg
}

func (apcupsd *Apcupsd) GetContainerName() string {
return fmt.Sprint(ContainerID_Apcupsd)
}

func (apcupsd *Apcupsd) GetEnabledParameter() *cfgtypes.Parameter {
return &apcupsd.cfg.Enabled
}

func (apcupsd *Apcupsd) GetContainerTag() string {
return containerTag
}

func (apcupsd *Apcupsd) UpdateEnvVars(envVars map[string]string) error {
if apcupsd.cfg.Enabled.Value == true {
cfgtypes.AddParametersToEnvVars(apcupsd.cfg.GetParameters(), envVars)
}
return nil
}
136 changes: 136 additions & 0 deletions addons/apcupsd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package apcupsd

import (
"github.com/rocket-pool/smartnode/shared/types/config"
)

// Constants
const (
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docker images are provided by third parties.
Please let me know if in your estimation there is a security risk here and if these images need to be audited.

containerTag string = "gersilex/apcupsd:v1.0.0"
exporterContainerTag string = "jangrewe/apcupsd-exporter:latest"
)

type Mode string

const (
Mode_Network Mode = "network"
Mode_Container Mode = "docker"
)

// Configuration for the Graffiti Wall Writer
type ApcupsdConfig struct {
Title string `yaml:"-"`

Enabled config.Parameter `yaml:"enabled,omitempty"`
ApcupsdContainerTag config.Parameter `yaml:"apcupsdContainerTag,omitempty"`
ApcupsdExporterContainerTag config.Parameter `yaml:"apcupsdExporterContainerTag,omitempty"`
MetricsPort config.Parameter `yaml:"metricsPort,omitempty"`
MountPoint config.Parameter `yaml:"mountPoint,omitempty"`
Mode config.Parameter `yaml:"mode,omitempty"`
NetworkAddress config.Parameter `yaml:"NetworkAddress,omitempty"`
}

// Creates a new configuration instance
func NewConfig() *ApcupsdConfig {
return &ApcupsdConfig{
Title: "APCUPSD Settings",

Enabled: config.Parameter{
ID: "enabled",
Name: "Enabled",
Description: "Enable APCUPSD monitoring",
Type: config.ParameterType_Bool,
Default: map[config.Network]interface{}{config.Network_All: false},
AffectsContainers: []config.ContainerID{ContainerID_Apcupsd, ContainerID_ApcupsdExporter},
EnvironmentVariables: []string{"ADDON_APCUPSD_ENABLED"},
CanBeBlank: false,
OverwriteOnUpgrade: false,
},
Mode: config.Parameter{
ID: "mode",
Name: "Mode",
Description: "How would you like to run APCUPSD?\n Select `Container` if you'd like smart node to run apcupsd inside a container for you.\nSelect `network` mode if you want to connect to an instance of apcupsd running on your host machine or on your network.",
Type: config.ParameterType_Choice,
Default: map[config.Network]interface{}{config.Network_All: Mode_Container},
AffectsContainers: []config.ContainerID{ContainerID_Apcupsd, ContainerID_ApcupsdExporter},
EnvironmentVariables: []string{},
CanBeBlank: false,
OverwriteOnUpgrade: false,
Options: []config.ParameterOption{{
Name: "Container",
Description: "Let the smart node run APCUPSD inside a container for you",
Value: Mode_Container,
}, {
Name: "Network",
Description: "Connect the APCUPSD exporter to an instance of APCUSD running on your host machine or on your network",
Value: Mode_Network,
}},
},
ApcupsdContainerTag: config.Parameter{
ID: "containerTag",
Name: "APCUPSD Container Tag",
Description: "The container tag name of the APCUPSD container.",
Type: config.ParameterType_String,
Default: map[config.Network]interface{}{config.Network_All: containerTag},
AffectsContainers: []config.ContainerID{ContainerID_Apcupsd},
EnvironmentVariables: []string{"ADDON_APCUPSD_CONTAINER_TAG"},
CanBeBlank: false,
OverwriteOnUpgrade: true,
},
ApcupsdExporterContainerTag: config.Parameter{
ID: "exporterContainerTag",
Name: "APCUPSD Exporter Container Tag",
Description: "The container tag name of the APCUPSD Prometheus Exporter.",
Type: config.ParameterType_String,
Default: map[config.Network]interface{}{config.Network_All: exporterContainerTag},
AffectsContainers: []config.ContainerID{ContainerID_ApcupsdExporter},
EnvironmentVariables: []string{"ADDON_APCUPSD_EXPORTER_CONTAINER_TAG"},
CanBeBlank: false,
OverwriteOnUpgrade: true,
},
MetricsPort: config.Parameter{
ID: "metricsPort",
Name: "APCUPSD Exporter Metrics Port",
Description: "The port the exporter should use to provide metrics to prometheus.",
Type: config.ParameterType_String,
Default: map[config.Network]interface{}{config.Network_All: "9162"},
AffectsContainers: []config.ContainerID{ContainerID_ApcupsdExporter, config.ContainerID_Prometheus},
EnvironmentVariables: []string{"ADDON_APCUPSD_METRICS_PORT"},
CanBeBlank: false,
OverwriteOnUpgrade: false,
},
MountPoint: config.Parameter{
ID: "mountPoint",
Name: "APC USB Mount Location",
Description: "The USB mount point for your APC device. This must be set correctly for the container to read data from your UPC. To determine the mount point on your system:\n1. Unplug the USB cable of your UPS and plug it back in.\n2. When your server detects the device an entry will show up when you run `sudo dmesg | grep usb`.\n3. Identify the mount point for your UPS. Often it is named `hiddev*` e.g. `hiddev0`,`hiddev1`... but may vary depending on how many peripherals you have connected.\n4. Verify the mount point for your distribution. Often this maps to `/dev/usb/hiddev*`\nThis is the value to enter in the field below. NOTE: If you reconnect your UPC this value may need to be updated.",
Type: config.ParameterType_String,
Default: map[config.Network]interface{}{config.Network_All: "/dev/usb/hiddev0"},
AffectsContainers: []config.ContainerID{ContainerID_Apcupsd},
EnvironmentVariables: []string{"ADDON_APCUPSD_MOUNT_POINT"},
CanBeBlank: false,
OverwriteOnUpgrade: false,
},
NetworkAddress: config.Parameter{
ID: "networkAddress",
Name: "APCUPSD Network Address",
Description: "The network address and port that should be used to connect to APCUPSD.\nIf you have apcupsd installed on your host you should use the default host.docker.internal:3551.",
Type: config.ParameterType_String,
Default: map[config.Network]interface{}{config.Network_All: "host.docker.internal:3551"},
AffectsContainers: []config.ContainerID{ContainerID_ApcupsdExporter},
EnvironmentVariables: []string{"ADDON_APCUPSD_NETWORK_ADDRESS"},
CanBeBlank: false,
OverwriteOnUpgrade: false,
},
}
}

// Get the parameters for this config
func (cfg *ApcupsdConfig) GetParameters() []*config.Parameter {
return []*config.Parameter{&cfg.Enabled, &cfg.Mode, &cfg.ApcupsdExporterContainerTag, &cfg.ApcupsdContainerTag, &cfg.MetricsPort, &cfg.MountPoint, &cfg.NetworkAddress}

}

// The the title for the config
func (cfg *ApcupsdConfig) GetConfigTitle() string {
return cfg.Title
}
5 changes: 5 additions & 0 deletions addons/constructors.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package addons

import (
"github.com/rocket-pool/smartnode/addons/apcupsd"
"github.com/rocket-pool/smartnode/addons/graffiti_wall_writer"
"github.com/rocket-pool/smartnode/shared/types/addons"
)

func NewGraffitiWallWriter() addons.SmartnodeAddon {
return graffiti_wall_writer.NewGraffitiWallWriter()
}

func NewApcupsd() addons.SmartnodeAddon {
return apcupsd.NewApcupsd()
}
174 changes: 174 additions & 0 deletions rocketpool-cli/service/config/addon-apcupsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package config

import (
"fmt"

"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/rocket-pool/smartnode/shared/services/config"
"github.com/rocket-pool/smartnode/shared/types/addons"
)

// The page wrapper for the APCUPSD addon config
type AddonApcupsdPage struct {
addonsPage *AddonsPage
page *page
layout *standardLayout
masterConfig *config.RocketPoolConfig
addon addons.SmartnodeAddon
enabledBox *parameterizedFormItem
modeBox *parameterizedFormItem
exporterImage *parameterizedFormItem
apcupsdImage *parameterizedFormItem
mountPoint *parameterizedFormItem
metricsPort *parameterizedFormItem
apcupsdAddress *parameterizedFormItem
}

// Creates a new page for the APCUPSD addon settings
func NewAddonApcupsdPage(addonsPage *AddonsPage, addon addons.SmartnodeAddon) *AddonApcupsdPage {

configPage := &AddonApcupsdPage{
addonsPage: addonsPage,
masterConfig: addonsPage.home.md.Config,
addon: addon,
}
configPage.createContent()

configPage.page = newPage(
addonsPage.page,
"settings-addon-apcupsd",
addon.GetName(),
addon.GetDescription(),
configPage.layout.grid,
)

return configPage

}

// Get the underlying page
func (configPage *AddonApcupsdPage) getPage() *page {
return configPage.page
}

// Creates the content for the APCUPSD settings page
func (configPage *AddonApcupsdPage) createContent() {

// Create the layout
configPage.layout = newStandardLayout()
configPage.layout.createForm(&configPage.masterConfig.Smartnode.Network, fmt.Sprintf("%s Settings", configPage.addon.GetName()))

// Return to the home page after pressing Escape
configPage.layout.form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEsc {
// Close all dropdowns and break if one was open
for _, param := range configPage.layout.parameters {
dropDown, ok := param.item.(*DropDown)
if ok && dropDown.open {
dropDown.CloseList(configPage.addonsPage.home.md.app)
return nil
}
}

// Return to the home page
configPage.addonsPage.home.md.setPage(configPage.addonsPage.page)
return nil
}
return event
})

// Get the parameters
enabledParam := configPage.addon.GetEnabledParameter()
// TODO: Don't like how I reference these by index here. Is there a better way?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestion for a cleaner way to fetch these parameters? It seems I can only access functions as defined by the generic addon interface.

modeParam := configPage.addon.GetConfig().GetParameters()[1]
exporterImageParam := configPage.addon.GetConfig().GetParameters()[2]
apcupsdImageParam := configPage.addon.GetConfig().GetParameters()[3]
metricsPortParam := configPage.addon.GetConfig().GetParameters()[4]
mountPointParam := configPage.addon.GetConfig().GetParameters()[5]
networkAddress := configPage.addon.GetConfig().GetParameters()[6]

// Set up the form items
configPage.enabledBox = createParameterizedCheckbox(enabledParam)
configPage.modeBox = createParameterizedDropDown(modeParam, configPage.layout.descriptionBox)
configPage.exporterImage = createParameterizedStringField(exporterImageParam)
configPage.apcupsdImage = createParameterizedStringField(apcupsdImageParam)
configPage.metricsPort = createParameterizedStringField(metricsPortParam)
configPage.mountPoint = createParameterizedStringField(mountPointParam)
configPage.apcupsdAddress = createParameterizedStringField(networkAddress)

// Map the parameters to the form items in the layout
configPage.layout.mapParameterizedFormItems(configPage.enabledBox)
configPage.layout.mapParameterizedFormItems(configPage.modeBox)
configPage.layout.mapParameterizedFormItems(configPage.exporterImage)
configPage.layout.mapParameterizedFormItems(configPage.apcupsdImage)
configPage.layout.mapParameterizedFormItems(configPage.metricsPort)
configPage.layout.mapParameterizedFormItems(configPage.mountPoint)
configPage.layout.mapParameterizedFormItems(configPage.apcupsdAddress)

// Set up the setting callbacks
configPage.enabledBox.item.(*tview.Checkbox).SetChangedFunc(func(checked bool) {
if enabledParam.Value == checked {
return
}
enabledParam.Value = checked
configPage.handleEnableChanged()
})
configPage.modeBox.item.(*DropDown).SetSelectedFunc(func(text string, index int) {
if configPage.modeBox.parameter.Value == configPage.modeBox.parameter.Options[index].Value {
return
}
configPage.modeBox.parameter.Value = configPage.modeBox.parameter.Options[index].Value
configPage.handleModeChanged()
})

// Do the initial draw
configPage.handleDraw()

}

// Handle all of the form changes when the Enabled box has changed
func (configPage *AddonApcupsdPage) handleEnableChanged() {
configPage.handleDraw()
}

// Handle all of the form changes when the Mode box has changed
func (configPage *AddonApcupsdPage) handleModeChanged() {
configPage.handleDraw()
}

func (configPage *AddonApcupsdPage) handleDraw() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review my understanding of how form items should be drawn.

configPage.layout.form.Clear(true)
configPage.layout.form.AddFormItem(configPage.enabledBox.item)

// Only add the supporting stuff if addon is enabled
if configPage.addon.GetEnabledParameter().Value == false {
return
}
configPage.addCommonFields()
if configPage.modeBox.parameter.Value == configPage.modeBox.parameter.Options[0].Value {
configPage.addContainerFields()
} else {
configPage.addNetworkFields()
}
configPage.layout.refresh()
}

func (configPage *AddonApcupsdPage) addCommonFields() {
configPage.layout.form.AddFormItem(configPage.modeBox.item)
configPage.layout.form.AddFormItem(configPage.exporterImage.item)
configPage.layout.form.AddFormItem(configPage.metricsPort.item)
}
func (configPage *AddonApcupsdPage) addContainerFields() {
configPage.layout.form.AddFormItem(configPage.apcupsdImage.item)
configPage.layout.form.AddFormItem(configPage.mountPoint.item)
}

func (configPage *AddonApcupsdPage) addNetworkFields() {
configPage.layout.form.AddFormItem(configPage.apcupsdAddress.item)
}

// Handle a bulk redraw request
func (configPage *AddonApcupsdPage) handleLayoutChanged() {
configPage.handleEnableChanged()
}
Loading