Category: General

Living Room Forced-Airflow Old-Laptop-Rack Cabinet Build

Living Room Forced-Airflow Old-Laptop-Rack Cabinet Build

Building a Do-It-Yourself server rack, with old laptops as servers, airflow optimization, network and power distribution and low-noise ventilation for use in a living room while completely blending in and looking like a regular cabinet.

Introduction

What a title! Honestly – this is kind of hard to describe but you will get the idea. A few years ago I ditched my Chef-driven, hand-cooked Raspberry Pi farm and went for a more modern solution with less configuration. I wasted so much time trying to automate the software installation, OS maintenance and monitoring everything and yet couldn’t perform all the upgrades in time. It was terrible. So I moved to the everything is containerized operating system called “Rancher OS” and stacked those dusty old laptops I had lying around to a 2-server “Rack”.

Fast-forward 2 years – everything is running fine and the whole building and shipping happen on my workstation. No in-depth automation needed. When in use, the laptops got very hot and even switched themselves off as last resort. Even though the cabinet has no back wall there was just no good airflow. Leaving the door open did not help as well. Time for a better solution.

IMG_20180913_191835-e156f63b-5500-4aff-b932-50f4d5600bd2.jpg


Old cabinet: Over 29 °C with open back and front, laptops on the floor below, remains of the Raspberry Pi farm on the left

Taking measurements

I already have another one of those cabinets which is almost empty. I will transform this cheapo cabinet into a budget-friendly server cabinet. Before firing up the PC I take some measurements of the cabinet to bring it into my CAD software for further planning.

vlcsnap-2018-10-31-21h46m19s993-abe7d435-127e-4db4-8592-1f340de8af1d.png

3D Scan The Real Cabinet Into The CAD Software

Jk, it’s still 2018 so I have to re-create the cabinet manually using a Computer-Aided Drawing software. Being a complete beginner I heard a lot of good things about Autodesk’s Fusion 360 and already happily use Autodesk’s EAGLE software for PCB design so I will try this one out. They even let you register for the full version if you use it for non-commercial or low-volume purposes (Start-Up License) without any limits.

3D-CAD/CAM-Software für das Produktdesign | Fusion 360

Untitled-1c8b1625-d21d-465c-9f7f-1ee3bf0df830.png


First steps in Fusion 360: Creating the outer shell of the cabinet

Getting around in Fusion 360 is very easy and I did not need any tutorials or help. I’m totally amazed how easy it was. Autodesk has a very very in-depth tutorial series about absolutely everything, so if you feel like it, check it out on the Fusion 360 product page.

I will not cover every step of the creation of the 3D model. If you are interested in how I did the things I did or miss a step, you can inspect, download and change my project, which I released under an open-source license for you.

Server Cabinet

Untitled-30293efc-ba82-4669-a16f-665d67446cef.png


“Extruded” wooden block from the sketch above. Those are the outer measurements of the original cabinet

Untitled-108659ef-eaf0-4553-b59d-61649cf8e6b9.png


“Hollowed” the block and added a back wall. Thickness of the wood matches the original cabinet

Creation Of The Custom Back Wall And Cabinet Floors

Untitled-ebd6c0b3-c680-462a-ad8d-0d46b134e02b.png


Cutouts for the air inlet. Those have the size of 3 140×140 computer fan filters (only the filters) to reduce dust contamination

Untitled-a8c9e0a4-2e51-4e54-902c-c3f8e40112d0.png


Same for the fan air outlets on top and a cable inlet hole right above the air inlet

Alright. The basics of a server cabinet are there. Let’s now get to the really interesting part: Custom cabinet floors for each laptop model. To get the maximum cooling efficiency I will force the air through every air inlet of the laptops. There will be no chance of any heat jam in the cabinet itself and hot, used laptop air will be transported out of the cabinet right away. I have never seen something like this so let’s give it a try.

Untitled-19e0f0d4-7e23-45fc-9fb4-35ab2420a3d7.png
render-c6d1ca6b-d0d3-4f83-b401-73daa18fbf4b.png


Slightly outdated 3D render

Looks good to me. Now let’s go buy some wood. In Fusion 360 you can generate a drawing from your model. All the measurements are already known so you never have to type anything manually. Awesome. Let’s just put down some drawings and take these to the hardware store.

Untitled-c0feb212-08a6-48fc-9c45-9707ecaee64a.png


I did this for every new part I want to build. The only interesting measurements right now are the ones marked in red. I buy wood with the right thickness and dimensions and do all the cutouts with saws later by myself.

Now that I got myself some wood. Erm. Let’s just start doing the cutouts.

IMG_20180906_184719-ac9ef59b-aad2-4a5c-a340-f3dcd92f37c8.jpg


I use the jigsaw for all the rectangular and the three big fan outlet cutouts and a holesaw for the smaller, round ones.

20180903_193349-dcfc49f6-593d-46cb-b01c-8934b91d25e9.jpg
IMG_20180911_213322-7885416d-63d2-4b2f-b478-cbf8a9e991a8.jpg
IMG_20180911_213417-7813060e-b8fb-4fd2-acab-a8f2a42ca56a.jpg


The cable inlet gets an especially tight fit

IMG_20180911_215004-681abc7d-3e55-4b4b-b6c0-6683678e20df.jpg


I use some duct tape to fix the dust filters from the outer side to be able to easily dust it off if needed

IMG_20180911_220241-1f22e94c-617a-4b55-bd50-f0565a539649.jpg
IMG_20180911_220511-31445a39-278d-437c-8e74-7e04380433e6.jpg
IMG_20180909_144259-734f48ea-5596-45cc-9bfc-06039dd22bfb.jpg


After drilling new holes at the correct height for the cabinet floors, I can test if it fits. In case you wonder about that additional tiny hole in the back wall – The power connector of one laptop is in an awkward position and I have no chance to connect it from inside the cabinet. Looks weird, works well.

Adding Peripherals

IMG_20180911_200912-62041efb-9c59-4f25-b1e3-2359ce5b8742.jpg


Yes, I went shopping again.

I bought some peripherals to give the cabinet some life. The Amazon links below are affiliate links and you support me by clicking them and buying from there. Thanks for your support!

ARCTIC F14 PWM PST CO – 140 mm PWM PST Gehäuselüfter für Dauerbetrieb | Case Fan mit PST-Anschluss (PWM Sharing Technology) + Doppelkugellager | Reguliert RPM synchron

Novaato 2x Metall Kabeldurchführungen 60 mm Kabeldurchlass mit Bürste für mehr Ordnung auf dem Schreibtisch

ARCTIC Fan Grill – Lüfterabdeckung aus Stahl für 140 mm Lüfter I Lüftergitter Luftstrom-Durchlässig I Erhältlich in unterschiedlichen Größen

140mm PVC schwarze Computer PC Kühler Lüfter Lüftergitter Staubfilter Filtermatte Gehäuselüfter, 10 Stück

D-Link DGS-108 8-Port Layer2 Gigabit Switch (bis zu 2000 Mbit/s Datenübertragung pro Port, Non-Blocking-Architektur, lüfterlos, Metallgehäuse) schwarz

Brennenstuhl Eco-Line 6-fach Steckdosenleiste (mit Überspannungsschutz, Steckerleiste, Kindersicherung, Schalter und 5 m Kabel) anthrazit

The fans are especially interesting, because they are optimized for 24/7 operation and can be daisy-chained.

IMG_20180911_202405-71e34457-b51c-4faa-91c1-aae849080a1c.jpg
IMG_20180911_202937-082e6719-65ed-470a-968a-a98fb6fac7ba.jpg
IMG_20180911_203046_Bokeh-f0913180-e5a5-4abb-b78a-f6d6a3841f06.jpg
IMG_20180912_192852-bf45d37d-d786-4cf8-bc1d-b62f689b264d.jpg


After drilling holes for the fans, I use screws and nuts to fix the fan on the inside and the fan grill on the outside

IMG_20180912_192743_Bokeh-fe3eebd0-ba37-4a8e-9504-96b5cffdd907.jpg
IMG_20180912_201436-21d09be0-1fce-4e3f-bac4-06185bc069c5.jpg


Daisy-chained fans on the back wall and the turned-around cabinet behind it

IMG_20180912_202538-9b6cf1c8-d68f-4b43-a477-97e6c3616081.jpg


Nailed it

IMG_20180912_203127-dbd1f830-1a83-4fa0-954a-479bfae3af4c.jpg
IMG_20180913_182333-3d555208-31a5-4d26-96e3-a8a46bba1043.jpg


Add power and network

The network cable comes directly from the central switch. The 12 V fans are powered by a 9 V power supply to keep the noise low. The power comes from a discrete output of a UPS which can supply the whole cabinet (together with the rest of the infrastructure) for about 20 minutes allowing for a clean shutdown for all servers.

https://hub.docker.com/r/gersilex/apcupsd

My Docker image to shut down the Docker host from an APCUPSd container

APC Back-UPS BX – Unterbrechungsfreie Stromversorgung 700VA, BX700U-GR (AVR, 4 Schuko Ausgänge, USB) schwarz

Amazon affiliate link: The UPS I use, for a clean power output to the infrastructure. Worth it.

Alright. Now I have a stable network and power connection and I can even hear the air being pulled inside when closing the door. I think it pulls a lot of air through the cracks of that cheap cabinet.

IMG_20180913_182643-13d97aec-4c61-40a1-a826-41565b8a8de9.jpg
IMG_20180913_182650-23e01d34-32bf-40a2-b918-075883ddafe0.jpg
IMG_20180913_182637-ed76d404-7328-42ec-ae69-30eabc5ebdee.jpg


I use Door/Window insulation foam band and put it into or behind all the cracks

IMG_20180913_204354-8e10b905-3d2e-48eb-b4e4-56773102f106.jpg
IMG_20180913_183832-237787e2-8d6b-4c6c-aa6e-3a7d87c6cfe4.jpg


Just the usual cabinet. Or is it? It is. Or is it?

Conclusion

I can barely hear the new cabinet. In fact – it’s quieter than before because of the closed system. And thanks to the airflow and the cold air the laptop fans don’t need to spin high at all. Win-win.

IMG_20181101_003710-61a83da4-616d-4c46-a0b3-c0c86dd0a86c.jpg


The precise trusty old Chinese Alecto thermometer knock-off confirms our success. We now have about 1 °C ambient-to-cabinet difference as opposed to the 8 °C from before.

What’s Next?

I expect to add one or two more laptops and maybe some other small electronics. I will test with a pegboard next, which is basically all holes. This will put the directed cooling method I used in contrast with an overall cooling airflow concept. I will definitely report back as soon as I have new information.

I hope you enjoyed my biggest project to date. Have a great one!

Rocketbeans Lamp – Update #1

Rocketbeans Lamp – Update #1

Hey guys,
recently Rocketbeans updated their Website and also updated the API Endpoint for our beautiful lamp.

If you haven’t read about the Smart RGB-LED Lamp before you better take a look at my older Post here.

Changes

  • The API endpoint changed to https://api.rocketbeans.tv/v1/frontend/init
  • Added all possible colors (Cyan, Yellow and Purple)
  • Added error signals
    • Cyan = If JSON parsing failed
    • Purple = If show type not matching [“rerun”, “preview”, “live”]
    • Yellow = If HTTP connection failed
  • Added a switch to enable or disable error signals
  • I needed to dig deeper into the JSON to get the current show type

Code

As usual you are able to watch or star my Github repository to keep updated.

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

/*
* Coniguration
** CONNECTION_INDICATOR - blinking led during startup when defined
** ERROR_INDICATOR - blinking led sequence when parsing or http error occured
*/
#define CONNECTION_INDICATOR
#define ERROR_INDICATOR
#define LED_RED GPIO_NUM_14
#define LED_GREEN GPIO_NUM_27
#define LED_BLUE GPIO_NUM_26

#define WIFI_SSID "Your Wifi"
#define WIFI_PASSWORD "Your Wifi Password"
#define ROCKETBEANS_API "https://api.rocketbeans.tv/v1/frontend/init"

/*
* Information
*/
#define __product__ "Beanlight"
#define __version__ "1001DEV"
#define __author__ "Marvyn Zalewski <[email protected]>"
#define __copyright__ "(c) 2018 KeyboardInterrupt.org"

/*
* Global Variables
*/
int status = WL_IDLE_STATUS;

/*
* Functions
*/
void setLampToBlack()
{
    digitalWrite(LED_RED, LOW);
    digitalWrite(LED_GREEN, LOW);
    digitalWrite(LED_BLUE, LOW);
}

void setLampToBlue()
{
    digitalWrite(LED_RED, LOW);
    digitalWrite(LED_GREEN, LOW);
    digitalWrite(LED_BLUE, HIGH);
}

void setLampToGreen()
{
    digitalWrite(LED_RED, LOW);
    digitalWrite(LED_GREEN, HIGH);
    digitalWrite(LED_BLUE, LOW);
}

void setLampToCyan()
{
    digitalWrite(LED_RED, LOW);
    digitalWrite(LED_GREEN, HIGH);
    digitalWrite(LED_BLUE, HIGH);
}

void setLampToRed()
{
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GREEN, LOW);
    digitalWrite(LED_BLUE, LOW);
}

void setLampToPurple()
{
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GREEN, LOW);
    digitalWrite(LED_BLUE, HIGH);
}

void setLampToYellow()
{
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GREEN, HIGH);
    digitalWrite(LED_BLUE, LOW);
}

void setLampToWhite()
{
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GREEN, HIGH);
    digitalWrite(LED_BLUE, HIGH);
}

/*
* Init
*/
void setup()
{
    Serial.begin(115200);
    Serial.println(__product__);
    Serial.println(__version__);
    Serial.println(__author__);
    Serial.println((String)__copyright__ + "\n");
    pinMode(LED_RED, OUTPUT);
    pinMode(LED_BLUE, OUTPUT);
    pinMode(LED_GREEN, OUTPUT);
    while (status != WL_CONNECTED)
    {
        Serial.println((String)__product__ + " attempting to connect to Wifi network, WIFI_SSID: " + (String)WIFI_SSID);
        status = WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
        #ifdef CONNECTION_INDICATOR
        setLampToRed();
        delay(2000);
        setLampToBlue();
        delay(2000);
        setLampToGreen();
        delay(2000);
        #else
        delay(6000);
        #endif
    };
    #ifdef CONNECTION_INDICATOR
    for (int i = 0; i <= 3; i++)
    {
        setLampToGreen();
        delay(250);
        setLampToBlack();
        delay(250);
    };
    #endif
    Serial.println((String)__product__ + " connected to network");
}

void loop()
{
    if ((WiFi.status() == WL_CONNECTED))
    {
        HTTPClient http;
        http.begin(ROCKETBEANS_API);
        int httpCode = http.GET();
        if (httpCode > 0)
        {
            const size_t bufferSize = JSON_ARRAY_SIZE(4) + JSON_ARRAY_SIZE(8) + 2 * JSON_OBJECT_SIZE(2) + 10 * JSON_OBJECT_SIZE(3) + 5 * JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(8) + 1190;
            DynamicJsonBuffer jsonBuffer(bufferSize);
            String payload = http.getString();
            JsonObject &root = jsonBuffer.parseObject(payload);
            JsonObject &data = root["data"];
            JsonObject &data_streamInfo = data["streamInfo"];
            JsonObject &data_streamInfo_info = data_streamInfo["info"];
            String data_streamInfo_info_type = data_streamInfo_info["type"].as<String>();

            if (!root.success())
            {
                Serial.println((String)__product__ + " parsing failed.");
                Serial.println(payload);
                #ifdef ERROR_INDICATOR
                for (int i = 0; i <= 6; i++)
                {
                    for (int i = 0; i <= 3; i++)
                    {
                        setLampToCyan();
                        delay(100);
                        setLampToBlack();
                        delay(100);
                    };
                    delay(500);
                }
                #endif
                setLampToCyan();
            } else
            {
                /* When current show is live */
                if (data_streamInfo_info_type == "live")
                    setLampToRed();
                /* When current show is new */
                else if (data_streamInfo_info_type == "premiere")
                    setLampToBlue();
                /* When current show is playback */
                else if (data_streamInfo_info_type == "rerun")
                    setLampToWhite();
                /* Graceful handling if no type matched */
                else {
                    Serial.println((String)__product__ + " unsupported show type -> "+ data_streamInfo_info_type +".");
                    #ifdef ERROR_INDICATOR
                    for (int i = 0; i <= 3; i++)
                    {
                        for (int i = 0; i <= 3; i++)
                        {
                            setLampToPurple();
                            delay(100);
                            setLampToBlack();
                            delay(100);
                        };
                        delay(500);
                    }
                    #endif
                    setLampToPurple();
                }
            }
        }
        else
        {
            Serial.println((String)__product__ + " error on HTTP request");
            #ifdef ERROR_INDICATOR
            for (int i = 0; i <= 3; i++)
            {
                for (int i = 0; i <= 3; i++)
                {
                    setLampToRed();
                    delay(100);
                    setLampToBlack();
                    delay(100);
                    setLampToYellow();
                    delay(100);
                    setLampToBlack();
                    delay(100);
                };
                delay(500);
            }
            #endif
            setLampToYellow();
        }
        http.end();
    }
    delay(60000);
}
iPXE Boot With Shoelaces

iPXE Boot With Shoelaces

Hey guys,
today I’d like to tell you about Shoelaces from Thousandeyes. Shoelaces may be used to automate Server installation with iPXE Boot.
If you are already familiar with the basics about PXE, iPXE and how to set up the environment you can go ahead to Shoelaces Setup.

Environment

Before I dig into the details I prepared an Image to show you the Software Stack and also the whole server set up.

Software Stack and Server Setup

All commands are executed on a server named pxe-server.local with IP 192.168.0.2 running on CentOS 7.4.

DHCPD (ISC DHCP)

The DHCP Server is the first line of the stack. As soon as the Client boot in PXE Mode, it requests an IP Address and receives an IP address with the command where it can find the Image to boot from.
I prepared an example configuration file below.

[email protected]:[~]: cat /etc/dhcp/dhcpd.conf
subnet 192.168.0.0 netmask 255.255.255.0 {
  range 192.168.0.10 192.168.0.20;
  option subnet-mask 255.255.255.0;
  option routers 192.168.0.1;
  option domain-name-servers 192.168.0.1;
  next-server 192.168.0.2;
  if exists user-class and option user-class = "iPXE" {
    filename "http://192.168.0.2/poll/1/${netX/mac:hexhyp}";
  } else {
    filename "undionly.kpxe";
  }
}

As you can see the DHCP Server manages the IP Range 192.168.0.10-20 and also sends the next-server option which tells the client to boot from 192.168.0.2:69/filename (TFTP Server) option. If the Client supports iPXE it boots from http://192.168.0.2/poll/1/${netX/mac:hexhyp} which is Shoelaces. If the client doesn’t support iPXE it boots a microkernel which supports iPXE.

You can test your DHCP Settings with nmap


[email protected]:[~]: nmap --script broadcast-dhcp-discover
 
Starting Nmap 6.40 ( http://nmap.org ) at 2018-08-06 12:58 UTC
Pre-scan script results:
| broadcast-dhcp-discover:
|   IP Offered: 192.168.0.10
|   DHCP Message Type: DHCPOFFER
|   Server Identifier: 192.168.0.2
|   IP Address Lease Time: 0 days, 0:05:00
|   Subnet Mask: 255.255.255.0
|   Router: 192.168.0.1
|_  Domain Name Server: 192.168.0.1
WARNING: No targets were specified, so 0 hosts scanned.
Nmap done: 0 IP addresses (0 hosts up) scanned in 0.07 seconds

TFTP

The TFTP Server is needed to serve the undionly.kpxe when the client doesn’t support iPXE.

To “install” the microkernel you need to download the file from ipxe.org and save it into /var/lib/tftpboot.

[email protected]:[~]: wget -P /var/lib/tftpboot https://boot.ipxe.org/undionly.kpxe

Xinetd

You need Xinetd to actually serve the microkernel.

Here is the config file we used to glue tftp and Xinetd together:

[email protected]:[~]: cat /etc/xinetd.d/tftp
{
    socket_type     = dgram
    protocol        = udp
    wait            = yes
    user            = root
    server          = /usr/sbin/in.tftpd
    server_args     = -s /var/lib/tftpboot
    disable         = no
    per_source      = 11
    cps             = 100 2
    flags           = IPv4
}

As soon as you created the config file you need to restart xinetd and check if the tftp module is running.

[email protected]:[~]: systemctl status xinetd.service
● xinetd.service - Xinetd A Powerful Replacement For Inetd
   Loaded: loaded (/usr/lib/systemd/system/xinetd.service; enabled; vendor preset: enabled)
   Active: active (running) since Mo 2018-08-11 15:19:41 UTC; 3s ago
  Process: 8060 ExecStart=/usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid $EXTRAOPTIONS (code=exited, status=0/SUCCESS)
 Main PID: 8061 (xinetd)
   CGroup: /system.slice/xinetd.service
           └─8061 /usr/sbin/xinetd -stayalive -pidfile /var/run/xinetd.pid
 
Aug 11 15:19:41 pxe-server xinetd[8061]: removing daytime
Aug 11 15:19:41 pxe-server xinetd[8061]: removing discard
Aug 11 15:19:41 pxe-server xinetd[8061]: removing discard
Aug 11 15:19:41 pxe-server xinetd[8061]: removing echo
Aug 11 15:19:41 pxe-server xinetd[8061]: removing echo
Aug 11 15:19:41 pxe-server xinetd[8061]: removing tcpmux
Aug 11 15:19:41 pxe-server xinetd[8061]: removing time
Aug 11 15:19:41 pxe-server xinetd[8061]: removing time
Aug 11 15:19:41 pxe-server xinetd[8061]: xinetd Version 2.3.15 started with libwrap loadavg labeled-networking options compiled in.
Aug 11 15:19:41 pxe-server xinetd[8061]: Started working: 1 available service
[email protected]:[~]: netstat -tulpen | grep xinetd
udp        0      0 0.0.0.0:69              0.0.0.0:*                           0          235681     8061/xinetd

When you execute the status command at the end of the log should be at least ‘1’ available service printed. The Port 69/UDP should be open as well.

Shoelaces

Now we’re running the basic iPXE Environment. At the moment we are able to boot a client with PXE boot, obtain an IP Address and boot the iPXE microkernel if the client doesn’t support iPXE.

But what happens if the client boot with this line (which we configured in the dhcpd section)?
http://192.168.0.2/poll/1/${netX/mac:hexhyp}
Before answering the question I’ll guide you through the shoelaces installation.

Installation

Shoelaces is a program written in Go Lang. Therefore we need to install go at first.

# Determine your needed go version at https://golang.org/dl/#stable first. I need the amd64 binary which is version 1.10.3 at the moment.
[email protected]:[~]: wget https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
[email protected]:[~]: tar -xvf go1.10.3.linux-amd64.tar.gz
[email protected]:[~]: mv go /usr/local
[email protected]:[~]: echo "export GOROOT=/usr/local/go" >> /etc/bashrc
[email protected]:[~]: echo "export GOPATH=/opt/go" >> /etc/bashrc
[email protected]:[~]: echo "export PATH=$GOPATH/bin:$GOROOT/bin:$PATH" >> /etc/bashrc
[email protected]:[~]: src /etc/bashrc

To install shoelaces you need to get the repository with go get then you need to build the shoelaces binary.

[email protected]:[~]: go get github.com/thousandeyes/shoelaces
[email protected]:[~]: cd $GOPATH/src/github.com/thousandeyes/shoelaces
[email protected]:[~]: go build

Daemonize

Currently, we’re only able to start the service on demand with a command.

[email protected]:[~]: cd $GOPATH/src/github.com/thousandeyes/shoelaces
[email protected]:[~]: ./shoelaces -config configs/shoelaces.conf

But if you want the process permanently running you need to create a systemd file.
Luckily I already created one which needs a user to be created.

[email protected]:[~]: useradd shoelaces
[email protected]:[~]: cat /lib/systemd/system/shoelaces.service
[Unit]
Description=Shoelaces
Documentation=https://github.com/thousandeyes/shoelaces
ConditionPathExists=/opt/go/src/github.com/thousandeyes/shoelaces
After=network.target

[Service]
User=shoelaces

# Restart Policy
Restart=on-failure
RestartSec=10

# Execution
ExecStartPre=/bin/chown -R shoelaces /opt/go/src/github.com/thousandeyes/shoelaces
ExecStartPre=/bin/chmod u+x /opt/go/src/github.com/thousandeyes/shoelaces/shoelaces
ExecStart=/opt/go/src/github.com/thousandeyes/shoelaces/shoelaces -config configs/shoelaces.conf

# Environment
WorkingDirectory=/opt/go/src/github.com/thousandeyes/shoelaces
PermissionsStartOnly=true
NonBlocking=true

# Logging
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=shoelaces

[Install]
WantedBy=multi-user.target

After you added the content to the file /lib/systemd/system/shoelaces.service execute systemctl daemon-reload; systemctl start shoelaces.service; systemctl status shoelaces.service to start the service and check the status. You can always take a look into logs if you execute journalctl -f -u shoelaces.service.

Configure

I guess you already noticed the only missing part. Configuring Shoelaces.

It’s straight forward and fairly easy. Just go into the shoelaces directory and take a look at the default config file.

[email protected]:[~]: cd $GOPATH/src/github.com/thousandeyes/shoelaces
[email protected]:[/opt/go/src/github.com/thousandeyes/shoelaces]: cat configs/shoelaces.conf
port=8081
domain=localhost
data-dir=configs/example-templates-configs/
template-extension=.slc
mappings-file=mappings.yaml
debug=true

For production I changed the config file to the following one …

[email protected]:[/opt/go/src/github.com/thousandeyes/shoelaces]: cat configs/shoelaces.conf
port=8080
domain=192.168.0.2
data-dir=configs/templates-configs/
template-extension=.slc
mappings-file=mappings.yaml
debug=true

… and moved configs/example-templates-configs to configs/templates-configs.

Starting

Recently we changed the config file. Therefore you need to restart your service.

Now open your shoelaces URL (http://<domain>:<port>) with your favourite browser. If everything went well you should see the following website. If not, take a look into the logs what’s wrong with shoelaces.

Shoelaces Startup Image

Deep Dive

Back to the question before the shoelaces installation.

But what happens if the client boot with this line (which we configured in the dhcpd section)?
http://192.168.0.2/poll/1/${netX/mac:hexhyp}

The client connects to shoelaces and kept in a waiting loop until the user executes a “Boot!” command or shoelaces is able to bootstrap the server with ‘mappings’. This state looks like this:

Server Registered

As soon as you click on ‘Select an iPXE script’ you are able to choose an iPXE script located in $GOPATH/src/github.com/thousandeyes/shoelaces/configs/templates-configs/ipxe.

I clicked on centos.ipxe and filled out both arguments. If you check $GOPATH/src/github.com/thousandeyes/shoelaces/configs/templates-configs/ipxe/centos.ipxe.slc there are two strings which look like {{.<STRING>}}. These strings are the arguments shown in the UI.

[email protected]:[~]: cat $GOPATH/src/github.com/thousandeyes/shoelaces/configs/example-templates-configs/ipxe/centos.ipxe.slc
{{define "centos.ipxe" -}}
#!ipxe
set hostname {{.hostname}}
set release {{.release}}
set base http://mirror.centos.org/centos/${release}/os/x86_64

echo This automatically overwrites data!
echo CentOS ${release}
echo Installing ${hostname}

kernel ${base}/images/pxeboot/vmlinuz initrd=initrd.img repo=${base} ks=http://{{.baseURL}}/configs/centos.ks?hostname=$hostname&release=$release
initrd ${base}/images/pxeboot/initrd.img
boot
{{end}}

As soon as I click on the big red button “Boot!” the client leaves the waiting loop and starts to execute the iPXE script. It depends on your kickstart file how long it takes until your client is back again but normally it should’nt take longer than 10 minutes.

Conclusion

Shoelaces is a very small footprint software written in go which supports bootstrapping a huge amount of server. Even in a small environment, it helps a lot. Take a look at their Github Project Site if you want to discover all further features.

For example you may use the mapping feature if you want to bootstrap server automatically based on IP or Hostname pattern.

Leave a comment or contribute to the project if you like the software.

Our First Anniversary

Our First Anniversary

Hey guys,

We reached our first anniversary. For me, it was an awesome year and I learned a lot.

I prepared some anniversary facts:

Thank you so much for reading our blog.

 

Title Image via LINK

Modify a battery-powered speaker to be a permanent DC-powered speaker

Modify a battery-powered speaker to be a permanent DC-powered speaker

In my first tech-video I explain how I tranform the JKR KR-1000 – a battery-powered speaker with included charging circuits – to a classic DC-powered speaker, bypassing the battery circuits completely. This allows for permanent use of this speaker without the battery-protection circuits triggering and cutting off the power.

Feedback is greatly appreciated.

Buy the speaker on AliExpress.com // Amazon.de // Amazon.com // Gearbest.com // Ebay.com

(All links are clean search links and don’t bring us any money.)

Google CTF 2018 – Web: Translate

Google CTF 2018 – Web: Translate

Hey guys,

I decided to spend some time in solving CTF’s. To be honest, this CTF is the second I solved until yet. Recently I read about the Google CTF which may be a good starting point.

Normally the Google CTF is an event which lasts for two days. The best teams will be rewarded. You can read more about on their Blog.

Anyway, without knowing any of the Challenges I started with the ‘Web’ CTF challenges because this is the environment I know the best. The first challenge is named ‘Translate’.

The task for this CTF is to gather the content of the File named ./flag.txt. There’s also a hint in the task description which says that the app is client-side rendered but not in the browser. This hint told me to watch out for a rendering language. It may be a Python-based rendering-engine like Jinja2 or a Javascript framework like Angular, AngularJS or React.

Information Gathering

I downloaded the file, unzipped it and got an HTML named index.html.

[email protected]:[/ctf]: cat index.html

<script>location=’http://translate.ctfcompetition.com:1337/'</script>

The index.html only redirect the browser to a new location which is http://translate.ctfcompetition.com:1337/. Therefore I open Chrome and open the URL.

As you can see the Webapp is a simple translation utility which is able to translate from english to french or vice versa.

But how it works? I checked the debug translations site and noticed that the app is a simple Key-Value database.

I noticed the {{UserQuery}} string in both dictionaries. The double curly braces are used in Jinja2 or Angular. I started the Chrome observer to check the source.

What a luck! There is an unrendered ng-bind declaration which approves Angular. 🙂

It seems like there’s an object named i18n which have a method named word. I guess it is used to translate a word from one language to the other one.

Let’s check a bit more of the web app if we find some other useful information. I opened the add words link and a wild form appeared.

This may be a security issue in combination with a server-rendered user input. I wanted to make sure I understood how the web app work. So I decided to add a new word and check if it will be translated correctly.

As expected my word was translated correctly.

When I entered a new word I noticed the HTTP GET parameters which are passed into the application.

http://translate.ctfcompetition.com:1337/add?lang=fr&word=moi&translated=my

I wondered if the web app also supports a different language than English or French. So I changed lang=fr to lang=de.

Boom! An unhandled error with stack trace.

Error: ENOENT: no such file or directory, open ‘./i18n/de.json’
   at Object.fs.openSync (fs.js:646:18)
at Object.fs.readFileSync (fs.js:551:33)
at Object.load (/usr/local/chall/srcs/restricted_fs.js:24:20)
at app.get (/usr/local/chall/srcs/server.js:175:57)
at Layer.handle [as handle_request
(/usr/local/chall/node_modules/express/lib/router/layer.js:95:5)
at next (/usr/local/chall/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch     (/usr/local/chall/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request]   (/usr/local/chall/node_modules/express/lib/router/layer.js:95:5)
at /usr/local/chall/node_modules/express/lib/router/index.js:281:22
at Function.process_params   (/usr/local/chall/node_modules/express/lib/router/index.js:335:12)

It seems like the translations are saved on the server side in ./i18n/(fr|en).json.

Exploiting

Now I tried to figure out if I’m able to execute the template on the server side. I already discovered that i18n is the object which I need to exploit.

So I added a new word hackme with the value {{ i18n.word(“moi”) }}.

Checking the word results in a plain string.

Anyway, I remembered the string which rendered into the website. It simply replaced the {{userQuery}} with the translated word I entered before. So I went back to the translation dump copied the key of the string which contains the curly braces (in_lang_query_is_spelled).

I changed the value of in_lang_query_is_spelled to {{ i18n.word(“moi”) }} and checked the output of translate which is already defined.

To my surprise, the template injection did the job.

http://translate.ctfcompetition.com:1337/?query=translate&lang=fr

Since I know the i18n is an object and it’s also in scope I tried to figure out which methods it has.

Let us try if we are able to iterate through the object. I changed the key in_lang_query_is_spelled to <p *ngFor=”let child of this.i18n”>{{ child }}</p>. But this command returns simply nothing. So I read through the AngularJS documentation to figure out how it works there. Accordingly I changed in_lang_query_is_spelled to <p ng-repeat=”child in this.i18n”>{{ child }}</p>. Awesome! I got the available methods.

I noticed there is a seconds method available called forTemplate(t). I guess the function may be template because it’s the same for myI18n.forSingleWord(w) and word.

With the latest knowledge I changed in_lang_query_is_spelled to {{ this.i18n.template(“fr.json”) }} which results in an error.

I noticed the path which is ./fr.json. As I told you at the beginning we’re looking for a file named ./flag.txt. And again I changed in_lang_query_is_spelled to {{ this.i18n.template(“./flag.txt”) }} and opened http://translate.ctfcompetition.com:1337/?query=translate&lang=fr.

Look, the flag smiles at me.

CTF{Televersez_vos_exploits_dans_mon_nuagiciel}

Conclusion

I hope you enjoyed my adventure through this CTF. If you know a better way to find the flag, leave a comment. 🙂

See you soon.

Credits

  • Title Image via LINK
Portable HDMI Screen Using Your Smartphone

Portable HDMI Screen Using Your Smartphone

When it comes to Raspberry Pies, it’s all fun and games until one of them doesn’t boot anymore. If you are lucky, you can attach your TV or a Computer screen to the HDMI or Composite port and watch the boot messages. My recent routine got a little uncomfortable as I tend to have my devices in a stacking enclosure and cannot carry them to the TV. It now mostly got like this:

  1. Power off the problematic Raspberry Pi
  2. Power off the Hyperion (Ambilight-Clone) Raspberry Pi attached behind the TV
  3. Change the cabling of the TV to output Hyperion’s boot messages.
  4. Swap the SD cards
  5. Power it up and watch the boot messages of the problematic Raspberry Pi’s SD Card in the Hyperion Pi
  6. Write it down
  7. Try to fix it
  8. Swap & repeat

Wow. This takes a lot of time and only works with the same hardware. This was no permanent solution anymore. But where can I get a small HDMI-capable screen, ideally battery-powered and cheap? Everything I found were some DVD-Players with HDMI input, starting at around 100 European bucks or car reverse cameras at 12 Volts with open-ended cables or lighter plugs. Both are weird and too expensive. But wait a second: While Ebaying for these devices I noticed I actually am staring at such a screen right now and it’s even battery-powered!

DIY HDMI Screen

Alright, we can’t have it completely free, but we don’t need a new display-and-battery whatever. Look at this:

Different USB video grabbers
Different USB video grabbers

I recently found a very cheap HDMI USB grabber on Ebay. If you already built an Ambilight clone, you may recognize the hardware on the center and right hand side. They are a combination of a (2) Composite USB Grabber and a (3) HDMI to Composite Converter, which – in combination – resemble a pretty ugly HDMI USB Grabber. The Converter even needs extra power. That’s not a good portable solution. Also the quality of the image sucks, because it is a digital signal converted to 480p analog video and then back to HDMI (still at 480p, because the pixels are lost forever).

There is an (1) All-In-One solution available now. Unfortunately it doesn’t have better specs, but it does not need extra power and is a lot smaller and handier. I took it apart and it uses almost the same circuitry as the combination of (2) and (3). It even converts it down to analog and back to digital, but without the signal loss of the connectors and cables:

USB HDMI grabber disassembled
USB HDMI grabber disassembled

Some of the chips are unmarked. I appreciate any identification hints.

If you don’t use a tablet with an USB A port, you might also need a microUSB to USB A (or USB C to USB A, if you are not using a 2014 phone) adapter. They are very cheap and (at least in the case of Micro-USB) only consist of wire traces without any logic or ICs. Some of mine look like this:

Smartphone, USB OTG Adapter, USB HDMI Grabber
Smartphone, USB OTG Adapter, USB HDMI Grabber
Smartphone, USB OTG Adapter, USB HDMI Grabber
Smartphone, USB OTG Adapter, USB HDMI Grabber
USB OTG Adapter, USB HDMI Grabber plugged into the Smartphone
USB OTG Adapter, USB HDMI Grabber plugged into the Smartphone

 

Fits acceptably. Now we still need a piece of software to view the grabber’s video input. All of my tested grabbers use EasyCap-compatible chipsets. The modules are usually included in the linux-firmware and work with any Video-For-Linux compatible software. If you use an Android smartphone there are multiple apps that work just fine. I chose “USB Camera” by ShenYao China which is free but shows banner advertisements.

Let’s plug in a Raspberry Pi and start it up:

The video quality is pretty bad. I recommend to reduce the default HDMI resolution of the Raspi to something lower to increase readability. After that, and with the pinch-to-zoom functionality of the app, I can now use it for debugging misbehaving hardware without the hassle of disconnecting and moving stuff. Great!

I also switched to this USB HDMI Grabber for my Hyperion-based Ambilight clone. It produces absolutely no green flashes (grabber dropouts) and saves a lot of space behind the TV. Do you have any other applications in mind? Let me know in the comments below!

Links

USB HDMI Grabber

MicroUSB OTG Adapter

Raspberry Pi Docs

Google Play Store Apps

Save your SD cards: Raspberry Pi on a network file system

Save your SD cards: Raspberry Pi on a network file system

If you work a lot with your Raspberry Pi, you probably have burned one or another SD card. While SD flash cards are great for storing photos or music, they suck at being written to very often. This is exactly the case if you run a whole operating system on them. Hundreds of thousands of read and write operations wear your SD cards out and if you do not have a solid backup concept you have probably lost some data, too.

In my home, a couple of systems are running on the cheap, embedded hardware by the Raspberry Pi Foundation because they are both small and power-saving at a very affordable price point. And so are the accessories like cases and add-on-boards. At the time of writing, these are the jobs that are done by my Raspberries:

  • SSH server (as gateway and tunnel from the internet into the LAN)
  • Monitoring
  • OpenHAB smart home controller
  • TV Server
  • Backup
  • Ambilight

 

When living alone, having all this run on SD card-based Raspberries is fine and you know what to do when something breaks. As soon as you have a partner to share your home with, you probably need to agree on an SLA to ensure a high WAF:

SLA (Service Level Agreement):

If you want to have a smart home controlled by a computer, you have a very short acceptable “TTR” if something breaks. Our Time-To-Recover is usually as short as 1 day. Honestly, it is more like same-day. Do not go to sleep until it is fixed.

WAF (Wife Acceptance Factor):

If you stick to the SLA, you are free to do all this crazy stuff; your partner accepts it and maybe even likes it.

 

And that is basically the reason I am writing this. It is completely OK if the ambilight stops working, it is also acceptable if the backups do not run for some days. But if the smart home controller fails, … well, you better have a backup plan. You do not want to deal with someone who spent two hours trying to turn the lights up or the television on and ultimately failed because it simply does not work anymore without the central controlling instance.

So we should reduce these outages to a minimum:

The Objective

  • Increase device uptime / reduce device outages
  • save data on a central storage for easy backups or snapshots

 

There are so many other things that can fail. The processor may overheat and hang, forcing a reboot by the watchdog, the power consumption could be too high and disable the USB bus (including the Ethernet Port), the cheap phone charger you use for powering may fail as well, but the single biggest problem is – without a doubt – the SD card.
So today I am showing you how simple it is to boot and run your Pi completely from the network.

 

The Requirements

  • always-on network storage with NFS server software
  • stable network connection to the storage (wired whenever possible)
  • 16+ MB SD card for the Raspberry Pi bootloader and config file that points to your network storage
    (Note: Starting with the Raspberry Pi 3 there are ways to modify the internal bootloader to boot from network without an SD card. I am not going to cover this here; this method will work with all the Raspberry Pi versions)
  • 3 beers of time

Everything I do is based on a Linux computer (I use Arch Linux and Manjaro) but you can also do this on OS X and maybe on Windows with a lot of tools. I really recommend you to use a Linux computer or a linux VM.

The Network Storage

You need a network storage capable of running an nfs-server. You can even use a Raspberry Pi as sever but that would not really solve the problem here, would it? 😉

Ensure the NFS service is globally enabled

Most commercial NAS systems support NFS and so does my Seagate BlackArmor NAS 4000. I highly recommend to create a seperate share for the filesystems of your Raspberries to be able to add stricter permissions later.

You may need to enable the NFS service first, because a lot of people do not need it and use CIFS only.

Create a share where you will put the filesystems for your Raspis

The resulting config, that my NAS generates, looks like this:

~ $ grep rootfs /etc/exports
/nfs/rootfs *(rw,async,insecure,no_root_squash,no_subtree_check)

Please note that the options rw, no_root_squash and no_subtree_check are obligatory for what we want to achieve. For a detailed description of all possible options, please consult the man page on your system or here.

 

The Extraction Of The SD Card Image Content

You have your NFS Server up and running and configured.

I already stumbled across some distributions of Raspberry Pi SD card images, that depend on and wait for partitions on the local sd card – even if you boot them from NFS. If you see your Raspberry Pi failing at boot, because it waited for a local partition or device for too long, simply flash the sd card image onto that sd card like you would normally. Then, change the settings to have it boot over NFS. Your local SD card still will not be touched, but the boot scripts and dependencies will work now.

This topic can be split into two subtopics: Using a freshly downloaded image to start from scratch, or converting a normal SD-card-based Raspberry Pi to a NFS-based one. Skip to Convert SD-Card-Based Raspberry Pi To NFS if you already have an SD card image, which you want to convert.

 

Create A RootFS From Scratch (New Download)

Now you can download any SD card image like you would do normally. But instead of flashing it onto an SD card, you will mount it and copy the content to your NFS share.

In this example I will download the official Raspberry Pi Foundation Raspbian Stretch Lite Image over the Torrent protocol on my Manjaro Linux machine.

[[email protected] Downloads]$ transmission-cli https://downloads.raspberrypi.org/raspbian_lite_latest.torrent
2017-09-07-raspbian-stretch-lite.zip: State changed from "Incomplete" to "Complete"

Be nice and give something back to the torrent community by leaving this open seeding (uploading) for the other downloaders. Meanwhile in another terminal window:

Check the checksum to check if the data was downloaded successfully and was not modified:

[[email protected] Downloads]$ sha256sum 2017-09-07-raspbian-stretch-lite.zip 
bd2c04b94154c9804cc1f3069d15e984c927b750056dd86b9d86a0ad4be97f12 2017-09-07-raspbian-stretch-lite.zip

The checksum equals the checksum from the website. Great! Let’s unpack it and have a look inside the partition table. It is a complete device image, so it contains everything from (or for) the SD card, including partition table.

[[email protected] Downloads]$ unzip 2017-09-07-raspbian-stretch-lite.zip 
Archive: 2017-09-07-raspbian-stretch-lite.zip
 inflating: 2017-09-07-raspbian-stretch-lite.img
[[email protected] Downloads]$ fdisk -l 2017-09-07-raspbian-stretch-lite.img 
Disk 2017-09-07-raspbian-stretch-lite.img: 1.7 GiB, 1854590976 bytes, 3622248 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
                                ^^^-----<<< write this down! <<<-------
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x11eccc69

Device                           Boot Start  End      Sectors Size Id Type
2017-09-07-raspbian-stretch-lite.img1 8192   93813    85622   41.8M c W95 FAT32 (LBA)
2017-09-07-raspbian-stretch-lite.img2 94208  3622247  3528040 1.7G 83 Linux
                                      ^^^^^-----<<< and this! <<<-------

In the output of fdisk -l you see some important information. As we are not interested in putting the boot partition onto our NFS server (but the rootfs partition, the second partition) instead, we need to know the offset where the interesting partition starts. To get this offset you simply multiply the sector size (usually always 512 bytes) with the start sector of the second partition (both are marked above).

[[email protected] Downloads]$ echo $(( 512 * 94208 ))
48234496

Let’s create a loopback device with this information so we can mount it and steal the data from it.

[[email protected] Downloads]$ sudo losetup -f --show -o $((512*94208)) 2017-09-07-raspbian-stretch-lite.img 
/dev/loop1

You need to be root to create loopback devices. -f found us a free loop device, -o took the offset we calculated and –show made sure we get to know which loop device losetup used. Now we can go and mount it like any other partition.

[[email protected] Downloads]$ mkdir /tmp/fresh-pi-rootfs
[[email protected] Downloads]$ sudo mount -v /dev/loop1 /tmp/fresh-pi-rootfs
mount: /dev/loop1 mounted on /tmp/fresh-pi-rootfs.
[[email protected] Downloads]$ cd /tmp/fresh-pi-rootfs
[[email protected] fresh-pi-rootfs]$ ls
bin boot dev etc home lib lost+found media mnt opt proc root run sbin srv sys tmp usr var

Now let’s copy that data to your NFS share. Refer to the code snippet in Convert SD-Card-Based Raspberry Pi To NFS, but copy from /tmp/fresh-pi-rootfs instead of /.

Afterwards, burn the SD card image to your SD card like you would normally. This ensures that you have the proper boot partition (the first partition in the partition table) on your card. If you feel fancy or have a tiny SD card, you can also extract the partition data like we did above and put it onto a fresh FAT partition on the SD card with the same start sector.

 

Convert SD-Card-Based Raspberry Pi To NFS

If you already have a running Raspberry Pi, you probably do not want to rebuild everything from scratch but move your existing data to the network share. All you need to do is to log into your Pi, install rsync (or any other application that reliably copies filesystem attributes like scp), and copy all your stuff over. Do not forget to stop your applications to have them in a defined (stopped) state.

[email protected] ~ $ mkdir /tmp/rootfs
[email protected] ~ $ mount nas:/nfs/rootfs /tmp/rootfs
[email protected] ~ $ rsync -Phax --numeric-ids / /tmp/rootfs/openhab

There are about 31.000 files to copy, so this might take a while. Go brew some coffee or grab a beer.

 

The Configuration

All your data are belong to the NFS share. The last step is to tell the Raspberry Pi to boot from the share instead of the local SD card. One widespread method that works on almost every linux device is to modify the kernel command line. This is usually done in the bootloader (Syslinux,  Grub, U-Boot). The Raspberry Pi got a configurable U-Boot bootloader starting with version 3. In the versions before you have to have a partition on your SD-Card, containing the boot files and a file called “cmdline.txt” that contains the kernel command line. This also works with Version 3 and newer, if you do not want to bother with U-Boot and you are fine with using an SD card for that.

Modify the “cmdline.txt” file on the boot partition. You can do this directly on your Raspberry Pi, or by plugging the SD card into another computer. When using another computer, you’ll find the file on the first Partition of the card, usually called “boot” partition. When editing the file directly on the Pi, it is located at “/boot/cmdline.txt”.

This is an example cmdline.txt I use in production:

dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=/dev/nfs nfsroot=192.168.21.1:/nfs/rootfs/openhab ip=dhcp elevator=deadline

This is basically the default cmdline.txt with additions of the root, nfsroot and ip options. If you want to define a static IP address, use something like

ip=192.168.21.50::192.168.21.1:255.255.255.0:raspberrypi:eth0:off

(ip=<client-IP-number>::<gateway-IP-number>:<netmask>:<client-hostname>:eth0:off)

Please keep in mind, that there must only be exactly one line in the cmdline.txt file. No comments, nothing else. Just one line.

 

The First Boot

Save it, unmount the boot partition (Apple and Microsoft call this “eject securely” or similar), put the SD card into your Pi and fire it up. If you have a screen attached, you will see the communication with the DHCP server and the handover to the NFS filesystem. This usually takes only a second. My boot time increases by 50% which is most likely the case because of my very old and slow NAS. Results may vary.

Congratulations, you just separated hardware from software resources and made your IT a lot more agile. Now, if your Pi explodes, you can just swap it with a new one from you Raspberry-Pi-drawer and be up again in no time. I am not responsible for exploding Pis, though.

 

The Remaining Questions

People asked me about this a lot so maybe this is interesting for you as well:

Q: Wait! What happens, if my NAS crashes, or is offline in some other way?
A: As soon as the NFS server doesn’t answer anymore, your devices will just stop completely. The root filesystem is essential so your Raspberry Pi will just wait until it is reachable again. It will not crash and it won’t reboot or panic. It just waits until your server is back. I think this is quite nice.

 

If you have any other questions, feel free to ask in the comments section below!

 

Read Also

Wow, another tech blog

Wow, another tech blog

So here we are. A small group of Linux-administering, Raspberry Pi-stacking, Micro-controller-soldering and programming people, that opened up yet another Weblog for the world out there.

After sourcing so much useful information from all those tech bloggers across the planet, we now want to give something back. We hope to help you avoid pitfalls we fell into and provide solutions for problems we struggled with.

We are not sure about the name, nor do we fully know which topics we may share with you. Prepare for a chaotic startup, some unclean redesigns, changing domain names and server downtimes whenever you need us. But it will become nicer, cleaner and more stable. And that is a promise – we are all admins at least, so what could possibly go wrong?

See you soon!

 

via https://www.desktopbackground.org/wallpaper/8-bit-pixel-art-wallpapers-959054