Merge ~cgrabowski/maas:rack_spike_power_drivers into maas:rack_region_exploration

Proposed by Christian Grabowski
Status: Merged
Approved by: Alexsander de Souza
Approved revision: 69f139c38789997c1e8f7f9dd3e580286328bc36
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~cgrabowski/maas:rack_spike_power_drivers
Merge into: maas:rack_region_exploration
Diff against target: 1480 lines (+1444/-0)
6 files modified
src/rackd_spike/internal/drivers/ip_extractor.go (+20/-0)
src/rackd_spike/internal/drivers/power/driver.go (+194/-0)
src/rackd_spike/internal/drivers/power/ipmi.go (+248/-0)
src/rackd_spike/internal/drivers/power/ipmi/conn.go (+656/-0)
src/rackd_spike/internal/drivers/power/ipmi/packet.go (+287/-0)
src/rackd_spike/internal/machine_helpers/arp.go (+39/-0)
Reviewer Review Type Date Requested Status
Alexsander de Souza Approve
MAAS Lander Approve
Review via email: mp+407084@code.launchpad.net

Commit message

add fixes based on tcpdump results

add power on / off via IPMI

ipmi requests

define interfaces for power drivers

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b rack_spike_power_drivers lp:~cgrabowski/maas/+git/maas into -b rack_region_exploration lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 69f139c38789997c1e8f7f9dd3e580286328bc36

review: Approve
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/rackd_spike/internal/drivers/ip_extractor.go b/src/rackd_spike/internal/drivers/ip_extractor.go
2new file mode 100644
3index 0000000..8be4858
4--- /dev/null
5+++ b/src/rackd_spike/internal/drivers/ip_extractor.go
6@@ -0,0 +1,20 @@
7+package drivers
8+
9+import "regexp"
10+
11+var (
12+ IPExtractorIdentity = regexp.MustCompile("^(?P<address>.+?)$")
13+ IPExtractorURL = regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`)
14+)
15+
16+type IPExtractor struct {
17+ Field string
18+ Pattern *regexp.Regexp
19+}
20+
21+func NewIPExtractor(fieldName string, pattern *regexp.Regexp) IPExtractor {
22+ return IPExtractor{
23+ Field: fieldName,
24+ Pattern: pattern,
25+ }
26+}
27diff --git a/src/rackd_spike/internal/drivers/power/driver.go b/src/rackd_spike/internal/drivers/power/driver.go
28new file mode 100644
29index 0000000..0764268
30--- /dev/null
31+++ b/src/rackd_spike/internal/drivers/power/driver.go
32@@ -0,0 +1,194 @@
33+package power
34+
35+import (
36+ "context"
37+ "errors"
38+ "fmt"
39+ "net"
40+ "sync"
41+)
42+
43+const (
44+ ConfigTypeUnknown = iota
45+ ConfigTypeString
46+ ConfigTypeMACAddress
47+ ConfigTypeChoice
48+ ConfigTypePassword
49+)
50+
51+const (
52+ ConfigScopeBMC = iota
53+ ConfigScopeNode
54+)
55+
56+var (
57+ ErrFieldAlreadyDefined = errors.New("the given field is already defined")
58+ ErrTooManyOptionalAttrs = errors.New("too many optional attributes, only one set of optional attributes to a given field may be provied")
59+ ErrMustBeTypeChoice = errors.New("choices can only be provided for a field of type choice")
60+ ErrFieldNotFound = errors.New("the given field is not found in the given config")
61+ ErrFieldWrongType = errors.New("the given field is of a different type than requested")
62+ ErrInvalidChoice = errors.New("invalid choice provided")
63+ ErrInvalidType = errors.New("the given field is defined with an invalid type")
64+)
65+
66+type powerConfigField struct {
67+ Key string
68+ Val string
69+ Required bool
70+ Default string
71+ Type int
72+ Choices map[string]string
73+ Scope int
74+}
75+
76+func (p powerConfigField) ValOrDefault() string {
77+ if len(p.Val) > 0 {
78+ return p.Val
79+ }
80+ return p.Default
81+}
82+
83+type OptionalFieldAttrs struct {
84+ Default string
85+ Choices map[string]string
86+ Scope int
87+}
88+
89+type PowerConfig struct {
90+ sync.RWMutex
91+ cfg map[string]powerConfigField
92+}
93+
94+func (p *PowerConfig) CreateField(name string, fieldType int, required bool, optional ...OptionalFieldAttrs) error {
95+ p.Lock()
96+ defer p.Unlock()
97+
98+ if p.cfg == nil {
99+ p.cfg = make(map[string]powerConfigField)
100+ }
101+
102+ if _, ok := p.cfg[name]; ok {
103+ return fmt.Errorf("%w: %s", ErrFieldAlreadyDefined, name)
104+ }
105+ if len(optional) > 1 {
106+ return ErrTooManyOptionalAttrs
107+ }
108+
109+ field := powerConfigField{
110+ Key: name,
111+ Type: fieldType,
112+ Required: required,
113+ }
114+ if len(optional) == 1 {
115+ field.Default = optional[0].Default
116+ field.Scope = optional[0].Scope
117+ if optional[0].Choices != nil {
118+ if field.Type != ConfigTypeChoice {
119+ return fmt.Errorf("%w: %s", ErrMustBeTypeChoice, name)
120+ }
121+ field.Choices = optional[0].Choices
122+ }
123+ }
124+
125+ p.cfg[name] = field
126+ return nil
127+}
128+
129+func (p *PowerConfig) Get(name string) (string, error) {
130+ p.RLock()
131+ defer p.RUnlock()
132+
133+ field, ok := p.cfg[name]
134+ if !ok {
135+ return "", fmt.Errorf("%w: %s", ErrFieldNotFound, name)
136+ }
137+ return field.ValOrDefault(), nil
138+}
139+
140+func (p *PowerConfig) Set(name, val string) error {
141+ p.Lock()
142+ defer p.Unlock()
143+
144+ field, ok := p.cfg[name]
145+ if !ok {
146+ return fmt.Errorf("%w: %s", ErrFieldNotFound, name)
147+ }
148+ switch field.Type {
149+ case ConfigTypeString:
150+ field.Val = val
151+ case ConfigTypeChoice:
152+ _, ok := field.Choices[val]
153+ if !ok {
154+ return fmt.Errorf("%w: %s is an invalid choice for %s", ErrInvalidChoice, val, name)
155+ }
156+ field.Val = val
157+ case ConfigTypeMACAddress:
158+ _, err := net.ParseMAC(val)
159+ if err != nil {
160+ return err
161+ }
162+ field.Val = val
163+ case ConfigTypePassword:
164+ field.Val = val
165+ default:
166+ return fmt.Errorf("%w: %s", ErrInvalidType, name)
167+ }
168+ p.cfg[name] = field
169+ return nil
170+}
171+
172+func (p *PowerConfig) GetMAC(name string) (net.HardwareAddr, error) {
173+ field, ok := p.cfg[name]
174+ if !ok {
175+ return nil, fmt.Errorf("%w: %s", ErrFieldNotFound, name)
176+ }
177+ if field.Type != ConfigTypeMACAddress {
178+ return nil, fmt.Errorf("%w: %s is not of type MAC Address", ErrFieldWrongType, name)
179+ }
180+ return net.ParseMAC(field.ValOrDefault())
181+}
182+
183+func (p *PowerConfig) GetChoice(name string) (string, error) {
184+ field, ok := p.cfg[name]
185+ if !ok {
186+ return "", fmt.Errorf("%w: %s", ErrFieldNotFound, name)
187+ }
188+ if field.Type != ConfigTypeChoice {
189+ return "", fmt.Errorf("%w: %s is not of type Choice", ErrFieldWrongType, name)
190+ }
191+ return field.Choices[field.ValOrDefault()], nil
192+}
193+
194+func (p *PowerConfig) GetPassword(name string) (string, error) {
195+ field, ok := p.cfg[name]
196+ if !ok {
197+ return "", fmt.Errorf("%w: %s", ErrFieldNotFound, name)
198+ }
199+ if field.Type != ConfigTypePassword {
200+ return "", fmt.Errorf("%w: %s is not of type Password", ErrFieldWrongType, name)
201+ }
202+ return field.ValOrDefault(), nil
203+}
204+
205+type PowerDriver interface {
206+ Name() string
207+ Description() string
208+ Settings() PowerConfig
209+ IPExactor() (string, string)
210+ Queryable() bool
211+ Chassis() bool
212+ CanProbe() bool
213+ CanSetBootOrder() bool
214+ DetectMissingPackages() error
215+ Schema() map[string]interface{}
216+ GetSetting(string) (string, error)
217+ PowerOn(context.Context, string, PowerConfig) error
218+ PowerOff(context.Context, string, PowerConfig) error
219+ PowerCycle(context.Context, string, PowerConfig) error
220+ PowerQuery(context.Context, string, PowerConfig) (string, error)
221+}
222+
223+type OrderableBootPowerDriver interface {
224+ PowerDriver
225+ SetBootOrder(context.Context, string, PowerConfig, []string)
226+}
227diff --git a/src/rackd_spike/internal/drivers/power/ipmi.go b/src/rackd_spike/internal/drivers/power/ipmi.go
228new file mode 100644
229index 0000000..165e10e
230--- /dev/null
231+++ b/src/rackd_spike/internal/drivers/power/ipmi.go
232@@ -0,0 +1,248 @@
233+package power
234+
235+import (
236+ "context"
237+ "net"
238+
239+ "rackd/internal/drivers"
240+ "rackd/internal/drivers/power/ipmi"
241+ machinehelpers "rackd/internal/machine_helpers"
242+)
243+
244+const (
245+ IPMIDriverLan = "LAN"
246+ IPMIDriverLan2 = "LAN_2_0"
247+
248+ IPMIBootTypeDefault = "auto"
249+ IPMIBootTypeLegacy = "legacy"
250+ IPMIBootTypeEFI = "efi"
251+
252+ IPMIPrivilegeLevelUser = "USER"
253+ IPMIPrivilegeLevelOperator = "OPERATOR"
254+ IPMIPrivilegeLevelAdmin = "ADMIN"
255+)
256+
257+var (
258+ IPMIWaitTime = [4]int{4, 8, 16, 32}
259+
260+ IPMIDriverChoices = map[string]string{
261+ IPMIDriverLan: "LAN [IPMI 1.5]",
262+ IPMIDriverLan2: "LAN_2_0 [IPMI 2.0]",
263+ }
264+
265+ IPMIBootTypeChoices = map[string]string{
266+ IPMIBootTypeDefault: "Automatic",
267+ IPMIBootTypeLegacy: "Legacy boot",
268+ IPMIBootTypeEFI: "EFI boot",
269+ }
270+
271+ IPMIBootTypeMapping = map[string]string{
272+ IPMIBootTypeEFI: "EFI",
273+ IPMIBootTypeLegacy: "PC-COMPATIBLE",
274+ }
275+
276+ IPMICipherSuiteIDChoices = map[string]string{
277+ "": "freeipmi-tools default",
278+ "17": "17 - HMAC-SHA256::HMAC_SHA256_128::AES-CBC-128",
279+ "3": "3 - HMAC-SHA1::HMAC-SHA1-96::AES-CBC-128",
280+ "8": "8 - HMAC-MD5::HMAC-MD5-128::AES-CBC-128",
281+ "12": "12 - HMAC-MD5::MD5-128::AES-CBC-128",
282+ }
283+
284+ IPMIPrivilegeLevelChoices = map[string]string{
285+ IPMIPrivilegeLevelUser: "User",
286+ IPMIPrivilegeLevelOperator: "Operator",
287+ IPMIPrivilegeLevelAdmin: "Administrator",
288+ }
289+)
290+
291+type IPMIDriver struct {
292+ settings *PowerConfig
293+}
294+
295+func NewIPMIDriver() *IPMIDriver {
296+ settings := &PowerConfig{}
297+ err := settings.CreateField("power_driver", ConfigTypeChoice, true,
298+ OptionalFieldAttrs{Default: IPMIDriverLan, Choices: IPMIDriverChoices})
299+ if err != nil {
300+ panic(err) // All these errors should be the result of something defined at compile-time, so better to error hard
301+ }
302+ err = settings.CreateField("power_boot_type", ConfigTypeChoice, false,
303+ OptionalFieldAttrs{Default: IPMIBootTypeDefault, Choices: IPMIBootTypeChoices})
304+ if err != nil {
305+ panic(err)
306+ }
307+ err = settings.CreateField("mac_address", ConfigTypeMACAddress, true)
308+ if err != nil {
309+ panic(err)
310+ }
311+ err = settings.CreateField("power_address", ConfigTypeMACAddress, false)
312+ if err != nil {
313+ panic(err)
314+ }
315+ err = settings.CreateField("power_user", ConfigTypeString, false)
316+ if err != nil {
317+ panic(err)
318+ }
319+ err = settings.CreateField("power_pass", ConfigTypePassword, false)
320+ if err != nil {
321+ panic(err)
322+ }
323+ err = settings.CreateField("k_g", ConfigTypePassword, false)
324+ if err != nil {
325+ panic(err)
326+ }
327+ err = settings.CreateField("cipher_suite_id", ConfigTypeChoice, false,
328+ OptionalFieldAttrs{Default: "3", Choices: IPMICipherSuiteIDChoices})
329+ if err != nil {
330+ panic(err)
331+ }
332+ err = settings.CreateField("privilege_level", ConfigTypeChoice, false,
333+ OptionalFieldAttrs{Default: IPMIPrivilegeLevelOperator, Choices: IPMIPrivilegeLevelChoices})
334+ if err != nil {
335+ panic(err)
336+ }
337+ return &IPMIDriver{
338+ settings: settings,
339+ }
340+}
341+
342+func (i *IPMIDriver) Name() string {
343+ return "ipmi"
344+}
345+
346+func (i *IPMIDriver) Description() string {
347+ return "IPMI"
348+}
349+
350+func (i *IPMIDriver) Settings() *PowerConfig {
351+ return i.settings
352+}
353+
354+func (i *IPMIDriver) IPExtractor() drivers.IPExtractor {
355+ return drivers.NewIPExtractor("power_address", drivers.IPExtractorIdentity)
356+}
357+
358+func (i *IPMIDriver) Queryable() bool {
359+ return false
360+}
361+
362+func (i *IPMIDriver) Chassis() bool {
363+ return false
364+}
365+
366+func (i *IPMIDriver) CanProbe() bool {
367+ return false
368+}
369+
370+func (i *IPMIDriver) CanSetBootOrder() bool {
371+ return false
372+}
373+
374+func (i *IPMIDriver) DetectMissingPackages() error {
375+ return nil
376+}
377+
378+func (i *IPMIDriver) Schema() map[string]interface{} {
379+ return nil
380+}
381+
382+func (i *IPMIDriver) GetSetting(field string) (string, error) {
383+ return "", nil
384+}
385+
386+func (i *IPMIDriver) connFromCfg(ctx context.Context, cfg *PowerConfig) (*ipmi.LanConn, error) {
387+ remoteIPStr, err := cfg.Get("power_address")
388+ if err != nil {
389+ return nil, err
390+ }
391+
392+ var remoteIP net.IP
393+ if len(remoteIPStr) == 0 {
394+ mac, err := cfg.GetMAC("mac_address")
395+ if err != nil {
396+ return nil, err
397+ }
398+ remoteIP, err = machinehelpers.FindIPByArp(ctx, mac)
399+ if err != nil {
400+ return nil, err
401+ }
402+ }
403+ user, err := cfg.Get("power_user")
404+ if err != nil {
405+ return nil, err
406+ }
407+ password, err := cfg.Get("power_pass")
408+ if err != nil {
409+ return nil, err
410+ }
411+ privLvl, err := cfg.GetChoice("privilege_level")
412+ if err != nil {
413+ return nil, err
414+ }
415+ info := ipmi.ConnInfo{
416+ Port: 623,
417+ IP: remoteIP.String(),
418+ BindAddr: "0.0.0.0",
419+ Username: user,
420+ Password: password,
421+ PrivLvl: privLvl,
422+ }
423+ return ipmi.NewLanConn(info), nil
424+}
425+
426+func (i *IPMIDriver) applyConfig(conn *ipmi.LanConn, cfg *PowerConfig) error {
427+ err := conn.SetBootDevice(ipmi.IPMIBootDevicePxe)
428+ if err != nil {
429+ return err
430+ }
431+ return nil
432+}
433+
434+func (i *IPMIDriver) PowerOn(ctx context.Context, systemID string, cfg *PowerConfig) error {
435+ conn, err := i.connFromCfg(ctx, cfg)
436+ if err != nil {
437+ return err
438+ }
439+ err = conn.Open()
440+ if err != nil {
441+ return err
442+ }
443+ defer conn.Close()
444+ err = conn.StartSession()
445+ if err != nil {
446+ return err
447+ }
448+ defer conn.EndSession()
449+ err = i.applyConfig(conn, cfg)
450+ if err != nil {
451+ return err
452+ }
453+ return conn.PowerCtrl(ipmi.IPMIPowerStateOn)
454+}
455+
456+func (i *IPMIDriver) PowerOff(ctx context.Context, systemID string, cfg *PowerConfig) error {
457+ conn, err := i.connFromCfg(ctx, cfg)
458+ if err != nil {
459+ return err
460+ }
461+ err = conn.Open()
462+ if err != nil {
463+ return err
464+ }
465+ defer conn.Close()
466+ err = conn.StartSession()
467+ if err != nil {
468+ return err
469+ }
470+ defer conn.EndSession()
471+ err = i.applyConfig(conn, cfg)
472+ if err != nil {
473+ return err
474+ }
475+ return conn.PowerCtrl(ipmi.IPMIPowerStateOff)
476+}
477+
478+func (i *IPMIDriver) PowerQuery(ctx context.Context, systemID string, cfg *PowerConfig) error {
479+ return nil
480+}
481diff --git a/src/rackd_spike/internal/drivers/power/ipmi/conn.go b/src/rackd_spike/internal/drivers/power/ipmi/conn.go
482new file mode 100644
483index 0000000..62eaf7e
484--- /dev/null
485+++ b/src/rackd_spike/internal/drivers/power/ipmi/conn.go
486@@ -0,0 +1,656 @@
487+package ipmi
488+
489+import (
490+ "bytes"
491+ "crypto/md5"
492+ "crypto/rand"
493+ "encoding"
494+ "encoding/binary"
495+ "io"
496+ "net"
497+ "strconv"
498+ "strings"
499+ "sync/atomic"
500+ "time"
501+)
502+
503+const (
504+ recvBufferSize = 1024
505+)
506+
507+const (
508+ IPMICMDGetDeviceID uint8 = 0x01
509+ IPMICMDChassisStatus uint8 = 0x01
510+ IPMICMDChassisControl uint8 = 0x02
511+ IPMICMDSetSystemBootOptions uint8 = 0x08
512+ IPMICMDGetSystemBootOptions uint8 = 0x09
513+ IPMICMDAuthCapabilities uint8 = 0x38
514+ IPMICMDGetSessionChallenge uint8 = 0x39
515+ IPMICMDActivateSession uint8 = 0x3a
516+ IPMICMDSetSessionPrivilegeLevel uint8 = 0x3b
517+ IPMICMDCloseSession uint8 = 0x3c
518+ IPMICMDSetUsername uint8 = 0x45
519+ IPMICMDGetUsername uint8 = 0x46
520+
521+ IPMIBootDeviceNone uint8 = 0x00
522+ IPMIBootDevicePxe uint8 = 0x04
523+ IPMIBootDeviceDisk uint8 = 0x08
524+ IPMIBootDeviceSafe uint8 = 0x0c
525+ IPMIBootDeviceDiag uint8 = 0x10
526+ IPMIBootDeviceCDROM uint8 = 0x14
527+ IPMIBootDeviceBios uint8 = 0x18
528+ IPMIBootDeviceRemoteFloppy uint8 = 0x1c
529+ IPMIBootDeviceRemoteCDROM uint8 = 0x20
530+ IPMIBootDeviceRemotePrimary uint8 = 0x24
531+ IPMIBootDeviceRemoteDisk uint8 = 0x2c
532+ IPMIBootDeviceFloppy uint8 = 0x3c
533+
534+ IPMIAuthTypeNone uint8 = 0x0
535+ IPMIAuthTypeMD2 uint8 = 0x1
536+ IPMIAuthTypeMD5 uint8 = 0x2
537+ IPMIAuthTypeKey uint8 = 0x4
538+ IPMIAuthTypePassword uint8 = 0x4
539+ IPMIAuthTypeOEM uint8 = 0x5
540+ IPMIAuthTypeRMCPPlus uint8 = 0x6
541+)
542+
543+const (
544+ IPMIInProgressSetComplete uint8 = iota
545+ IPMIInProgressInProgress
546+ IPMIInProgressCommit
547+)
548+
549+const (
550+ IPMIPowerStateOff uint8 = iota
551+ IPMIPowerStateOn
552+ IPMIPowerStateCycle
553+ IPMIPowerStateHardReset
554+ IPMIPowerStatePulseDiag
555+ IPMIPowerStateAcpiSoft
556+)
557+
558+const (
559+ IPMIBootParamInProgress uint8 = iota
560+ IPMIBootParamSvcPartSelect
561+ IPMIBootParamSvcPartScan
562+ IPMIBootParamFlagValid
563+ IPMIBootParamInfoAck
564+ IPMIBootParamBootFlags
565+ IPMIBootParamInitInfo
566+ IPMIBootParamInitMbox
567+)
568+
569+const (
570+ IPMIPrivLevelNone = iota
571+ IPMIPrivLevelCallback
572+ IPMIPrivLevelUser
573+ IPMIPrivLevelOperator
574+ IPMIPrivLevelAdmin
575+ IPMIPrivLevelOEM
576+)
577+
578+type ConnInfo struct {
579+ Port int
580+ Path string
581+ IP string
582+ BindAddr string
583+ Username string
584+ Password string
585+ PrivLvl string
586+}
587+
588+type Request struct {
589+ NetworkFn uint8
590+ Cmd uint8
591+ Data interface{}
592+}
593+
594+type Response interface {
595+ encoding.BinaryUnmarshaler
596+ Data() interface{}
597+}
598+
599+type LanConn struct {
600+ Session session
601+ IPMIHeader ipmiHeader
602+ Info ConnInfo
603+ rqSeq uint8
604+ transport net.Conn
605+ connected bool
606+ authCode [16]byte
607+ username [16]byte
608+ priv uint8
609+ lun uint8
610+ timeout time.Duration
611+}
612+
613+func NewLanConn(info ConnInfo) *LanConn {
614+ conn := &LanConn{
615+ Info: info,
616+ }
617+ switch strings.ToLower(info.PrivLvl) {
618+ case "user":
619+ conn.priv = IPMIPrivLevelUser
620+ case "operator":
621+ conn.priv = IPMIPrivLevelOperator
622+ case "administrator":
623+ conn.priv = IPMIPrivLevelAdmin
624+ }
625+ copy(conn.username[:], info.Username[:])
626+ copy(conn.authCode[:], info.Password[:])
627+ return conn
628+}
629+
630+func (l *LanConn) Open() error {
631+ lAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(l.Info.BindAddr, "0"))
632+ if err != nil {
633+ return err
634+ }
635+ rAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(l.Info.IP, strconv.Itoa(l.Info.Port)))
636+ if err != nil {
637+ return err
638+ }
639+ conn, err := net.DialUDP("udp", lAddr, rAddr)
640+ if err != nil {
641+ return err
642+ }
643+ l.transport = conn
644+ l.connected = true
645+ return nil
646+}
647+
648+func (l *LanConn) Close() error {
649+ if l.connected {
650+ l.Session = session{}
651+ return l.transport.Close()
652+ }
653+ return nil
654+}
655+
656+func (l *LanConn) nextSeq() uint32 {
657+ if l.Session.Seq != 0 {
658+ atomic.AddUint32(&l.Session.Seq, 1)
659+ }
660+ return atomic.LoadUint32(&l.Session.Seq) << 2
661+}
662+
663+func (l *LanConn) nextRqSeq() uint8 {
664+ l.IPMIHeader.RqSeq++
665+ return l.IPMIHeader.RqSeq
666+}
667+
668+func (l *LanConn) sigMD5(data []byte) []byte {
669+ hash := md5.New()
670+ binary.Write(hash, binary.BigEndian, l.authCode)
671+ binary.Write(hash, binary.BigEndian, l.Session.ID)
672+ binary.Write(hash, binary.BigEndian, data)
673+ binary.Write(hash, binary.BigEndian, l.Session.Seq)
674+ binary.Write(hash, binary.BigEndian, l.authCode)
675+ return hash.Sum(nil)
676+}
677+
678+func (l *LanConn) Send(req *Request, resp Response) error {
679+ pkt := &Packet{
680+ RMCPHeader: rmcpHeader{
681+ Version: rmcpV1,
682+ Sequence: 0xff,
683+ Class: rmcpClassIPMI,
684+ },
685+ Session: session{
686+ AuthType: l.Session.AuthType,
687+ Seq: l.nextSeq(),
688+ ID: l.Session.ID,
689+ },
690+ IPMIHeader: ipmiHeader{
691+ RsAddr: 0x20,
692+ NetFnRsLUN: req.NetworkFn<<2 | l.IPMIHeader.NetFnRsLUN&3,
693+ Cmd: req.Cmd,
694+ RqAddr: 0x81,
695+ RqSeq: l.nextRqSeq(),
696+ },
697+ }
698+
699+ if l.Session.AuthType != 0 {
700+ copy(pkt.AuthCode[:], l.authCode[:])
701+ }
702+
703+ err := pkt.SetData(req.Data)
704+ if err != nil {
705+ return err
706+ }
707+
708+ msg, err := MarshalPacket(pkt)
709+ if err != nil {
710+ return err
711+ }
712+
713+ if l.Session.AuthType == IPMIAuthTypeMD5 {
714+ hlen := binary.Size(rmcpHeader{}) + binary.Size(session{})
715+ offset := hlen + len(pkt.AuthCode) + 1
716+ sig := l.sigMD5(msg[offset:])
717+ copy(msg[hlen:], sig)
718+ }
719+
720+ _, err = l.transport.Write(msg)
721+ if err != nil {
722+ return err
723+ }
724+
725+ buf := make([]byte, recvBufferSize)
726+ if l.timeout > 0 {
727+ err = l.transport.SetReadDeadline(time.Now().Add(l.timeout))
728+ if err != nil {
729+ return err
730+ }
731+ }
732+ n, err := l.transport.Read(buf)
733+ if err != nil && err != io.EOF {
734+ return err
735+ }
736+ rPkt, err := UnmarshalPacket(buf[:n])
737+ if err != nil {
738+ return err
739+ }
740+ if resp != nil {
741+ return resp.UnmarshalBinary(rPkt.Data)
742+ }
743+ return nil
744+}
745+
746+func (l *LanConn) genSeq() ([4]byte, error) {
747+ var seq [4]byte
748+ _, err := rand.Read(seq[:])
749+ if err != nil {
750+ return seq, err
751+ }
752+ return seq, nil
753+}
754+
755+func (l *LanConn) Ping() (err error) {
756+ pkt := &ASFPacket{
757+ RMCPHeader: rmcpHeader{
758+ Version: rmcpV1,
759+ Class: rmcpClassASF,
760+ Sequence: 0xff,
761+ },
762+ ASFHeader: asfHeader{
763+ IANANum: ianaASF,
764+ Type: asfTypePing,
765+ },
766+ }
767+
768+ payload := MarshalASFPacket(pkt)
769+ _, err = l.transport.Write(payload)
770+ if err != nil {
771+ return err
772+ }
773+ buf := make([]byte, recvBufferSize)
774+ if l.timeout > 0 {
775+ err = l.transport.SetReadDeadline(time.Now().Add(l.timeout))
776+ if err != nil {
777+ return err
778+ }
779+ }
780+
781+ n, err := l.transport.Read(buf)
782+ if err != nil {
783+ return err
784+ }
785+ pkt, err = UnmarshalASFPacket(buf[n:])
786+ if err != nil {
787+ return err
788+ }
789+ return pkt.ValidatePong()
790+}
791+
792+type AuthCapReq struct {
793+ Channel uint8
794+ PrivLevel uint8
795+}
796+
797+type AuthCapResp struct {
798+ Channel uint8
799+ AuthTypeSupport uint8
800+ Flags uint8
801+ V1 uint8
802+ V2 uint8
803+ OEMID [3]byte
804+ OEMAux uint8
805+}
806+
807+func (a *AuthCapResp) UnmarshalBinary(b []byte) error {
808+ buf := bytes.NewReader(b)
809+ err := binary.Read(buf, binary.BigEndian, a)
810+ if err != nil && err != io.EOF {
811+ return err
812+ }
813+ return nil
814+}
815+
816+func (a *AuthCapResp) Data() interface{} {
817+ return a
818+}
819+
820+func (l *LanConn) GetAuthCaps() (*AuthCapResp, error) {
821+ req := &Request{
822+ NetworkFn: NetworkFnApp,
823+ Cmd: IPMICMDAuthCapabilities,
824+ Data: AuthCapReq{
825+ Channel: 0x0e,
826+ PrivLevel: l.priv,
827+ },
828+ }
829+ var resp AuthCapResp
830+
831+ err := l.Send(req, &resp)
832+ if err != nil {
833+ return nil, err
834+ }
835+
836+ return &resp, nil
837+}
838+
839+type SessionChallengeRequest struct {
840+ AuthType uint8
841+ Username [16]byte
842+}
843+
844+type SessionChallengeResp struct {
845+ Code uint8
846+ SessionID uint32
847+ Challenge [16]byte
848+}
849+
850+func (s *SessionChallengeResp) UnmarshalBinary(b []byte) error {
851+ buf := bytes.NewReader(b)
852+ return binary.Read(buf, binary.BigEndian, s)
853+}
854+
855+func (s *SessionChallengeResp) Data() interface{} {
856+ return s
857+}
858+
859+func (l *LanConn) GetSessionChallenge(authType uint8) (*SessionChallengeResp, error) {
860+ req := &Request{
861+ NetworkFn: NetworkFnApp,
862+ Cmd: IPMICMDGetSessionChallenge,
863+ Data: SessionChallengeRequest{
864+ AuthType: authType,
865+ Username: l.username,
866+ },
867+ }
868+ var resp SessionChallengeResp
869+
870+ err := l.Send(req, &resp)
871+ if err != nil {
872+ return nil, err
873+ }
874+ l.Session.AuthType = authType
875+ return &resp, nil
876+}
877+
878+type ActivateSessionReq struct {
879+ AuthType uint8
880+ PrivLevel uint8
881+ AuthCode [16]byte
882+ Seq [4]byte
883+}
884+
885+type ActivateSessionResp struct {
886+ Code uint8
887+ AuthType uint8
888+ SessionID uint32
889+ Seq uint32
890+ MaxPriv uint8
891+}
892+
893+func (a *ActivateSessionResp) UnmarshalBinary(b []byte) error {
894+ buf := bytes.NewReader(b)
895+ return binary.Read(buf, binary.BigEndian, a)
896+}
897+
898+func (a *ActivateSessionResp) Data() interface{} {
899+ return a
900+}
901+
902+func (l *LanConn) ActivateSession(challenge *SessionChallengeResp) error {
903+ outboundSeq, err := l.genSeq()
904+ if err != nil {
905+ return err
906+ }
907+ req := &Request{
908+ NetworkFn: NetworkFnApp,
909+ Cmd: IPMICMDActivateSession,
910+ Data: ActivateSessionReq{
911+ AuthType: l.Session.AuthType,
912+ PrivLevel: l.priv,
913+ AuthCode: challenge.Challenge,
914+ Seq: outboundSeq,
915+ },
916+ }
917+ var resp ActivateSessionResp
918+ err = l.Send(req, &resp)
919+ if err != nil {
920+ return err
921+ }
922+
923+ l.connected = true
924+ l.Session.ID = resp.SessionID
925+ l.Session.AuthType = resp.AuthType
926+ l.Session.Seq = resp.Seq
927+ return nil
928+}
929+
930+type SetPrivilegeReq struct {
931+ Priv uint8
932+}
933+
934+type SetPrivilegeResponse struct {
935+ Code uint8
936+ Priv uint8
937+}
938+
939+func (s *SetPrivilegeResponse) UnmarshalBinary(b []byte) error {
940+ buf := bytes.NewReader(b)
941+ return binary.Read(buf, binary.BigEndian, s)
942+}
943+
944+func (s *SetPrivilegeResponse) Data() interface{} {
945+ return s
946+}
947+
948+func (l *LanConn) SetPrivilege() error {
949+ req := &Request{
950+ NetworkFn: NetworkFnApp,
951+ Cmd: IPMICMDSetSessionPrivilegeLevel,
952+ Data: SetPrivilegeReq{
953+ Priv: l.priv,
954+ },
955+ }
956+ var resp SetPrivilegeResponse
957+ err := l.Send(req, &resp)
958+ if err != nil {
959+ return err
960+ }
961+ l.priv = resp.Priv
962+ return nil
963+}
964+
965+func (l *LanConn) StartSession() error {
966+ err := l.Ping()
967+ if err != nil {
968+ return err
969+ }
970+ authCaps, err := l.GetAuthCaps()
971+ if err != nil {
972+ return err
973+ }
974+
975+ authType := IPMIAuthTypeNone
976+ for _, authT := range []uint8{IPMIAuthTypeMD5, IPMIAuthTypePassword, IPMIAuthTypeMD2} {
977+ if authCaps.AuthTypeSupport&authT == 0 {
978+ authType = authT
979+ break
980+ }
981+ }
982+
983+ challenge, err := l.GetSessionChallenge(authType)
984+ if err != nil {
985+ return err
986+ }
987+
988+ err = l.ActivateSession(challenge)
989+ if err != nil {
990+ return err
991+ }
992+ return l.SetPrivilege()
993+}
994+
995+type CloseSessionReq struct {
996+ SessionID uint32
997+}
998+
999+func (l *LanConn) EndSession() error {
1000+ req := &Request{
1001+ NetworkFn: NetworkFnApp,
1002+ Cmd: IPMICMDCloseSession,
1003+ Data: CloseSessionReq{
1004+ SessionID: l.Session.ID,
1005+ },
1006+ }
1007+ return l.Send(req, nil)
1008+}
1009+
1010+type DeviceIDResp struct {
1011+ Code uint8
1012+ DeviceID uint8
1013+ DeviceRevision uint8
1014+ FirmwareRevision1 uint8
1015+ FirmwareRevision2 uint8
1016+ IPMIVersion uint8
1017+ AdditionalDeviceSupport uint8
1018+ ManufacturerID uint16
1019+ ProductID uint16
1020+}
1021+
1022+func (d *DeviceIDResp) UnmarshalBinary(b []byte) error {
1023+ buf := bytes.NewReader(b)
1024+ return binary.Read(buf, binary.BigEndian, d)
1025+}
1026+
1027+func (d *DeviceIDResp) Data() interface{} {
1028+ return d
1029+}
1030+
1031+func (l *LanConn) DeviceID() (*DeviceIDResp, error) {
1032+ req := &Request{
1033+ NetworkFn: NetworkFnApp,
1034+ Cmd: IPMICMDGetDeviceID,
1035+ }
1036+ var resp DeviceIDResp
1037+ err := l.Send(req, &resp)
1038+ if err != nil {
1039+ return nil, err
1040+ }
1041+ return &resp, nil
1042+}
1043+
1044+type SetBootOptionsReq struct {
1045+ Param uint8
1046+ Data []uint8
1047+}
1048+
1049+func (l *LanConn) SetBootParam(param uint8, vals ...uint8) error {
1050+ req := &Request{
1051+ NetworkFn: NetworkFnChassis,
1052+ Cmd: IPMICMDSetSystemBootOptions,
1053+ Data: SetBootOptionsReq{
1054+ Param: param,
1055+ Data: vals,
1056+ },
1057+ }
1058+ return l.Send(req, nil)
1059+}
1060+
1061+func (l *LanConn) SetBootDevice(bootDev uint8) error {
1062+ err := l.SetBootParam(IPMIBootParamInProgress, IPMIInProgressInProgress)
1063+ if err != nil {
1064+ return err
1065+ }
1066+ defer l.SetBootParam(IPMIBootParamInProgress, IPMIInProgressSetComplete)
1067+
1068+ err = l.SetBootParam(IPMIBootParamInfoAck, 0x01, 0x01)
1069+ if err != nil {
1070+ return err
1071+ }
1072+
1073+ err = l.SetBootParam(IPMIBootParamBootFlags, 0x80, bootDev, 0x00, 0x00, 0x00)
1074+ if err != nil {
1075+ return err
1076+ }
1077+ err = l.SetBootParam(IPMIBootParamInProgress, IPMIInProgressCommit)
1078+ if err != nil {
1079+ return err
1080+ }
1081+ return nil
1082+}
1083+
1084+func (l *LanConn) PowerCtrl(state uint8) error {
1085+ req := &Request{
1086+ NetworkFn: NetworkFnChassis,
1087+ Cmd: IPMICMDChassisControl,
1088+ Data: state,
1089+ }
1090+ return l.Send(req, nil)
1091+}
1092+
1093+type GetUsernameReq struct {
1094+ UserID uint8
1095+}
1096+
1097+type GetUsernameResp struct {
1098+ Code uint8
1099+ Username string
1100+}
1101+
1102+func (g *GetUsernameResp) UnmarshalBinary(b []byte) error {
1103+ buf := bytes.NewReader(b)
1104+ return binary.Read(buf, binary.BigEndian, g)
1105+}
1106+
1107+func (g *GetUsernameResp) Data() interface{} {
1108+ return g
1109+}
1110+
1111+func (l *LanConn) GetUserName(id uint8) (string, error) {
1112+ req := &Request{
1113+ NetworkFn: NetworkFnApp,
1114+ Cmd: IPMICMDGetUsername,
1115+ Data: GetUsernameReq{
1116+ UserID: id,
1117+ },
1118+ }
1119+ var resp GetUsernameResp
1120+ err := l.Send(req, &resp)
1121+ if err != nil {
1122+ return "", err
1123+ }
1124+ return resp.Username, nil
1125+}
1126+
1127+type SetUsernameReq struct {
1128+ UserID uint8
1129+ Username string
1130+}
1131+
1132+func (l *LanConn) SetUsername(id uint8, username string) error {
1133+ req := &Request{
1134+ NetworkFn: NetworkFnApp,
1135+ Cmd: IPMICMDSetUsername,
1136+ Data: SetUsernameReq{
1137+ UserID: id,
1138+ Username: username,
1139+ },
1140+ }
1141+ return l.Send(req, nil)
1142+}
1143diff --git a/src/rackd_spike/internal/drivers/power/ipmi/packet.go b/src/rackd_spike/internal/drivers/power/ipmi/packet.go
1144new file mode 100644
1145index 0000000..a6a1fa5
1146--- /dev/null
1147+++ b/src/rackd_spike/internal/drivers/power/ipmi/packet.go
1148@@ -0,0 +1,287 @@
1149+package ipmi
1150+
1151+import (
1152+ "bytes"
1153+ "encoding"
1154+ "encoding/binary"
1155+ "errors"
1156+ "fmt"
1157+)
1158+
1159+const (
1160+ rmcpClassASF uint8 = 0x06
1161+ rmcpClassIPMI uint8 = 0x07
1162+ rmcpClassNoAckSeq uint8 = 0xff
1163+
1164+ rmcpV1 uint8 = 0x06
1165+
1166+ ianaASF uint32 = 0x000011be
1167+
1168+ asfTypePing uint8 = 0x80
1169+ asfTypePong uint8 = 0x40
1170+
1171+ NetworkFnChassis uint8 = 0x00
1172+ NetworkFnApp uint8 = 0x06
1173+)
1174+
1175+var (
1176+ minPktSize = binary.Size(rmcpHeader{}) + binary.Size(session{}) + binary.Size(ipmiHeader{})
1177+
1178+ ErrInvalidPacket = errors.New("the received packet is invalid")
1179+)
1180+
1181+func computeChecksum(data ...uint8) uint8 {
1182+ var checksum uint8
1183+ for _, d := range data {
1184+ checksum += d
1185+ }
1186+ return -checksum
1187+}
1188+
1189+type rmcpHeader struct {
1190+ Version uint8
1191+ Reserved uint8
1192+ Sequence uint8
1193+ Class uint8
1194+}
1195+
1196+func (r *rmcpHeader) IsAck() bool {
1197+ return r.Class&0x80 != 0
1198+}
1199+
1200+type session struct {
1201+ AuthType uint8
1202+ Seq uint32
1203+ ID uint32
1204+}
1205+
1206+type ipmiHeader struct {
1207+ Len uint8
1208+ RsAddr uint8
1209+ NetFnRsLUN uint8
1210+ Checksum uint8
1211+ RqAddr uint8
1212+ RqSeq uint8
1213+ Cmd uint8
1214+}
1215+
1216+type Packet struct {
1217+ RMCPHeader rmcpHeader
1218+ Session session
1219+ AuthCode [16]byte
1220+ IPMIHeader ipmiHeader
1221+ Data []byte
1222+}
1223+
1224+func (p *Packet) SetData(data interface{}) error {
1225+ if data == nil {
1226+ return nil
1227+ }
1228+ if encoder, ok := data.(encoding.BinaryMarshaler); ok {
1229+ buf, err := encoder.MarshalBinary()
1230+ if err != nil {
1231+ return err
1232+ }
1233+ p.Data = buf
1234+ return nil
1235+ }
1236+ byteBuf := &bytes.Buffer{}
1237+ err := binary.Write(byteBuf, binary.BigEndian, data)
1238+ if err != nil {
1239+ return err
1240+ }
1241+ p.Data = byteBuf.Bytes()
1242+ return nil
1243+}
1244+
1245+func MarshalPacket(p *Packet) ([]byte, error) {
1246+ buf := &bytes.Buffer{}
1247+
1248+ err := binary.Write(buf, binary.BigEndian, &p.RMCPHeader)
1249+ if err != nil {
1250+ return nil, err
1251+ }
1252+
1253+ err = binary.Write(buf, binary.BigEndian, &p.Session)
1254+ if err != nil {
1255+ return nil, err
1256+ }
1257+
1258+ if p.Session.AuthType != IPMIAuthTypeNone {
1259+ err = binary.Write(buf, binary.BigEndian, p.AuthCode)
1260+ if err != nil {
1261+ return nil, err
1262+ }
1263+ }
1264+
1265+ p.IPMIHeader.Len = uint8(binary.Size(p.IPMIHeader) + len(p.Data))
1266+ p.IPMIHeader.Checksum = computeChecksum(p.IPMIHeader.RsAddr, p.IPMIHeader.NetFnRsLUN)
1267+ err = binary.Write(buf, binary.BigEndian, &p.IPMIHeader)
1268+ if err != nil {
1269+ return nil, err
1270+ }
1271+
1272+ _, err = buf.Write(p.Data)
1273+ if err != nil {
1274+ return nil, err
1275+ }
1276+
1277+ payloadChecksum := computeChecksum(p.IPMIHeader.RqAddr, p.IPMIHeader.RqSeq, p.IPMIHeader.Cmd) + computeChecksum(p.Data...)
1278+
1279+ err = binary.Write(buf, binary.BigEndian, payloadChecksum)
1280+ if err != nil {
1281+ return nil, err
1282+ }
1283+
1284+ return buf.Bytes(), nil
1285+}
1286+
1287+func UnmarshalPacket(buf []byte) (*Packet, error) {
1288+ if len(buf) < minPktSize {
1289+ return nil, fmt.Errorf("%w: too short", ErrInvalidPacket)
1290+ }
1291+
1292+ pkt := &Packet{}
1293+ reader := bytes.NewReader(buf)
1294+
1295+ err := binary.Read(reader, binary.BigEndian, &pkt.RMCPHeader)
1296+ if err != nil {
1297+ return nil, err
1298+ }
1299+
1300+ err = binary.Read(reader, binary.BigEndian, &pkt.Session)
1301+ if err != nil {
1302+ return nil, err
1303+ }
1304+ if pkt.Session.AuthType != 0 {
1305+ err = binary.Read(reader, binary.BigEndian, pkt.AuthCode)
1306+ if err != nil {
1307+ return nil, err
1308+ }
1309+ }
1310+
1311+ err = binary.Read(reader, binary.BigEndian, &pkt.IPMIHeader)
1312+ if err != nil {
1313+ return nil, err
1314+ }
1315+
1316+ if computeChecksum(pkt.IPMIHeader.RsAddr, pkt.IPMIHeader.NetFnRsLUN) != pkt.IPMIHeader.Checksum {
1317+ return nil, fmt.Errorf("%w: checksum does not match", ErrInvalidPacket)
1318+ }
1319+ if pkt.IPMIHeader.Len <= 0 {
1320+ return nil, fmt.Errorf("%w: malformed message len", ErrInvalidPacket)
1321+ }
1322+
1323+ dataLen := int(pkt.IPMIHeader.Len) - binary.Size(ipmiHeader{})
1324+ data := make([]byte, dataLen+1)
1325+ _, err = reader.Read(data)
1326+ if err != nil {
1327+ return nil, err
1328+ }
1329+
1330+ dataChecksum := data[len(data)-1]
1331+ localChecksum := computeChecksum(
1332+ pkt.IPMIHeader.RqAddr,
1333+ pkt.IPMIHeader.RqSeq,
1334+ pkt.IPMIHeader.Cmd,
1335+ ) + computeChecksum(data[:len(data)-1]...)
1336+ if dataChecksum != localChecksum {
1337+ return nil, fmt.Errorf("%w: checksum does not match", ErrInvalidPacket)
1338+ }
1339+
1340+ pkt.Data = data[:len(data)-1]
1341+ return pkt, nil
1342+}
1343+
1344+func Marshal(data interface{}) ([]byte, error) {
1345+ pkt := &Packet{}
1346+ pkt.SetData(data)
1347+ return MarshalPacket(pkt)
1348+}
1349+
1350+func Unmarshal(buf []byte, data interface{}) error {
1351+ pkt, err := UnmarshalPacket(buf)
1352+ if err != nil {
1353+ return err
1354+ }
1355+ if decoder, ok := data.(encoding.BinaryUnmarshaler); ok {
1356+ return decoder.UnmarshalBinary(pkt.Data)
1357+ }
1358+ reader := bytes.NewReader(buf)
1359+ err = binary.Read(reader, binary.BigEndian, pkt.Data)
1360+ if err != nil {
1361+ return err
1362+ }
1363+ return nil
1364+}
1365+
1366+type asfHeader struct {
1367+ IANANum uint32
1368+ Type uint8
1369+ Tag uint8
1370+ Reserved uint8
1371+ DataLen uint8
1372+}
1373+
1374+type ASFPong struct {
1375+ IANANum uint32
1376+ OEM uint32
1377+ Entities uint8
1378+ SupportedInteractions uint8
1379+ Reserved [6]uint8
1380+}
1381+
1382+type ASFPacket struct {
1383+ RMCPHeader rmcpHeader
1384+ ASFHeader asfHeader
1385+ Data []byte
1386+}
1387+
1388+func (a *ASFPacket) SetData(data interface{}) (err error) {
1389+ if encoder, ok := data.(encoding.BinaryMarshaler); ok {
1390+ a.Data, err = encoder.MarshalBinary()
1391+ if err != nil {
1392+ return err
1393+ }
1394+ return nil
1395+ }
1396+ buf := &bytes.Buffer{}
1397+ err = binary.Write(buf, binary.BigEndian, data)
1398+ if err != nil {
1399+ return err
1400+ }
1401+ a.Data = buf.Bytes()
1402+ return nil
1403+}
1404+
1405+func (a *ASFPacket) ValidatePong() error {
1406+ return nil
1407+}
1408+
1409+func MarshalASFPacket(pkt *ASFPacket) []byte {
1410+ buf := &bytes.Buffer{}
1411+ binary.Write(buf, binary.BigEndian, pkt.RMCPHeader)
1412+ binary.Write(buf, binary.BigEndian, pkt.ASFHeader)
1413+ buf.Write(pkt.Data)
1414+ return buf.Bytes()
1415+}
1416+
1417+func UnmarshalASFPacket(buf []byte) (*ASFPacket, error) {
1418+ hlen := binary.Size(rmcpHeader{}) + binary.Size(asfHeader{})
1419+ if len(buf) < hlen {
1420+ return nil, fmt.Errorf("%w: too short", ErrInvalidPacket)
1421+ }
1422+ var pkt ASFPacket
1423+ reader := bytes.NewReader(buf)
1424+ fullLen := reader.Len()
1425+ err := binary.Read(reader, binary.BigEndian, &pkt.RMCPHeader)
1426+ if err != nil {
1427+ return nil, err
1428+ }
1429+ err = binary.Read(reader, binary.BigEndian, &pkt.ASFHeader)
1430+ remainder := fullLen - reader.Len()
1431+ if remainder > 0 {
1432+ copy(pkt.Data[:], buf[remainder:])
1433+ }
1434+ return &pkt, nil
1435+}
1436diff --git a/src/rackd_spike/internal/machine_helpers/arp.go b/src/rackd_spike/internal/machine_helpers/arp.go
1437new file mode 100644
1438index 0000000..3c9e6c5
1439--- /dev/null
1440+++ b/src/rackd_spike/internal/machine_helpers/arp.go
1441@@ -0,0 +1,39 @@
1442+package machinehelpers
1443+
1444+import (
1445+ "context"
1446+ "errors"
1447+ "net"
1448+ "os/exec"
1449+ "strings"
1450+)
1451+
1452+var (
1453+ ErrIPNotFound = errors.New("ip not found for given mac address")
1454+)
1455+
1456+func FindIPByArp(ctx context.Context, mac net.HardwareAddr) (net.IP, error) {
1457+ cmd := exec.CommandContext(ctx, "arp", "-n")
1458+
1459+ out, err := cmd.CombinedOutput()
1460+ if err != nil {
1461+ return nil, err
1462+ }
1463+
1464+ for _, line := range strings.Split(string(out), "\n") {
1465+ columns := strings.Split(line, " ")
1466+ var strippedColumns []string
1467+ for _, column := range columns {
1468+ column = strings.TrimSpace(column)
1469+ if column == "" {
1470+ continue
1471+ }
1472+ strippedColumns = append(strippedColumns, column)
1473+ }
1474+ columns = strippedColumns
1475+ if len(columns) == 5 && strings.ToLower(columns[2]) == strings.ToLower(mac.String()) {
1476+ return net.ParseIP(columns[0]), nil
1477+ }
1478+ }
1479+ return nil, ErrIPNotFound
1480+}

Subscribers

People subscribed via source and target branches