mirror of
https://github.com/vishvananda/netlink
synced 2024-12-27 17:12:23 +00:00
084abd93d3
Add a specific error to report that a netlink response had NLM_F_DUMP_INTR set, indicating that the set of results may be incomplete or inconsistent. unix.EINTR was previously returned (with no results) when the NLM_F_DUMP_INTR flag was set. Now, errors.Is(err, unix.EINTR) will still work. But, this will be a breaking change for any code that's checking for equality with unix.EINTR. Return results with ErrDumpInterrupted. Results may be incomplete or inconsistent, but give the caller the option of using them. Look for NLM_F_DUMP_INTR in more places: - linkSubscribeAt, neighSubscribeAt, routeSubscribeAt - can do an initial dump, which may report inconsistent results -> if there's an error callback, call it with ErrDumpInterrupted - socketDiagXDPExecutor - makes an NLM_F_DUMP request, without using Execute() -> give it the same behaviour as functions that do use Execute() Signed-off-by: Rob Murray <rob.murray@docker.com>
492 lines
14 KiB
Go
492 lines
14 KiB
Go
package netlink
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/vishvananda/netlink/nl"
|
|
)
|
|
|
|
type vdpaDevID struct {
|
|
Name string
|
|
ID uint32
|
|
}
|
|
|
|
// VDPADev contains info about VDPA device
|
|
type VDPADev struct {
|
|
vdpaDevID
|
|
VendorID uint32
|
|
MaxVQS uint32
|
|
MaxVQSize uint16
|
|
MinVQSize uint16
|
|
}
|
|
|
|
// VDPADevConfig contains configuration of the VDPA device
|
|
type VDPADevConfig struct {
|
|
vdpaDevID
|
|
Features uint64
|
|
NegotiatedFeatures uint64
|
|
Net VDPADevConfigNet
|
|
}
|
|
|
|
// VDPADevVStats conatins vStats for the VDPA device
|
|
type VDPADevVStats struct {
|
|
vdpaDevID
|
|
QueueIndex uint32
|
|
Vendor []VDPADevVStatsVendor
|
|
NegotiatedFeatures uint64
|
|
}
|
|
|
|
// VDPADevVStatsVendor conatins name and value for vendor specific vstat option
|
|
type VDPADevVStatsVendor struct {
|
|
Name string
|
|
Value uint64
|
|
}
|
|
|
|
// VDPADevConfigNet conatins status and net config for the VDPA device
|
|
type VDPADevConfigNet struct {
|
|
Status VDPADevConfigNetStatus
|
|
Cfg VDPADevConfigNetCfg
|
|
}
|
|
|
|
// VDPADevConfigNetStatus contains info about net status
|
|
type VDPADevConfigNetStatus struct {
|
|
LinkUp bool
|
|
Announce bool
|
|
}
|
|
|
|
// VDPADevConfigNetCfg contains net config for the VDPA device
|
|
type VDPADevConfigNetCfg struct {
|
|
MACAddr net.HardwareAddr
|
|
MaxVQP uint16
|
|
MTU uint16
|
|
}
|
|
|
|
// VDPAMGMTDev conatins info about VDPA management device
|
|
type VDPAMGMTDev struct {
|
|
BusName string
|
|
DevName string
|
|
SupportedClasses uint64
|
|
SupportedFeatures uint64
|
|
MaxVQS uint32
|
|
}
|
|
|
|
// VDPANewDevParams contains parameters for new VDPA device
|
|
// use SetBits to configure requried features for the device
|
|
// example:
|
|
//
|
|
// VDPANewDevParams{Features: SetBits(0, VIRTIO_NET_F_MTU, VIRTIO_NET_F_CTRL_MAC_ADDR)}
|
|
type VDPANewDevParams struct {
|
|
MACAddr net.HardwareAddr
|
|
MaxVQP uint16
|
|
MTU uint16
|
|
Features uint64
|
|
}
|
|
|
|
// SetBits set provided bits in the uint64 input value
|
|
// usage example:
|
|
// features := SetBits(0, VIRTIO_NET_F_MTU, VIRTIO_NET_F_CTRL_MAC_ADDR)
|
|
func SetBits(input uint64, pos ...int) uint64 {
|
|
for _, p := range pos {
|
|
input |= 1 << uint64(p)
|
|
}
|
|
return input
|
|
}
|
|
|
|
// IsBitSet check if specific bit is set in the uint64 input value
|
|
// usage example:
|
|
// hasNetClass := IsBitSet(mgmtDev, VIRTIO_ID_NET)
|
|
func IsBitSet(input uint64, pos int) bool {
|
|
val := input & (1 << uint64(pos))
|
|
return val > 0
|
|
}
|
|
|
|
// VDPANewDev adds new VDPA device
|
|
// Equivalent to: `vdpa dev add name <name> mgmtdev <mgmtBus>/mgmtName [params]`
|
|
func VDPANewDev(name, mgmtBus, mgmtName string, params VDPANewDevParams) error {
|
|
return pkgHandle.VDPANewDev(name, mgmtBus, mgmtName, params)
|
|
}
|
|
|
|
// VDPADelDev removes VDPA device
|
|
// Equivalent to: `vdpa dev del <name>`
|
|
func VDPADelDev(name string) error {
|
|
return pkgHandle.VDPADelDev(name)
|
|
}
|
|
|
|
// VDPAGetDevList returns list of VDPA devices
|
|
// Equivalent to: `vdpa dev show`
|
|
//
|
|
// If the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func VDPAGetDevList() ([]*VDPADev, error) {
|
|
return pkgHandle.VDPAGetDevList()
|
|
}
|
|
|
|
// VDPAGetDevByName returns VDPA device selected by name
|
|
// Equivalent to: `vdpa dev show <name>`
|
|
func VDPAGetDevByName(name string) (*VDPADev, error) {
|
|
return pkgHandle.VDPAGetDevByName(name)
|
|
}
|
|
|
|
// VDPAGetDevConfigList returns list of VDPA devices configurations
|
|
// Equivalent to: `vdpa dev config show`
|
|
//
|
|
// If the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func VDPAGetDevConfigList() ([]*VDPADevConfig, error) {
|
|
return pkgHandle.VDPAGetDevConfigList()
|
|
}
|
|
|
|
// VDPAGetDevConfigByName returns VDPA device configuration selected by name
|
|
// Equivalent to: `vdpa dev config show <name>`
|
|
func VDPAGetDevConfigByName(name string) (*VDPADevConfig, error) {
|
|
return pkgHandle.VDPAGetDevConfigByName(name)
|
|
}
|
|
|
|
// VDPAGetDevVStats returns vstats for VDPA device
|
|
// Equivalent to: `vdpa dev vstats show <name> qidx <queueIndex>`
|
|
func VDPAGetDevVStats(name string, queueIndex uint32) (*VDPADevVStats, error) {
|
|
return pkgHandle.VDPAGetDevVStats(name, queueIndex)
|
|
}
|
|
|
|
// VDPAGetMGMTDevList returns list of mgmt devices
|
|
// Equivalent to: `vdpa mgmtdev show`
|
|
//
|
|
// If the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func VDPAGetMGMTDevList() ([]*VDPAMGMTDev, error) {
|
|
return pkgHandle.VDPAGetMGMTDevList()
|
|
}
|
|
|
|
// VDPAGetMGMTDevByBusAndName returns mgmt devices selected by bus and name
|
|
// Equivalent to: `vdpa mgmtdev show <bus>/<name>`
|
|
func VDPAGetMGMTDevByBusAndName(bus, name string) (*VDPAMGMTDev, error) {
|
|
return pkgHandle.VDPAGetMGMTDevByBusAndName(bus, name)
|
|
}
|
|
|
|
type vdpaNetlinkMessage []syscall.NetlinkRouteAttr
|
|
|
|
func (id *vdpaDevID) parseIDAttribute(attr syscall.NetlinkRouteAttr) {
|
|
switch attr.Attr.Type {
|
|
case nl.VDPA_ATTR_DEV_NAME:
|
|
id.Name = nl.BytesToString(attr.Value)
|
|
case nl.VDPA_ATTR_DEV_ID:
|
|
id.ID = native.Uint32(attr.Value)
|
|
}
|
|
}
|
|
|
|
func (netStatus *VDPADevConfigNetStatus) parseStatusAttribute(value []byte) {
|
|
a := native.Uint16(value)
|
|
netStatus.Announce = (a & VIRTIO_NET_S_ANNOUNCE) > 0
|
|
netStatus.LinkUp = (a & VIRTIO_NET_S_LINK_UP) > 0
|
|
}
|
|
|
|
func (d *VDPADev) parseAttributes(attrs vdpaNetlinkMessage) {
|
|
for _, a := range attrs {
|
|
d.parseIDAttribute(a)
|
|
switch a.Attr.Type {
|
|
case nl.VDPA_ATTR_DEV_VENDOR_ID:
|
|
d.VendorID = native.Uint32(a.Value)
|
|
case nl.VDPA_ATTR_DEV_MAX_VQS:
|
|
d.MaxVQS = native.Uint32(a.Value)
|
|
case nl.VDPA_ATTR_DEV_MAX_VQ_SIZE:
|
|
d.MaxVQSize = native.Uint16(a.Value)
|
|
case nl.VDPA_ATTR_DEV_MIN_VQ_SIZE:
|
|
d.MinVQSize = native.Uint16(a.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *VDPADevConfig) parseAttributes(attrs vdpaNetlinkMessage) {
|
|
for _, a := range attrs {
|
|
c.parseIDAttribute(a)
|
|
switch a.Attr.Type {
|
|
case nl.VDPA_ATTR_DEV_NET_CFG_MACADDR:
|
|
c.Net.Cfg.MACAddr = a.Value
|
|
case nl.VDPA_ATTR_DEV_NET_STATUS:
|
|
c.Net.Status.parseStatusAttribute(a.Value)
|
|
case nl.VDPA_ATTR_DEV_NET_CFG_MAX_VQP:
|
|
c.Net.Cfg.MaxVQP = native.Uint16(a.Value)
|
|
case nl.VDPA_ATTR_DEV_NET_CFG_MTU:
|
|
c.Net.Cfg.MTU = native.Uint16(a.Value)
|
|
case nl.VDPA_ATTR_DEV_FEATURES:
|
|
c.Features = native.Uint64(a.Value)
|
|
case nl.VDPA_ATTR_DEV_NEGOTIATED_FEATURES:
|
|
c.NegotiatedFeatures = native.Uint64(a.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *VDPADevVStats) parseAttributes(attrs vdpaNetlinkMessage) {
|
|
for _, a := range attrs {
|
|
s.parseIDAttribute(a)
|
|
switch a.Attr.Type {
|
|
case nl.VDPA_ATTR_DEV_QUEUE_INDEX:
|
|
s.QueueIndex = native.Uint32(a.Value)
|
|
case nl.VDPA_ATTR_DEV_VENDOR_ATTR_NAME:
|
|
s.Vendor = append(s.Vendor, VDPADevVStatsVendor{Name: nl.BytesToString(a.Value)})
|
|
case nl.VDPA_ATTR_DEV_VENDOR_ATTR_VALUE:
|
|
if len(s.Vendor) == 0 {
|
|
break
|
|
}
|
|
s.Vendor[len(s.Vendor)-1].Value = native.Uint64(a.Value)
|
|
case nl.VDPA_ATTR_DEV_NEGOTIATED_FEATURES:
|
|
s.NegotiatedFeatures = native.Uint64(a.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *VDPAMGMTDev) parseAttributes(attrs vdpaNetlinkMessage) {
|
|
for _, a := range attrs {
|
|
switch a.Attr.Type {
|
|
case nl.VDPA_ATTR_MGMTDEV_BUS_NAME:
|
|
d.BusName = nl.BytesToString(a.Value)
|
|
case nl.VDPA_ATTR_MGMTDEV_DEV_NAME:
|
|
d.DevName = nl.BytesToString(a.Value)
|
|
case nl.VDPA_ATTR_MGMTDEV_SUPPORTED_CLASSES:
|
|
d.SupportedClasses = native.Uint64(a.Value)
|
|
case nl.VDPA_ATTR_DEV_SUPPORTED_FEATURES:
|
|
d.SupportedFeatures = native.Uint64(a.Value)
|
|
case nl.VDPA_ATTR_DEV_MGMTDEV_MAX_VQS:
|
|
d.MaxVQS = native.Uint32(a.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Handle) vdpaRequest(command uint8, extraFlags int, attrs []*nl.RtAttr) ([]vdpaNetlinkMessage, error) {
|
|
f, err := h.GenlFamilyGet(nl.VDPA_GENL_NAME)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req := h.newNetlinkRequest(int(f.ID), unix.NLM_F_ACK|extraFlags)
|
|
req.AddData(&nl.Genlmsg{
|
|
Command: command,
|
|
Version: nl.VDPA_GENL_VERSION,
|
|
})
|
|
for _, a := range attrs {
|
|
req.AddData(a)
|
|
}
|
|
|
|
resp, executeErr := req.Execute(unix.NETLINK_GENERIC, 0)
|
|
if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) {
|
|
return nil, executeErr
|
|
}
|
|
messages := make([]vdpaNetlinkMessage, 0, len(resp))
|
|
for _, m := range resp {
|
|
attrs, err := nl.ParseRouteAttr(m[nl.SizeofGenlmsg:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
messages = append(messages, attrs)
|
|
}
|
|
return messages, executeErr
|
|
}
|
|
|
|
// dump all devices if dev is nil
|
|
//
|
|
// If dev is nil and the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func (h *Handle) vdpaDevGet(dev *string) ([]*VDPADev, error) {
|
|
var extraFlags int
|
|
var attrs []*nl.RtAttr
|
|
if dev != nil {
|
|
attrs = append(attrs, nl.NewRtAttr(nl.VDPA_ATTR_DEV_NAME, nl.ZeroTerminated(*dev)))
|
|
} else {
|
|
extraFlags = extraFlags | unix.NLM_F_DUMP
|
|
}
|
|
messages, executeErr := h.vdpaRequest(nl.VDPA_CMD_DEV_GET, extraFlags, attrs)
|
|
if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) {
|
|
return nil, executeErr
|
|
}
|
|
devs := make([]*VDPADev, 0, len(messages))
|
|
for _, m := range messages {
|
|
d := &VDPADev{}
|
|
d.parseAttributes(m)
|
|
devs = append(devs, d)
|
|
}
|
|
return devs, executeErr
|
|
}
|
|
|
|
// dump all devices if dev is nil
|
|
//
|
|
// If dev is nil, and the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func (h *Handle) vdpaDevConfigGet(dev *string) ([]*VDPADevConfig, error) {
|
|
var extraFlags int
|
|
var attrs []*nl.RtAttr
|
|
if dev != nil {
|
|
attrs = append(attrs, nl.NewRtAttr(nl.VDPA_ATTR_DEV_NAME, nl.ZeroTerminated(*dev)))
|
|
} else {
|
|
extraFlags = extraFlags | unix.NLM_F_DUMP
|
|
}
|
|
messages, executeErr := h.vdpaRequest(nl.VDPA_CMD_DEV_CONFIG_GET, extraFlags, attrs)
|
|
if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) {
|
|
return nil, executeErr
|
|
}
|
|
cfgs := make([]*VDPADevConfig, 0, len(messages))
|
|
for _, m := range messages {
|
|
cfg := &VDPADevConfig{}
|
|
cfg.parseAttributes(m)
|
|
cfgs = append(cfgs, cfg)
|
|
}
|
|
return cfgs, executeErr
|
|
}
|
|
|
|
// dump all devices if dev is nil
|
|
//
|
|
// If dev is nil and the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func (h *Handle) vdpaMGMTDevGet(bus, dev *string) ([]*VDPAMGMTDev, error) {
|
|
var extraFlags int
|
|
var attrs []*nl.RtAttr
|
|
if dev != nil {
|
|
attrs = append(attrs,
|
|
nl.NewRtAttr(nl.VDPA_ATTR_MGMTDEV_DEV_NAME, nl.ZeroTerminated(*dev)),
|
|
)
|
|
if bus != nil {
|
|
attrs = append(attrs,
|
|
nl.NewRtAttr(nl.VDPA_ATTR_MGMTDEV_BUS_NAME, nl.ZeroTerminated(*bus)),
|
|
)
|
|
}
|
|
} else {
|
|
extraFlags = extraFlags | unix.NLM_F_DUMP
|
|
}
|
|
messages, executeErr := h.vdpaRequest(nl.VDPA_CMD_MGMTDEV_GET, extraFlags, attrs)
|
|
if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) {
|
|
return nil, executeErr
|
|
}
|
|
cfgs := make([]*VDPAMGMTDev, 0, len(messages))
|
|
for _, m := range messages {
|
|
cfg := &VDPAMGMTDev{}
|
|
cfg.parseAttributes(m)
|
|
cfgs = append(cfgs, cfg)
|
|
}
|
|
return cfgs, executeErr
|
|
}
|
|
|
|
// VDPANewDev adds new VDPA device
|
|
// Equivalent to: `vdpa dev add name <name> mgmtdev <mgmtBus>/mgmtName [params]`
|
|
func (h *Handle) VDPANewDev(name, mgmtBus, mgmtName string, params VDPANewDevParams) error {
|
|
attrs := []*nl.RtAttr{
|
|
nl.NewRtAttr(nl.VDPA_ATTR_DEV_NAME, nl.ZeroTerminated(name)),
|
|
nl.NewRtAttr(nl.VDPA_ATTR_MGMTDEV_DEV_NAME, nl.ZeroTerminated(mgmtName)),
|
|
}
|
|
if mgmtBus != "" {
|
|
attrs = append(attrs, nl.NewRtAttr(nl.VDPA_ATTR_MGMTDEV_BUS_NAME, nl.ZeroTerminated(mgmtBus)))
|
|
}
|
|
if len(params.MACAddr) != 0 {
|
|
attrs = append(attrs, nl.NewRtAttr(nl.VDPA_ATTR_DEV_NET_CFG_MACADDR, params.MACAddr))
|
|
}
|
|
if params.MaxVQP > 0 {
|
|
attrs = append(attrs, nl.NewRtAttr(nl.VDPA_ATTR_DEV_NET_CFG_MAX_VQP, nl.Uint16Attr(params.MaxVQP)))
|
|
}
|
|
if params.MTU > 0 {
|
|
attrs = append(attrs, nl.NewRtAttr(nl.VDPA_ATTR_DEV_NET_CFG_MTU, nl.Uint16Attr(params.MTU)))
|
|
}
|
|
if params.Features > 0 {
|
|
attrs = append(attrs, nl.NewRtAttr(nl.VDPA_ATTR_DEV_FEATURES, nl.Uint64Attr(params.Features)))
|
|
}
|
|
_, err := h.vdpaRequest(nl.VDPA_CMD_DEV_NEW, 0, attrs)
|
|
return err
|
|
}
|
|
|
|
// VDPADelDev removes VDPA device
|
|
// Equivalent to: `vdpa dev del <name>`
|
|
func (h *Handle) VDPADelDev(name string) error {
|
|
_, err := h.vdpaRequest(nl.VDPA_CMD_DEV_DEL, 0, []*nl.RtAttr{
|
|
nl.NewRtAttr(nl.VDPA_ATTR_DEV_NAME, nl.ZeroTerminated(name))})
|
|
return err
|
|
}
|
|
|
|
// VDPAGetDevList returns list of VDPA devices
|
|
// Equivalent to: `vdpa dev show`
|
|
//
|
|
// If the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func (h *Handle) VDPAGetDevList() ([]*VDPADev, error) {
|
|
return h.vdpaDevGet(nil)
|
|
}
|
|
|
|
// VDPAGetDevByName returns VDPA device selected by name
|
|
// Equivalent to: `vdpa dev show <name>`
|
|
func (h *Handle) VDPAGetDevByName(name string) (*VDPADev, error) {
|
|
devs, err := h.vdpaDevGet(&name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(devs) == 0 {
|
|
return nil, fmt.Errorf("device not found")
|
|
}
|
|
return devs[0], nil
|
|
}
|
|
|
|
// VDPAGetDevConfigList returns list of VDPA devices configurations
|
|
// Equivalent to: `vdpa dev config show`
|
|
//
|
|
// If the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func (h *Handle) VDPAGetDevConfigList() ([]*VDPADevConfig, error) {
|
|
return h.vdpaDevConfigGet(nil)
|
|
}
|
|
|
|
// VDPAGetDevConfigByName returns VDPA device configuration selected by name
|
|
// Equivalent to: `vdpa dev config show <name>`
|
|
func (h *Handle) VDPAGetDevConfigByName(name string) (*VDPADevConfig, error) {
|
|
cfgs, err := h.vdpaDevConfigGet(&name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(cfgs) == 0 {
|
|
return nil, fmt.Errorf("configuration not found")
|
|
}
|
|
return cfgs[0], nil
|
|
}
|
|
|
|
// VDPAGetDevVStats returns vstats for VDPA device
|
|
// Equivalent to: `vdpa dev vstats show <name> qidx <queueIndex>`
|
|
func (h *Handle) VDPAGetDevVStats(name string, queueIndex uint32) (*VDPADevVStats, error) {
|
|
messages, err := h.vdpaRequest(nl.VDPA_CMD_DEV_VSTATS_GET, 0, []*nl.RtAttr{
|
|
nl.NewRtAttr(nl.VDPA_ATTR_DEV_NAME, nl.ZeroTerminated(name)),
|
|
nl.NewRtAttr(nl.VDPA_ATTR_DEV_QUEUE_INDEX, nl.Uint32Attr(queueIndex)),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(messages) == 0 {
|
|
return nil, fmt.Errorf("stats not found")
|
|
}
|
|
stats := &VDPADevVStats{}
|
|
stats.parseAttributes(messages[0])
|
|
return stats, nil
|
|
}
|
|
|
|
// VDPAGetMGMTDevList returns list of mgmt devices
|
|
// Equivalent to: `vdpa mgmtdev show`
|
|
//
|
|
// If the returned error is [ErrDumpInterrupted], results may be inconsistent
|
|
// or incomplete.
|
|
func (h *Handle) VDPAGetMGMTDevList() ([]*VDPAMGMTDev, error) {
|
|
return h.vdpaMGMTDevGet(nil, nil)
|
|
}
|
|
|
|
// VDPAGetMGMTDevByBusAndName returns mgmt devices selected by bus and name
|
|
// Equivalent to: `vdpa mgmtdev show <bus>/<name>`
|
|
func (h *Handle) VDPAGetMGMTDevByBusAndName(bus, name string) (*VDPAMGMTDev, error) {
|
|
var busPtr *string
|
|
if bus != "" {
|
|
busPtr = &bus
|
|
}
|
|
devs, err := h.vdpaMGMTDevGet(busPtr, &name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(devs) == 0 {
|
|
return nil, fmt.Errorf("mgmtdev not found")
|
|
}
|
|
return devs[0], nil
|
|
}
|