API Request Scheduler
For a technically minded person, having a server at home doesn’t sound too strange. They can be extremely useful for anything from file shares, media servers, to development boxes, etc etc. A friend of mine uses theirs as a backup repository, as well as share files between machines with OwnCloud. Another friend runs VIRL, a virtual routing environment, so that he can build network designs and study networks. However, servers are not designed for home use, therefore pose some challenges when used at home.
Problem Overview
- Very noisy
- Get stuck in a cupboard, loft, or garage
- Not easily accessible
- Consume a lot of power
- Consume power when you’re not using it
- No sleep functions in server OS (how would you wake it up if it did)
- I calculated the yearly cost to be £300-£400 on power
- Scheduling off at night alone brings this down to £180-£200
- Scheduling off at night and office hours brings this down to £80-£100
- Becomes a crucial part of the home setup
- SMB shares
- Media server
- Backup repo
- Data integrity is key
Solution Approaches
I have tackled the noise, by sticking it in a cupboard away from my living space, but this is closer to my bedroom, so can be noisy at night. Also, I’m not using it at night, so the noise and power use is not wanted, nor necessary. I saw 3 approaches to manage this:
- Manually power ON and OFF the server every day
- I tried this first, but it wasn’t very effective
- When needing the server, I realise I’ve forgotten to power it on, then have to wait whilst it boots
- Booting can take a while with a server, as it checks all components and isn’t designed for quick boots (typically a server never gets shut down, except for maintenance)
- At night, or leaving the house, I would forget to turn it off
- No-one wants to get back up once cosy in bed
- Using a ‘smart plug’ with a timer or schedule to kill and provide the power when desired
- Typically they do not have a lot of scheduling flexibility
- Clunky user interfaces, or none at all (revering to programming the plug itself)
- This is not a graceful shutdown so it increases risk of data corruption and integrity errors, OS failing to boot, and hardware failures
- Use a tool to monitor a schedule, then trigger power ON and OFF commands to the server management API
- Schedule can be done in a few ways
- Triggering the commands via API allows me to do graceful shutdowns and protect the server
- Tool requires access to the API, sitting on the servers ‘powered off management console’ (CIMC, iLO, DRAC)
Option 3 is the direction I took, but I couldn’t see a simple way to do this with a tool that existed. Any service on the Internet would require access to the ‘powered off management console’ of the server, so I would have to setup port forwarding on my edge router (to traverse stateful NAT PAT), setup dynamic DNS with an agent running inside the house (to keep the home IP known externally if the ISP changes this), take more care with security of the servers ‘powered off management console’ and what it speaks back to (having had something similar setup in the past, the logs showed hundreds of automated login attempts on the admin interface, admin:password123, admin:pa$$w0rd123, …, …, …). Lastly, the API uses a self-signed certificate, many ‘more official’ tools expect properly configured certificates, as is best practice, but I do not want to have to manage a certificate on the server API.
So I took a different approach and built something to run within my home, within my LAN, from a RaspberryPi. As this is local to the ‘powered off management console’ I don’t have to set up and manage any of those things mentioned above. For scheduling I can hook into an ICAL, Google Calendar etc, which the RaspberryPi can request from within my home (forming the start of a stateful connection, so no port forwarding or dynamic DNS).
Solution
Components
These components allowed me to get going quickly, work around certain restrictions, and result in something that meets my requirements. Some of this has been detailed above in “Solution Approaches”.
- Scheduling with: Google Calendar
- Running the code with: RaspberryPi
- Code created for this: API Request Scheduler
- Code used within my code: ICS-GoLang
- API of the server: RedFish API
Google Calendar
Everyone knows what Google Calendar is (I hope). It is important to be realistic and efficient, I was not going to make a full calendaring function and host this for scheduling, particularly not of a high quality in the desired short time frame of the project. My initial thoughts were to code in time frames (06:00 to 22:00), but this wasn’t flexible, and would still require myself to create a front end for both PC and mobile.
This is why I chose to use someone else’s calendar, pulling the data via ICS, and making a decision based upon that. Allowing myself to now schedule the status via my PC (Google Calendar WebUI), via my phone (Google Calendar App), control sharing permissions with my family, from anywhere with an Internet connection.
- Walking back from town with popcorn and Doritos? Schedule the server to be on via my phone
- Just booked a weekend away, or a work evening away? Delete the ON entry via my PC
- Want the power on every weekday between 18:00 and 22:00? Schedule a repeating event in Google Calendar
- Family coming over and wants to watch some things during the day? Share the calendar with them
RaspberryPi
This Pi has been sat in a box for many years now, covering different roles, each which have been superseded by changes in needs or updates in tool-sets. For the project the Pi is perfect, running silently hidden away, consuming very little power, capable of running any code I give it. I created my tool with GoLang, which is supported across platforms. GoLang can compile into binary code for x86, ARM, Windows, Linux, BSD, etc. Getting the Pi up and running is a breeze, thanks to the work the RaspberryPi team have done, flashing on the Raspbian OS, enabling SSH and going from there.
API Request Scheduler
I created this code specifically for this project, but designed it to be flexible. With the use of a configuration file, it can be pointed at any API (which supports basic authentication), at any calendar which is ICS, and map search strings to API payloads that work with that API. At present it supports up to 5 different API payloads, in future it can be updated to support effectively an unlimited number, but since I only need 2 I am happy with it. My code is open on GitHub, so feel free to look, branch, etc. To have this run regularly, it’s set as a cron job on the Pi.
ICS-GoLang
A GoLang package imported into my tool, without this it would have taken a lot longer to develop the tool, with a lot more Googling and hair pulling. This is the power of open source! The code is open on GitHub, this is a great piece of work that worked flawlessly for me, thank you to PuloV for this.
RedFish API
Every API is different in someway, thankfully my server supports the RedFish API, which is a standard API for servers day to day operations. This is what allows me to do a graceful shutdown, not a ‘pull the power’, as well as booting up, getting information, etc etc. Nearly all the major server manufacturers support it in some shape or form, but hardware and software versions will need to be checked.
Code
The first part of the code is the simplest, to get the file path of the config file which the user wants to use. By specifying the path users can use any path they may like, rather than staticly mapped, which caused a problem for me (discussed in the Raspbian & Cron section).
func main() {
// READ command-line arguments to get config file path
// The [1:] skips the first word, the name of the binary
CliArguments := os.Args[1:]
// We can now use these arguments like so.
// This will print it to StdOut
fmt.Println(CliArguments[0])
}
Now that we know where the config file is, the next step is to load that and be prepared for the users environment. I could have passed all of these in as CLI arguments too, but I generally prefer config files as they are saved, easily editable, easily read, etc.
The first aspect is to create a struct, which will hold the data we pull in from the config file, which can then later be passed to the functions for use. Here I am quite static with the use of 5 string values to payloads; if you were to use much more than 5, you probably wouldn’t want to do it this way and instead be smarter in building this out, otherwise it can be very long.
// Config for user configuration used throughout
type Config struct {
ServerAPIPath string
ServerAPIUser string
ServerAPIPass string
ServerAPIPayloadDefault string
CalendarValue001 string
ServerAPIPayload001 string
CalendarValue002 string
ServerAPIPayload002 string
CalendarValue003 string
ServerAPIPayload003 string
CalendarValue004 string
ServerAPIPayload004 string
CalendarValue005 string
ServerAPIPayload005 string
ServerAPIIgnoreCertError bool
ICALPath string
}
These map directly to the config file, which is a JSON based file. As you can see the names match between the two, it is possible to map to different names, but I had no need for that. Below is the sampleConfig.json included with the code. We can also see the mappings here, so when the tool see’s an active calendar event with the subject/summary of ON
(CalendarValue001), it will trigger the payload of {"ResetType":"On"}
(ServerAPIPayload001).
{
"ServerAPIPath": "https://serverAPI/redfish/v1/Systems/SNabcabcabc/Actions/System.Reset",
"ServerAPIUser": "admin",
"ServerAPIPass": "Password123",
"ServerAPIPayloadDefault": "{\"ResetType\":\"GracefulShutdown\"}",
"CalendarValue001": "ON",
"ServerAPIPayload001": "{\"ResetType\":\"On\"}",
"CalendarValue002": "",
"ServerAPIPayload002": "",
"CalendarValue003": "",
"ServerAPIPayload003": "",
"CalendarValue004": "",
"ServerAPIPayload004": "",
"CalendarValue005": "",
"ServerAPIPayload005": "",
"ServerAPIIgnoreCertError": false,
"ICALPath": "https://calendar.google.com/calendar/ical/xyzxyzxyz/basic.ics"
}
To load this in we add a LoadConfiguration function call into main()
and the function itself.
func main() {
// READ command-line arguments to get config file path
// The [1:] skips the first word, the name of the binary
CliArguments := os.Args[1:]
// Pass first CLI argument as config file location
// LoadConfiguration then returns the data into the Config struct
config := LoadConfiguration(CliArguments[0])
}
// LoadConfiguration file then return Config struct
func LoadConfiguration(filename string) Config {
var config Config
// Load the configuration file using GoLang's built in os.Open()
// If there is an error, save this in err, to be displayed later
configFile, err := os.Open(filename)
// Wait until the above is completed
defer configFile.Close()
// If there is an error, display it on StdOut
if err != nil {
fmt.Println(err.Error())
}
// Decode the JSON into a format the tool can understand
jsonParser := json.NewDecoder(configFile)
jsonParser.Decode(&config)
// Return the values into the Config struct
return config
}
Now that we’ve loaded the configuration, we can start doing the fun stuff. The next phase is to pull down the ICAL data to inspect. For this I used PuloV/ics-golang. He provides some good examples in his repository, which is what I used to get started. Again we need to call this function from main()
.
func main() {
// READ command-line arguments to get config file path
// The [1:] skips the first word, the name of the binary
CliArguments := os.Args[1:]
// Pass first CLI argument as config file location
// LoadConfiguration then returns the data into the Config struct
config := LoadConfiguration(CliArguments[0])
// LoadICAL and return current event summary ON/OFF
// This will return an integer number, which I correlate with the config for the appropriate request payload
// This part is effectively limitless in the number of options you can have
// We are passing in the config struct, so that the function can see that data
desiredStatus := LoadICAL(config)
}
// LoadICAL and return current event summary ON/OFF
func LoadICAL(config Config) int {
// This first part is mostly a copy from the examples provided
// They are clearly laid out so I won't repeat here
// create new parser
parser := ics.New()
// set the filepath for the ics files
ics.FilePath = "tmp/new/"
// we dont want to delete the temp files
ics.DeleteTempFiles = false
ics.RepeatRuleApply = true
// get the input chan
inputChan := parser.GetInputChan()
// send the calendar urls to be parsed
inputChan <- config.ICALPath
// wait for the calendar to be parsed
parser.Wait()
// get all calendars in this parser
cal, _ := parser.GetCalendars()
// Check ICAL payload for errors
// I was getting some panic errors, where the data wasn't being pulled correctly
// I added a check to see if there was data and report accordingly
if len(cal) > 0 {
fmt.Println("LoadICAL() - Calender entries have been pulled. Slice is populated")
} else {
fmt.Println("LoadICAL() - Calender entries have not been pulled. Slice is empty")
fmt.Println("LoadICAL() - App will panic")
fmt.Println("LoadICAL() - Please check the URL of the calendar ICS")
}
// Check ICAL current event payload
// Once we have the payload we can look for the first active event
// I use a dedicated calendar for this, so there is no concerns of overlapping events etc
// Again this is not a scalable element, if doing a lot more than 5 options it will start to get very big
for _, e := range cal[0].GetEvents() {
now := time.Now()
if now.After(e.GetStart()) && now.Before(e.GetEnd()) {
if strings.EqualFold(config.CalendarValue001, e.GetSummary()) {
fmt.Println("LoadICAL() - Current even is", e.GetSummary(), "-", &e)
return 1
} else if strings.EqualFold(config.CalendarValue002, e.GetSummary()) {
fmt.Println("LoadICAL() - Current even is", e.GetSummary(), "-", &e)
return 2
} else if strings.EqualFold(config.CalendarValue003, e.GetSummary()) {
fmt.Println("LoadICAL() - Current even is", e.GetSummary(), "-", &e)
return 3
} else if strings.EqualFold(config.CalendarValue004, e.GetSummary()) {
fmt.Println("LoadICAL() - Current even is", e.GetSummary(), "-", &e)
return 4
} else if strings.EqualFold(config.CalendarValue005, e.GetSummary()) {
fmt.Println("LoadICAL() - Current even is", e.GetSummary(), "-", &e)
return 5
}
}
}
return 0
}
Perfect, now we have the summary of the current active event, we have checked that against the config to see what entry it refers to, then reported back an integer number that references it. This can now be passed to a function to set the server state accordingly.
func main() {
// READ command-line arguments to get config file path
// The [1:] skips the first word, the name of the binary
CliArguments := os.Args[1:]
// Pass first CLI argument as config file location
// LoadConfiguration then returns the data into the Config struct
config := LoadConfiguration(CliArguments[0])
// LoadICAL and return current event summary ON/OFF
// This will return an integer number, which I correlate with the config for the appropriate request payload
// This part is effectively limitless in the number of options you can have
// We are passing in the config struct, so that the function can see that data
desiredStatus := LoadICAL(config)
// Send request to API with desiredStatus
SetServerState(config, desiredStatus)
}
// SetServerState power state from desired state
func SetServerState(config Config, state int) {
// First we check to see if we should ignore self signed certificate warnings
// This is configured in the config file
if config.ServerAPIIgnoreCertError == true {
// CHANGE SECURITY TO IGNORE SELF SIGNED CERTIFICATE WARNINGS
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
// We pull down the url here, just as a test of what we've changed above
// If there is an error since this change, we dump the error to StdOut
_, err := http.Get("https://golang.org/")
if err != nil {
fmt.Println(err)
}
}
// PERFORM REQUEST ON API
// Set payload to match desired state
// We use the value we got in the previous function and map this to the payload value in the config file
// Again this is not a scalable element, if doing a lot more than 5 options it will start to get very big
var payload = strings.NewReader("")
if state == 0 {
payload = strings.NewReader(config.ServerAPIPayloadDefault)
} else if state == 1 {
payload = strings.NewReader(config.ServerAPIPayload001)
} else if state == 2 {
payload = strings.NewReader(config.ServerAPIPayload002)
} else if state == 3 {
payload = strings.NewReader(config.ServerAPIPayload003)
} else if state == 4 {
payload = strings.NewReader(config.ServerAPIPayload004)
} else if state == 5 {
payload = strings.NewReader(config.ServerAPIPayload005)
}
// Build & trigger the request
req, _ := http.NewRequest("POST", config.ServerAPIPath, payload)
req.SetBasicAuth(config.ServerAPIUser, config.ServerAPIPass)
req.Header.Add("Cache-Control", "no-cache")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
// Print the result of request
fmt.Println(res)
fmt.Println(string(body))
}
GoLang Compile
During initial testing I installed GoLang tools onto the Pi and ran the code through that, having Go compile at runtime. However, this was noticeably slow. On my desktop the difference is barely noticeable, the code isn’t super long nor complex, but there is a slight delay. On the Pi however, it was multiple seconds per run, which is not what I was aiming for. Although I wasn’t going to be sat watching it run, I always had the intension of compiling the code, and this additional delay during testing confirmed to me the power of compiling for the platform. Once I started to compile the ARM binary, the execution time was instant, as on my desktop.
To compile for the Pi, the GoLang environment variables specify the environment to which to compile for, you can see your current ones with go env
. To change these depends on your system, Linux or Windows, Google is your friend here. For the Pi, running Raspbian, you will want to have the following set. If instead you are using a BSD base OS, such as RaspBSD, your variables will be different. GoLang supports many different platforms, you will have to know the intended platform and specify for this.
GOOS=linux
GOARCH=arm
GOARM=5
Once compiled, you can push the binary to the Pi with SCP (or USB, or SD, etc). Secure Copy (SCP) is included in most if not all Linux OSs, tools can be downloaded for Windows too.
scp ./main.go pi@192.168.1.10:/home/pi/apiResourceScheduler.arm.bin
scp ./config.json pi@192.168.1.10:/home/pi/config.json
Raspbian & Cron
As mentioned, Raspbian was used as the OS for the RasbperryPi, as recommended by the Pi team. Rasbian is based on Debian, so feels very much as you would expect, with some nice little features added in by the Pi team.
The code above works, but at present needs to be run manually, not much of a scheduler at this point. The code could have been coded to wait for certain times to trigger an action, but this isn’t really reliable as then needs to be running 24 /7, nor recommended. Linux already has a scheduling functionality built in, called Cron Jobs, this is the recommended approach. I utilised that to run the code at a regular interval. On Rasbian the users cron jobs can be configured with crontab -e
. This command will open the default text editor with the configuration loaded, all you have to do is add your own entries.
# Edit this file to introduce tasks to be run by cron.
#
# <truncated output>
#
# m h dom mon dow command
#
# Run apiRequestScheduler every 1 minute
# Output the StdOut to a logfile
*/1 * * * * /home/pi/apiRequestScheduler/apiRequestScheduler.arm.bin /home/pi/apiRequestScheduler/config.json 1>> /home/pi/apiRequestScheduler/apiRequestScheduler.log
# Clear apiRequestScheduler log every 1st of the month
# I don't want the SD card to get full at any point
* * 1 * * echo "Log Cleared Daily from 'crontab -e'" > /home/pi/server_control/server_control.log
# Clear apiRequestScheduler temporary files every 1st of the month
# I don't want the SD card to get full at any point
* * 1 * * rm -rf /home/pi/server_control/tmp/*
The re-occurring frequency is handled by the prefix of numbers, these relate to minutes, hours, day, month, year. To have an entry repeat every minute, as used by the example above, ‘step values’ can be utilised. This is to say, in the minute column, a value of */1
will run every minute. A value of */2
will run every 2 minutes, etc. Whereas a value of 30
will run on the 30th minute. This is fairly configurable once you get your head around it, the crontab documentation has some good examples of these, but for this project every minute meets the requirement.
Every minute the shell runs the command /home/pi/apiRequestScheduler/apiRequestScheduler.arm.bin /home/pi/apiRequestScheduler/config.json 1>> /home/pi/apiRequestScheduler/apiRequestScheduler.log
which triggers the binary, passes in the config file and lastly outputs StdOut to a log file. Initially I had used a local config file reference ./config.json
in the code. However, the cron job is not run from the local directory of the binary, so the config is not in ./
. I updated the code to take in a config path, which can be specified in this command.
Closing Statements
That’s it, to see it all as one please look here on GitHub - main.go. There is not a lot of complexity in what goes on, thanks to GoLang, PuloV and the RaspberryPi team it was fun to setup and get a working tool in the end. That’s not to say I didn’t use Google and StackOverflow constantly. I feel the mix of different technologies was the right approach, if not at least an interesting endeavour for myself, as it allows me to use the best of different tool-set and get something really easy, quickly out the door, with good amount of flexibility.