kvm/internal/usbgadget/config_tx.go

350 lines
9.4 KiB
Go

package usbgadget
import (
"fmt"
"path"
"path/filepath"
"sort"
"github.com/rs/zerolog"
)
// no os package should occur in this file
type UsbGadgetTransaction struct {
c *ChangeSet
// below are the fields that are needed to be set by the caller
log *zerolog.Logger
udc string
dwc3Path string
kvmGadgetPath string
configC1Path string
orderedConfigItems orderedGadgetConfigItems
isGadgetConfigItemEnabled func(key string) bool
reorderSymlinkChanges *RequestedFileChange
}
func (u *UsbGadget) newUsbGadgetTransaction(lock bool) error {
if lock {
u.txLock.Lock()
defer u.txLock.Unlock()
}
if u.tx != nil {
return fmt.Errorf("transaction already exists")
}
tx := &UsbGadgetTransaction{
c: &ChangeSet{},
log: u.log,
udc: u.udc,
dwc3Path: dwc3Path,
kvmGadgetPath: u.kvmGadgetPath,
configC1Path: u.configC1Path,
orderedConfigItems: u.getOrderedConfigItems(),
isGadgetConfigItemEnabled: u.isGadgetConfigItemEnabled,
}
u.tx = tx
return nil
}
func (u *UsbGadget) WithTransaction(fn func() error) error {
u.txLock.Lock()
defer u.txLock.Unlock()
err := u.newUsbGadgetTransaction(false)
if err != nil {
u.log.Error().Err(err).Msg("failed to create transaction")
return err
}
if err := fn(); err != nil {
u.log.Error().Err(err).Msg("transaction failed")
return err
}
result := u.tx.Commit()
u.tx = nil
return result
}
func (tx *UsbGadgetTransaction) addFileChange(component string, change RequestedFileChange) string {
change.Component = component
tx.c.AddFileChangeStruct(change)
key := change.Key
if key == "" {
key = change.Path
}
return key
}
func (tx *UsbGadgetTransaction) mkdirAll(component string, path string, description string, deps []string) string {
return tx.addFileChange(component, RequestedFileChange{
Path: path,
ExpectedState: FileStateDirectory,
Description: description,
DependsOn: deps,
})
}
func (tx *UsbGadgetTransaction) removeFile(component string, path string, description string) string {
return tx.addFileChange(component, RequestedFileChange{
Path: path,
ExpectedState: FileStateAbsent,
Description: description,
})
}
func (tx *UsbGadgetTransaction) Commit() error {
tx.addFileChange("gadget-finalize", *tx.reorderSymlinkChanges)
err := tx.c.Apply()
if err != nil {
tx.log.Error().Err(err).Msg("failed to update usbgadget configuration")
return err
}
tx.log.Info().Msg("usbgadget configuration updated")
return nil
}
func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
items := make([]gadgetConfigItemWithKey, 0)
for key, item := range u.configMap {
items = append(items, gadgetConfigItemWithKey{key, item})
}
sort.Slice(items, func(i, j int) bool {
return items[i].item.order < items[j].item.order
})
return items
}
func (tx *UsbGadgetTransaction) MountConfigFS() {
tx.addFileChange("gadget", RequestedFileChange{
Path: configFSPath,
ExpectedState: FileStateMountedConfigFS,
Description: "mount configfs",
})
}
func (tx *UsbGadgetTransaction) CreateConfigPath() {
tx.mkdirAll(
"gadget",
tx.configC1Path,
"create config path",
[]string{configFSPath},
)
}
func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
// create kvm gadget path
tx.mkdirAll(
"gadget",
tx.kvmGadgetPath,
"create kvm gadget path",
[]string{tx.configC1Path},
)
deps := make([]string, 0)
deps = append(deps, tx.kvmGadgetPath)
for _, val := range tx.orderedConfigItems {
key := val.key
item := val.item
// check if the item is enabled in the config
if !tx.isGadgetConfigItemEnabled(key) {
tx.DisableGadgetItemConfig(item)
continue
}
deps = tx.writeGadgetItemConfig(item, deps)
}
tx.WriteUDC()
}
func (tx *UsbGadgetTransaction) getDisableKeys() []string {
disableKeys := make([]string, 0)
for _, item := range tx.orderedConfigItems {
if !tx.isGadgetConfigItemEnabled(item.key) {
continue
}
if item.item.configPath == nil || item.item.configAttrs != nil {
continue
}
disableKeys = append(disableKeys, fmt.Sprintf("disable-%s", item.item.device))
}
return disableKeys
}
func (tx *UsbGadgetTransaction) DisableGadgetItemConfig(item gadgetConfigItem) {
// remove symlink if exists
if item.configPath == nil {
return
}
configPath := joinPath(tx.configC1Path, item.configPath)
_ = tx.removeFile("gadget", configPath, "remove symlink: disable gadget config")
}
func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, deps []string) []string {
component := item.device
// create directory for the item
files := make([]string, 0)
files = append(files, deps...)
gadgetItemPath := joinPath(tx.kvmGadgetPath, item.path)
if gadgetItemPath != tx.kvmGadgetPath {
gadgetItemDir := tx.mkdirAll(component, gadgetItemPath, "create gadget item directory", files)
files = append(files, gadgetItemDir)
}
beforeChange := make([]string, 0)
disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device)
if item.configPath != nil && item.configAttrs == nil {
beforeChange = append(beforeChange, tx.getDisableKeys()...)
}
if len(item.attrs) > 0 {
// write attributes for the item
files = append(files, tx.writeGadgetAttrs(
gadgetItemPath,
item.attrs,
component,
beforeChange,
)...)
}
// write report descriptor if available
reportDescPath := path.Join(gadgetItemPath, "report_desc")
if item.reportDesc != nil {
tx.addFileChange(component, RequestedFileChange{
Path: reportDescPath,
ExpectedState: FileStateFileContentMatch,
ExpectedContent: item.reportDesc,
Description: "write report descriptor",
BeforeChange: beforeChange,
DependsOn: files,
})
} else {
tx.addFileChange(component, RequestedFileChange{
Path: reportDescPath,
ExpectedState: FileStateAbsent,
Description: "remove report descriptor",
BeforeChange: beforeChange,
DependsOn: files,
})
}
files = append(files, reportDescPath)
// create config directory if configAttrs are set
if len(item.configAttrs) > 0 {
configItemPath := joinPath(tx.configC1Path, item.configPath)
if configItemPath != tx.configC1Path {
configItemDir := tx.mkdirAll(component, configItemPath, "create config item directory", files)
files = append(files, configItemDir)
}
files = append(files, tx.writeGadgetAttrs(
configItemPath,
item.configAttrs,
component,
beforeChange,
)...)
}
// create symlink if configPath is set
if item.configPath != nil && item.configAttrs == nil {
configPath := joinPath(tx.configC1Path, item.configPath)
// the change will be only applied by `beforeChange`
tx.addFileChange(component, RequestedFileChange{
Key: disableGadgetItemKey,
Path: configPath,
ExpectedState: FileStateAbsent,
When: "beforeChange", // TODO: make it more flexible
Description: "remove symlink",
})
tx.addReorderSymlinkChange(configPath, gadgetItemPath, files)
}
return files
}
func (tx *UsbGadgetTransaction) writeGadgetAttrs(basePath string, attrs gadgetAttributes, component string, beforeChange []string) (files []string) {
files = make([]string, 0)
for key, val := range attrs {
filePath := filepath.Join(basePath, key)
tx.addFileChange(component, RequestedFileChange{
Path: filePath,
ExpectedState: FileStateFileContentMatch,
ExpectedContent: []byte(val),
Description: "write gadget attribute",
DependsOn: []string{basePath},
BeforeChange: beforeChange,
})
files = append(files, filePath)
}
return files
}
func (tx *UsbGadgetTransaction) addReorderSymlinkChange(path string, target string, deps []string) {
tx.log.Trace().Str("path", path).Str("target", target).Msg("add reorder symlink change")
if tx.reorderSymlinkChanges == nil {
tx.reorderSymlinkChanges = &RequestedFileChange{
Component: "gadget-finalize",
Key: "reorder-symlinks",
Path: tx.configC1Path,
ExpectedState: FileStateSymlinkInOrderConfigFS,
Description: "order symlinks",
ParamSymlinks: []symlink{},
}
}
tx.reorderSymlinkChanges.DependsOn = append(tx.reorderSymlinkChanges.DependsOn, deps...)
tx.reorderSymlinkChanges.ParamSymlinks = append(tx.reorderSymlinkChanges.ParamSymlinks, symlink{
Path: path,
Target: target,
})
}
func (tx *UsbGadgetTransaction) WriteUDC() {
// bound the gadget to a UDC (USB Device Controller)
path := path.Join(tx.kvmGadgetPath, "UDC")
tx.addFileChange("udc", RequestedFileChange{
Key: "udc",
Path: path,
ExpectedState: FileStateFileContentMatch,
ExpectedContent: []byte(tx.udc),
DependsOn: []string{"reorder-symlinks"},
Description: "write UDC",
})
}
func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
// remove the gadget from the UDC
tx.addFileChange("udc", RequestedFileChange{
Path: path.Join(tx.dwc3Path, "unbind"),
ExpectedState: FileStateFileWrite,
ExpectedContent: []byte(tx.udc),
Description: "unbind UDC",
DependsOn: []string{"udc"},
IgnoreErrors: ignoreUnbindError,
})
// bind the gadget to the UDC
tx.addFileChange("udc", RequestedFileChange{
Path: path.Join(tx.dwc3Path, "bind"),
ExpectedState: FileStateFileWrite,
ExpectedContent: []byte(tx.udc),
Description: "bind UDC",
DependsOn: []string{path.Join(tx.dwc3Path, "unbind")},
})
}