Followup: Automating with Ubqiuiti mPower

In my previous post, I talked about how to setup SSH Key authentication and symlinking the relay device so you could easily automate or script how to turn on/off/restart devices.  In this post, I wanted to show you one way in which this could be utliized.

I used shell scripting (on the mPower) and a PHP script running on another (internal) webserver to do this.  This is assuming you already have some experience in scripting (bash, sh, and knowledge of PHP).  You will need to have a webserver (apache,nginx,lighttpd, etc.) set up and configured on how to serve PHP content. I don’t go into this on this post.

I’ll first dazzle you with the end result:

This website uses CSS (bootstrap), html and PHP to render the page.  It utilizes an (additional) SSH key for the webserver to use to log in passwordlessly to the mPower Switch and call the shell script on there.  You can use the shell script by itself or with SSH calls as well.

Replace the bolded and italicized portions below to fit your environment.  The path to your SSH should be readable by the user that’s running the PHP script.  I actually changed the user’s shell (temporarily) to do a ssh-keygen -t rsa.  Also, the variables (e.g. wireless, retropi, etc.) should match the symlinks that you created in the previous post.

power.php

<?php
$path = "/var/etc/persistent/$device";
$ip = "10.0.100.1";
function getStatus() {
 $cmd="/usr/bin/ssh -q -oStrictHostKeyChecking=no -i /path/to/your/ssh_key ubnt@" . $GLOBALS['ip'] . " " . $GLOBALS['path'] . "/power.sh status";
 $out = Array();
 exec($cmd,$out,$retVal);
 if($retVal) {
 echo "<center><h3>Error retrieving status</h3></center>";
 print_r($out);
 exit(1);
 }
 echo "<table>\n";
 echo "<tr><th>Device</th><th>Status</th></tr>\n";
 foreach ($out as $value) {
 $output = explode(" ",$value);
 echo "<tr><td>$output[0]</td><td>$output[1]</td></tr>";
 }
 echo "</table>\n";
}
function deviceAction($device,$action) {
 $out = Array();
 $cmd="/usr/bin/ssh -q -oStrictHostKeyChecking=no -i /path/to/your/ssh_key ubnt@" . $GLOBALS['ip'] . " " . $GLOBALS['path'] . "/power.sh " . $device . " " . $action . " 2>&1";
 exec($cmd,$out,$retVal);
 if($retval) {
 echo "Error with SSH call<br>\n";
 print_r($out);
 echo "<br>";
 }
 echo "<h3>Called SSH command to do action \"$action\" on <b>$device</b></h3><br>\n";

}
echo "<center>";
if ($_POST["action"] == "off" || $_POST["action"] == "on" || $_POST["action"] == "cycle" ) {
 $action = $_POST["action"];
}
if ($_POST["device"] == "xfinity" || $_POST["device"] == "wireless" || $_POST["device"] == "retropi" || $_POST["device"] == "modem" ) {
 $device = $_POST["device"];
}

if (isset($device) && isset($action) && ($device !== "") && ($action !== "")) {
 deviceAction($device,$action);
} else {
 echo "<h3>Please select a device and action</h3>";
}
echo "</center>";
echo "<html>\n";
echo <<< HEAD

<head>
<link href="css/bootstrap.min.css" rel="stylesheet">\n</head>
<style>
.dropbtn {
 background-color: #4CAF50;
 color: white;
 padding: 16px;
 font-size: 16px;
 border: none;
 cursor: pointer;
}

.dropdown {
 position: relative;
 display: inline-block;
}

.dropdown-content {
 display: none;
 position: absolute;
 background-color: #f9f9f9;
 min-width: 160px;
 box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
 z-index: 1;
}

.dropdown-content a {
 color: black;
 padding: 12px 16px;
 text-decoration: none;
 display: block;
}

.dropdown-content a:hover {background-color: #f1f1f1}

.dropdown:hover .dropdown-content {
 display: block;
}

.dropdown:hover .dropbtn {
 background-color: #3e8e41;
}
table {
 font-family: arial, sans-serif;
 border-collapse: collapse;
 width: 100%;
}

td, th {
 border: 1px solid #dddddd;
 text-align: left;
 padding: 8px;
}

tr:nth-child(even) {
 background-color: #dddddd;
}
</style>
</head>
HEAD;
echo "<body>\n";
echo <<< EOB
<br><br>
<center>
<form name="Master" action="" method="post">
<h3>
 <label class="radio-inline">
 <input type="radio" name="device" id="device" value="wireless">Wireless
 </label>
 <label class="radio-inline">
 <input type="radio" name="device" id="device" value="xfinity">Xfinity
 </label>
 <label class="radio-inline">
 <input type="radio" name="device" id="device" value="modem">Modem
 </label>
 <label class="radio-inline">
 <input type="radio" name="device" id="device" value="retropi">RetroPi
 </label>
</h3>
<br>
<div class="dropdown">
 <select id="action" name="action" class="dropbtn">Dropdown</button>
 <div class="dropdown-content">
 <option selected value="">Select an Action</option>
 <option value="cycle">Restart</option>
 <option value="off">Turn Off</option>
 <option value="on">Turn On</option>
 </select>
 </div>
</div>
<br>
<br>
<input type="submit" name="send"/>
</form>
</center>
EOB;
getStatus();
echo "</body></html>\n";
?>

Copy and paste should probably work fine for the above file (Make sure you replace the bolded and italicized text with your environment’s information!). For this next one, I highly recommend copy and pasting it locally and then SCP’ing it over to the mPower. The tab/text formatting gets all screwy in vi; even with :set paste set. Also, make sure to set the permissions of power.sh to executable (chmod +x power.sh or chmod 0700 power.sh) and the PHP file will need to be read and executed by the web service process user.

/var/etc/persistent/power.sh

#!/usr/bin/sh
# Steve Rosenstein 2017 steve@steverosenstein.us

PATH="/var/etc/persistent";
DEVICES="xfinity wireless modem retropi"

status() {
for DEVICE in $DEVICES;
do
 STATUS=$(/usr/bin/cat $PATH/$DEVICE)
 if [ $STATUS == "0" ]
 then
 OUTPUT="off"
 elif [ $STATUS == "1" ]
 then
 OUTPUT="on"
 else
 OUTPUT="unknown"
 fi
 echo $DEVICE $OUTPUT
done
}

usage() {
 echo "$0 <device> [off|on|cycle|restart|status] or $0 status";
 echo "Please enter one of the following possible devices to restart:";
 echo ${DEVICES};
 exit 0;
}

action() {
 DEVICE=$1
 ACTION=$2
 case $ACTION in
 off)
 echo "Turning off ${DEVICE}"
 echo "0" > ${PATH}/${DEVICE};
 ;;
 on) 
 echo "Turning on ${DEVICE}"
 echo "1" > ${PATH}/${DEVICE};
 ;;
 cycle|restart) 
 echo "Turning ${DEVICE} off and then back on"
 echo "0" > ${PATH}/${DEVICE};
 /usr/bin/sleep 2;
 echo "1" > ${PATH}/${DEVICE};
 ;;
 status)
 STATUS=$(/usr/bin/cat ${PATH}/${DEVICE});
 if [ ${STATUS} == "0" ]
 then
 OUTPUT="off";
 elif [ ${STATUS} == "1" ]
 then
 OUTPUT="on";
 else 
 OUTPUT="unknown";
 fi 
 echo "$DEVICE $OUTPUT";
 ;;



*)
 echo "Invalid action selected."
 usage
 ;;
 esac
}
if [ $# == "1" ] && [ "$1" == "status" ]
then
 status;
 exit;
fi

if [ $# != "2" ]
then
 usage;
 exit 1;
else
 DEVICE=$1;
 ACTION=$2
fi
case $DEVICE in 
 retropi)
 action retropi $ACTION;
 ;;
 xfinity)
 action xfinity $ACTION;
 ;;
 wireless)
 action wireless $ACTION;
 ;;
 modem)
 action modem $ACTION;
 ;;
 *)
 usage
 ;;
esac

For the CSS file you should be able to just grab the CSS from here.  You just need to make sure that the bootstrap.min.css is in a directory called “css” in the root of your web directory.  Also make sure they are all readable, and the directory is executable, for the user that runs the web process.

Ubiquiti mPower Automation

So it’s been a while since my last (and first) post, been trying to wait for something worthwhile to write about.  So here it goes…

I’ve been having some issues with devices requiring power cycling.  Home automation/security devices, wireless APs (access points), raspberry Pi, etc.  In the past I used a X10 controller using a program called “heyu”.  It worked well for what it did, but every device had to have a dedicated outlet and was very clunky. After doing some research I found  this. I already have a ubiquiti edge router lite and have been pretty pleased with it, so I decided to pull the trigger to buy it.  They make a smaller 3 port device that is wifi only, but I wanted a hardwired version (especially since I’m going to be resetting access points). I read that you needed to install the mFi Controller software, so I ended up getting it running (had to change some ports, install (deprecated) JDK7, create a systemd startup script, etc.) and it turns out I didn’t really even need it, so not sure if it’s worth it or not.  Anyone feel free to comment on this.

I read on a forum, here, that shows how to use SSH to control the mPower device.  In their example they used expect to log in, which is OK, but I prefer to use SSH keys whenever possible.  Since this runs on busybox, I wasn’t sure if it would work, but I thought I would give it a try.

mkdir ~/.ssh
chmod 0700 ~/.ssh
vi ~/.ssh/authorized_keys
chmod 0600 ~/.ssh/authorized_keys

In the “vi” command above I put my ssh public key.  This is usually located /Users/steve/.ssh/id_rsa.pub (Mac) or /home/steve/.ssh/id_rsa.pub. You just can “cat” the file then copy and paste the output into the authorized_keys file.

If you don’t have one you can create one with something similar to:

ssh-keygen -t rsa

For this to be passwordless and automated, you just hit enter for the password (blank password) or you will have to enter it everytime you SSH.

Also, I had to change the algorithm to SSH to allow me to log in.  You can do this everytime you SSH…. or just put it in your ~/.ssh/config:

Host <Your mPower Device IP>
    User ubnt
    KexAlgorithms +diffie-hellman-group1-sha1

Now, you should just be able to “ssh <IP of mPower Device>”.  It should put in your user and your KexAlgorithm automatically. Voila! Passwordless SSH.

From the forum post I mentioned above, it mentions the /proc/power/relay${num} method of switching on and off the specific port.  I thought this was great, but I wanted to give a little more meaning to the names of the ports and not have to remember their location. So I created some symlinks:

ln -s /proc/power/relay8 ~/modem
ln -s /proc/power/relay3 ~/accesspoint
ln -s /proc/power/relay1 ~/homeautomation

The number in the relay${num} correlates to the port on the mPower power strip. You can plug them into any port you want, just make sure you document it (and maybe label the port).

0 = Off, 1 = On

You just echo 0 or 1 to your newly created symlink.  You can do this without logging in like:

ssh <IP Address of mPower Device> "echo 0 > ~/modem"

To turn off the modem for example, this can easily be scripted with a ping check, or some kind of monitoring tool like Zabbix, Nagios, Zenoss, OpManager, etc. etc. If you create any scripts I’d love to see what you’re doing with it!

How to Setup a Bittorrent client (Transmission) to use a VPN in a docker container

This tutorial will walk you through how to setup a docker container that will automatically start at boot, connect to a VPN, and launch transmission web GUI on CentOS 7.

A big shout out to haugene: https://github.com/haugene/docker-transmission-openvpn . This is the docker container that had everything I needed to get going with transmission and PIA VPN. I should note that haugene supports a TON of other VPNs, so you don’t necessarily need to use PIA, but that’s what I’m using. So let’s get started.

This is already assuming you are root/have sudo access and that docker is installed. If docker isn’t installed, then you should just be able to do a sudo yum -y install docker.

This isn’t the way I initially setup my system, but should make it easier for you.

vi /etc/systemd/system/multi-user.target.wants/transmission.service
Make it look like this:

[Unit]
Description=haugene/transmission-openvpn docker container
After=docker.service
Requires=docker.service

[Service]
User=root
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill transmission
ExecStartPre=-/usr/bin/docker rm transmission
ExecStartPre=/usr/bin/docker pull haugene/transmission-openvpn
ExecStart=/usr/bin/docker run \
–privileged\
–name transmission\
-v /Downloads/torrents:/data\
-v /etc/localtime:/etc/localtime:ro\
-e “OPENVPN_PROVIDER=PIA”\
-e “OPENVPN_CONFIG=US Texas”\
-e “OPENVPN_USERNAME=YOURUSERNAME“\
-e “OPENVPN_PASSWORD=YOURPASSWORD“\
-p 9091:9091\
-e “TRANSMISSION_DOWNLOAD_DIR=/data/complete/movies”\
-e “TRANSMISSION_BLOCKLIST_ENABLED=true”\
-e “TRANSMISSION_BLOCKLIST_URL=http://john.bitsurge.net/public/biglist.p2p.gz”\
-e “TRANSMISSION_SPEED_LIMIT_UP=0″\
-e “TRANSMISSION_SPEED_LIMIT_UP_ENABLED=true”\
haugene/transmission-openvpn
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

You will want to change a few things including the mounting of your local filesystem to to a location inside of the docker container. Above I’m mounting “/Downloads/torrents” to “/data” inside the docker container. You can see below that "TRANSMISSION_DOWNLOAD_DIR="/data/complete/movies, that is relative to the docker container. On your host system, in my case, that would equate to /Downloads/torrents/complete/movies, get it? I Set my config to use “US Texas”, but you should select the one that works the best for your or is the closest. I also set some other options like limiting the speed limit up to “0” (yes, that’s leeching and not nice) and enabled and setting a blocklist. You can set any option that’s in the transmission config. I highly encourage you to RTFM if you want to change/set an option.

If you’re not used to firewalld, it’s pretty simple. But I would highly recommend making sure you have bash completion installed as it makes tab completing the commands MUCH easier (yum install bash-completion).

I left my setup to use the default port 9091, you can change the on your docker host side to be any port you want to forward internally to the transmission container on port 9091.

firewall-cmd --add-masquerade --permanent
firewall-cmd --permanent --add-port=9091/tcp

Note the above --permanent argument, you could run the command without this argument, but whenever you restart firewalld or your system, it won’t persist.

Also, make sure that you restart firewalld so the changes go into effect:
firewall-cmd --reload

After you add that service, you need to re-read all of your service files. If you ever make a change to the transmission.service file, run this command again for them to be “seen” by the system.
sudo systemctl daemon-reload

Then you will want to enable the service at boot:
sudo systemctl enable transmission

Before you start the service, I ran into an issue where transmission was looking for a “watch” directory in the root of the data directory (“/data” for the container, “/Downloads/torrents” for the docker host). So I went ahead and created that:
mkdir -p /Downloads/torrents/watch

Then you can start the service:
sudo systemctl start transmission

You can then browse the IP of the docker host on port 9091 (http://10.0.1.200:9091) and you should see your transmission page:

I wanted to verify that my VPN was actually connecting, so you can download a torrent that will tell you your IP that you’re coming from, I downloaded it from here.

Shameless Plug:

You can compare that to your home NAT’d IP from a website I built here: http://ip-show.com/. You can also do a curl against ip-show.com to just get the IP without all of the extra info:
curl ip-show.com
If you want all of the info (sans header info) in JSON format you can change your useragent to “json”:
curl -A json ip-show.com

Troubleshooting

So let’s pretend that this tutorial isn’t as amazing as you were hoping and it doesn’t work right away, I know, I said pretend.  Part of the fun (at least for me) is getting stuff to work, which means it sometimes has to break for that to happen.

 

Things to check

Is docker running?
systemctl status docker

If not:
systemctl enable docker; systemctl start docker

Do you see the docker container running?

[root@yourhost /]# docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
320049380fb3 haugene/transmission-openvpn "dumb-init /etc/openv" 16 hours ago Up 16 hours 0.0.0.0:9091->9091/tcp transmission


Do you see tranmission running?

Should say active running, if not try to start it again or look at any errors it may have throw.
[root@yourhost /]# systemctl status transmission -l
● transmission.service - haugene/transmission-openvpn docker container
Loaded: loaded (/usr/lib/systemd/system/transmission.service; enabled; vendor preset: disabled)
Active: active (running) since Fri 2017-02-03 18:13:37 MST; 16h ago

Is transmission inside the docker container throwing errors?
Go to your the root of your data drive again, and look for transmission.log. Are there errors? Bad config? An extra space somewhere? Wrong password? Can’t find a directory?