Control your backup NAS from the desktop

Programming Snapshot – Remote Backup

© Lead Image © alphaspirit, 123RF.com

© Lead Image © alphaspirit, 123RF.com

Article from Issue 272/2023
Author(s):

To be able to power up and shut down his NAS and check the current status without getting out of his chair, Mike Schilli programs a graphical interface that sends a Magic Packet in this month's column.

As a backup solution, I use a Synology NAS with some hefty hard drives. But because of the strict regulations driven by paranoia and noise-protection goals in the hallowed halls of Perlmeister Studios, the device only runs when actually needed (i.e., when a backup is running). To switch it on when needed, I prefer to push a mouse around a GUI and click occasionally instead of getting out of my chair.

The Syno desktop application presented here (Figure 1) powers up the NAS via the local network at the push of a button and graphically displays the milestones during the boot process with progress bars. As soon as the system has finished booting and is ready for access, it notifies the user.

Figure 1: Control via desktop app: Mike's Synology NAS booting.

After the work is done, a mouse click on the GUI's Down button is all it takes, and the NAS receives the command to shut down via the network. While the shutdown is running, the GUI checks whether the NAS is still operational or if it is no longer responding to pings and has finally gone to sleep (Figure 2).

Figure 2: At the push of a button, the network storage device then shuts down again.

Just Switch It On

How does this magic work? Even when switched off, the NAS actively waits for a Wake-on-LAN (WoL) signal on the local network. Despite its control lights being off, the network card is running in a low power mode. If a matching broadcast packet arrives via the LAN, it tells the device's power supply or motherboard to switch on. The Magic Packet sent in this case contains the target system's MAC address, so that a controlling transmitter can notify different network nodes independently.

Figure 3 shows a practical example of the Magic Packet's format [1]. The first 6 bytes of the packet header each contain a fixed value, 0xFF. This is followed by the payload, which consists of 16 repetitions of the 6-byte MAC address of the target device (00:11:32:6c:ab:cd in this example). Each network card has its own setting, which specifies the device's manufacturer, model, and individual identifier.

Figure 3: A Magic Packet's hexdump for the MAC address 00:11:32:6c:ab:cd.

Listing 1 cobbles together the Magic Packet in Go. Line 7 sets the NAS's MAC address as a string, while line 8 specifies that its length really is 6 bytes. In rare cases, there could be devices with longer MAC addresses, but their Magic Packets are more difficult to build. For the purpose of illustration, I'll keep things short and sweet.

Listing 1

wol.go

01 package main
02 import (
03   "bytes"
04   "encoding/binary"
05   "net"
06 )
07 const synMAC = "00:11:32:6c:ab:cd"
08 const MacLen = 6
09 type MagicPacket struct {
10   header  [6]byte
11   payload [16][MacLen]byte
12 }
13 func sendMagicPacket() {
14   var packet MagicPacket
15   hwAddr, err := net.ParseMAC(synMAC)
16   if err != nil {
17     panic(err)
18   }
19   for idx := range packet.header {
20     packet.header[idx] = 0xFF
21   }
22   for idx := range packet.payload {
23     for i := 0; i < MacLen; i++ {
24       packet.payload[idx][i] = hwAddr[i]
25     }
26   }
27   buf := new(bytes.Buffer)
28   if err := binary.Write(buf, binary.BigEndian, packet); err != nil {
29     panic(err)
30   }
31   conn, err := net.Dial("udp", "255.255.255.255:9")
32   if err != nil {
33     panic(err)
34   }
35   defer conn.Close()
36   _, err = conn.Write(buf.Bytes())
37   if err != nil {
38     panic(err)
39   }
40 }

The MagicPacket structure starting in line 9 abstracts the packet's two different areas: the header and payload. The sendMagicPacket() function starting in line 13 then wraps the packet and, when done, sends it to the broadcast address 255.255.255.255 on the LAN. This means that all devices on the local network get to see the packet and can react accordingly. If a device listening via WoL receives a packet in which the packaged MAC address matches its own, it initiates the boot process.

The ParseMac() function from the standard net Go library treasure trove translates the MAC string from line 7 into a binary hardware address, which the library's network functions need in order to send off the packet. The packet header with 6 0xFF bytes is assembled by the for loop starting in line 19. The subsequent double loop starting in line 22 then writes the MAC address in binary format 16 times in succession to the packet's payload area.

To convert the Go structure of the MagicPacket type to a binary stream of bytes for packet recipients listening on the network, the standard binary.Write() function in line 28 trawls the structure of the packet variable. To do this, it writes the bytes of the structure in network format (big-endian, most significant byte first) to the buf buffer. Line 36 then uses Write() to send the buffer content via the UDP socket opened in line 31 by net.Dial() to the LAN's broadcast address.

Be careful: binary.Write() can only serialize a structure without error if all of its fields are of a fixed length. The function does not support Go's dynamically expandable array slices. You will see some really ugly runtime errors if you proceed without heeding this warning.

Panic

The states the application can be in at runtime are those of a simple finite machine. After starting the program, the NAS is usually asleep (state DOWN). The user issues the wake-up command by pressing the Up button. Following the bootstrap, the application keeps checking if the NAS can be pinged yet. If it shows no response, it is probably still asleep (i.e., resting in the DOWN state). But as soon as the ping command reports success, the NAS is ready for operation and the finite machine jumps to the UP state.

Figure 4 shows the state machine diagram, while Listing 2 contains the implementation using the Go fsm library from GitHub. The NewFSM function creates a new finite state machine starting in line 8 that processes two events: wake (line 11), which transitions from the DOWN state to the UP state, and sleep (line 12), which switches the machine from UP to DOWN. Listing 2 defines the conditions for these transitions in the enter_UP and enter_DOWN callbacks; the machine jumps to each of these before actually starting a transition.

Listing 2

fsm.go

01 package main
02 import (
03   "context"
04   "time"
05   "github.com/looplab/fsm"
06 )
07 func run(stateReporter chan string, startState string) *fsm.FSM {
08   boot := fsm.NewFSM(
09     startState,
10     fsm.Events{
11       {Name: "wake", Src: []string{"DOWN"}, Dst: "UP"},
12       {Name: "sleep", Src: []string{"UP"}, Dst: "DOWN"},
13     },
14     fsm.Callbacks{
15       "enter_UP": func(ctx context.Context, e *fsm.Event) {
16         for {
17           if isPingable() {
18             stateReporter <- "UP"
19             return
20           }
21           time.Sleep(1 * time.Second)
22         }
23       },
24       "enter_DOWN": func(_ context.Context, e *fsm.Event) {
25         for {
26           if !isPingable() {
27             stateReporter <- "DOWN"
28             return
29           }
30           time.Sleep(1 * time.Second)
31         }
32       },
33     },
34   )
35   return boot
36 }
Figure 4: The states and transitions of the simple finite machine.

Each of these two callbacks erects a hurdle that the program flow needs to clear before entering the new state. An infinite for loop keeps running isPingable() to check whether the desired state has already been reached, in which the NAS is either running or asleep, depending on the callback. If not, the callbacks wait a second and then try again. This is repeated until the desired state is reached.

Then the callbacks send a message with the new state of the machine on the stateReporter channel provided by the caller earlier. The run() function's final act is to return a reference for the ready-to-go state machine to the caller. With this handy tool at its disposal, the caller will be sending new commands to the machine using methods such as Event() to initiate the associated state transitions.

Interestingly, the third-party library used for the fsm library from GitHub uses strings instead of typed variables for its states. This is not recommended Go style, because it means that a simple typo in a state in the code is enough to trigger a frantic search for runtime errors. This way, the type checker in the Go compiler has no way to detect the bug at compile time. The library clearly still has room for improvement, but you can't look a gift horse in the mouth.

On Screen!

Listing 3 has all the code to draw the small GUI shown in the screenshots. It is based on the Fyne framework, which the code pulls in from GitHub at compile time.

Listing 3

syno.go

01 package main
02 import (
03   "context"
04   "fyne.io/fyne/v2"
05   "fyne.io/fyne/v2/app"
06   "fyne.io/fyne/v2/canvas"
07   "fyne.io/fyne/v2/container"
08   "fyne.io/fyne/v2/theme"
09   "fyne.io/fyne/v2/widget"
10   "os"
11 )
12 func main() {
13   state := "DOWN"
14   headText := "NAS Control Center"
15   a := app.New()
16   w := a.NewWindow(headText)
17   status := widget.NewLabelWithStyle(state, fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
18   progress := widget.NewProgressBarInfinite()
19   progress.Stop()
20   progress.Hide()
21   okIcon := widget.NewIcon(theme.ConfirmIcon())
22   okIcon.Hide()
23   downIcon := widget.NewIcon(theme.CancelIcon())
24   stateReporter := make(chan string)
25   runner := run(stateReporter, state)
26   var upButton *widget.Button
27   var downButton *widget.Button
28   upButton = widget.NewButton("Up", func() {
29     upButton.Disable()
30     downButton.Disable()
31     status.Text = "Coming up ..."
32     status.Refresh()
33     sendMagicPacket()
34     progress.Show()
35     go func() {
36       runner.Event(context.Background(), "wake")
37     }()
38   })
39   downButton = widget.NewButton("Down", func() {
40     upButton.Disable()
41     downButton.Disable()
42     status.Text = "Going down ..."
43     status.Refresh()
44     progress.Show()
45     shutdownNAS()
46     go func() {
47       runner.Event(context.Background(), "sleep")
48     }()
49   })
50   downButton.Disable()
51   go func() {
52     for {
53       select {
54       case newState := <-stateReporter:
55         progress.Hide()
56         switch newState {
57         case "DOWN":
58           okIcon.Hide()
59           downIcon.Show()
60           upButton.Enable()
61         case "UP":
62           okIcon.Show()
63           downIcon.Hide()
64           downButton.Enable()
65         }
66         status.Text = newState
67         status.Refresh()
68       }
69     }
70   }()
71   img := canvas.NewImageFromResource(nil)
72   img.SetMinSize(
73     fyne.NewSize(400, 0))
74   grid := container.NewVBox(
75     img,
76     status,
77     okIcon,
78     downIcon,
79     progress,
80     container.NewHBox(
81       upButton,
82       downButton,
83       widget.NewButton("Quit", func() {
84         os.Exit(0)
85       }),
86     ),
87   )
88   w.SetContent(grid)
89   w.ShowAndRun()
90 }

The app features an application window with a label widget that displays the NAS status (UP or DOWN). In addition, there is an icon (check mark if the NAS is operational; X if not) and three buttons for user control. A progress bar also appears during the transition phases. After starting the program, initially only the Up button is active, while the Down button is grayed out (Figure 5). If you press a button to initiate an action, the app grays all buttons to prevent further impatient clicking triggering confusing actions.

Figure 5: Initially, the Down button is inactive.

The application causes some parts of the GUI to change dynamically with the program flow by making certain widgets disappear in some situations (the Hide() function is used for this) and reappear later on using Show(). The NAS status icon – either a check mark or an X – actually consists of two separate widgets okIcon and downIcon, but the app only displays one of them at any given time and keeps the other one tucked away invisibly.

The infinite progress bar is also always present in the application window. However, it is only visible and moving if an action is currently running (e.g., in the callback of the Up button starting in line 28). After line 33 calls sendMagicPacket() to send the packet that starts the NAS via the network, line 34 calls progress.Show() to display the progress bar. If the state of the machine changes, line 55 uses Hide() to make the progress bar visually disappear from the app again.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • GUI Apps with Fyne

    The Fyne toolkit offers a simple way to build native apps that work across multiple platforms. We show you how to build a to-do list app to demonstrate Fyne's power.

  • Straight to the Point

    With the Fyne framework, Go offers an easy-to-use graphical interface for all popular platforms. As a sample application, Mike uses an algorithm to draw arrows onto images.

  • Wheat and Chaff

    If you want to keep only the good photos from your digital collection, you have to find and delete the fails. Mike Schilli writes a graphical application with Go and the Fyne framework to help you cull your photo library.

  • Treasure Hunt

    A geolocation guessing game based on the popular Wordle evaluates a player's guesses based on the distance from and direction to the target location. Mike Schilli turns this concept into a desktop game in Go using the photos from his private collection.

  • Chip Shot

    We all know that the Fyne framework for Go can be used to create GUIs for the desktop, but you can also write games with it. Mike Schilli takes on a classic from the soccer field.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News