Control your backup NAS from the desktop
Programming Snapshot – Remote Backup
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.
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).
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.
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 }
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.
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
(incl. VAT)
Buy Linux Magazine
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.
News
-
Latest Cinnamon Desktop Releases with a Bold New Look
Just in time for the holidays, the developer of the Cinnamon desktop has shipped a new release to help spice up your eggnog with new features and a new look.
-
Armbian 24.11 Released with Expanded Hardware Support
If you've been waiting for Armbian to support OrangePi 5 Max and Radxa ROCK 5B+, the wait is over.
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.