Showing posts with label LXD. Show all posts
Showing posts with label LXD. Show all posts

Sunday, November 13, 2022

Bridging LXD Containers in Ubuntu Core

I'm setting up a set of server containers on an Ubuntu Core 22 base.

This is slightly different from deb-based Ubuntu in several ways.

The hardware is a salvaged laptop motherboard, without keyboard or monitor.

Sunday, August 16, 2020

Installing Home Assistant Core in an LXD Container (Part 2)

Last time, we built a basic LXD container, and then build HomeAssistant inside.

This time, we're going to add a few more elements.

  • We're going to do all the steps on the Host instead of diving inside the container. So we're going to use lxc exec and lxc push. The goal is to make spinning up a new container scriptable
  • We're going to start/stop the HomeAssistant application using a systemd service
  • We're going to keep the data and config outside the container and use an lxd disk device to mount the data. Even if we destroy the container, the data and config survive to be mounted another day.

Preparing LXD

We're going to skip LXD initialization in this example. There's one addition from last time: We're going to add shiftfs, which permits us to chown mounted data. The macvlan profile and shiftfs enablement are persistent -- if you already have them, you don't need to redo them. All of these commands occur on the Host (we have not created the container yet!)

   # Create a macvlan profile, so the container will get it's IP Address from
   # the router instead of the host. This works on ethernet, but often not on wifi 
   ip route show default 0.0.0.0/0
   lxc profile copy default lanprofile
   lxc profile device set lanprofile eth0 nictype macvlan
   lxc profile device set lanprofile eth0 parent enp3s5

   # Test that macvlan networking is set up
   lxc profile show lanprofile
     config: {}
     description: Default LXD profile  // Copied. Not really the default
     devices:
       eth0:                           // Name, not real device
         nictype: macvlan              // Correct network type
         parent: enp3s5                // Correct real device
         type: nic

   # Enable shiftfs in LXD so data mounts work properly
   sudo snap set lxd shiftfs.enable=true
   sudo systemctl reload snap.lxd.daemon

   # Test that shiftfs is enabled:
   Host$ lxc info | grep shiftfs
    shiftfs: "true"

Create the Container and Initial Configuration

If LXD is already set up, then start here. We will mount the external data location, set the timezone and do all that apt setup. But this time, we will do all the commands on the Host instead of inside the container. We will also create the sources.list file on the host and push it into the container.

   # Create the container named "ha"
   lxc launch -p lanprofile ubuntu:focal ha

   # Mount the existing HomeAssistant data directory
   # Skip on the first run, since there won't be anything to mount
   # Shiftfs is needed, else the mounted data is owned by nobody:nogroup
   # Chown is needed because shiftfs changes the owner to 'ubuntu'
   lxc config device add ha data_mount disk source=/somewhere/else/.homeassistant path=/root/ha_data
   lxc config device set ha data_mount shift=true
   lxc exec ha -- chown -R root:root /root

   # Set the timezone non-interactively
   lxc exec ha -- ln -fs /usr/share/zoneinfo/US/Central /etc/localtime
   lxc exec ha -- dpkg-reconfigure -f noninteractive tzdata

   # Reduce apt sources to Main and Universe only
   # Create the new sources.list file on the host in /tmp
   # Paste all of these lines at once into the Host terminal
   cat <<EOF > /tmp/container-sources.list
   deb http://us.archive.ubuntu.com/ubuntu/ focal main universe
   deb http://us.archive.ubuntu.com/ubuntu/ focal-updates main universe
   deb http://security.ubuntu.com/ubuntu focal-security main universe
   EOF

   # Push the file into the container
   lxc file push /tmp/container-sources.list ha/etc/apt/sources.list

   # Apt removals and additions
   lxc exec ha -- apt autoremove openssh-server
   lxc exec ha -- apt update
   lxc exec ha -- apt upgrade
   lxc exec ha -- apt install python3-pip python3-venv

Create the Venv, Build HomeAssistant, and Test

This method is simpler than all that mucking around activating and venv and paying attention to your prompt. All these command are issued on the Host. You don't need a container shell prompt.

   # Setup the homeassistant venv in a dir called 'ha_system'
   #We will use the root account since it's an unprivileged container.
   lxc exec ha -- python3 -m venv --system-site-packages /root/ha_system

   # Build and install HomeAssistant
   lxc exec ha -- /root/ha_system/bin/pip3 install homeassistant

   # Learn the container's IP address. Need this for the web browser. 
   lxc list | grep ha

   # Run HomeAssistant
   lxc exec ha -- /root/ha_system/bin/hass -c "/root/ha_data"

   # Use your browser to open the the IP address:8123
   # HA takes a couple minutes to start up. Be patient.
   # Stop the server from within the Web UI or ^C to exit when done.

Start HomeAssistant at Boot (Container Startup)

The right way to do autostart is a systemd service file on the container. Like with the sources.list file, we will create it on the host, then push it into the container, then enable it. There's one optional ExecPreStart line - it will slow each startup slightly while it checks for and installs updates.

   cat <<EOF > /tmp/container-homeassistant.service
   [Unit]
   Description=Home Assistant
   After=network-online.target

   [Service]
   Type=simple
   User=root
   PermissionsStartOnly=true
   ExecPreStart=/root/ha_system/bin/pip3 install --upgrade homeassistant
   ExecStart=/root/ha_system/bin/hass -c "/root/ha_data"

   [Install]
   WantedBy=multi-user.target
   EOF

   # Push the .service file into the container, and enable it
   lxc file push /tmp/container-homeassistant.service ha/etc/systemd/system/homeassistant.service
   lxc exec ha -- systemctl --system daemon-reload
   lxc exec ha -- systemctl enable homeassistant.service
   lxc exec ha -- systemctl start homeassistant.service

Now we can test it. The last command should start HA. The same command with 'stop' should gracefully stop HA. Restarting the container should gracefully stop HA, and then restart it automatically. Your web browser UI should pick up each stop and start. You did it!


Final Notes

Remember how you start without any HomeAssitant data to mount? Now that you have a running HA Core, you can save a set of data:

   lxc file pull ha/root/.homeassistant /somewhere/else/.homeassistant --recursive

And remember to clean up your mess when youare done:

   lxc stop ha
   lxc delete ha

Saturday, August 15, 2020

Installing Home Assistant Core in an LXD Container (Part 1)

I've been running HomeAssistant Core reliably in an LXD container for almost two years now, so it's probably time to start detailing how to do it.

This is a step-by-step example of how to do it for folks who aren't very familiar with LXD containers and their features.

Installing LXD (documentation)

If you haven't used LXD before, you need to install it (it's a Snap) and initialize it (tell it where the storage is located). The initialization defaults are sane, so you should not have problems.

   sudo snap install lxd
   sudo lxd init

Container Profile: Macvlan Networking (optional)

A macvlan profile is one easy way for the container to get it's IP address from the router instead of the host. This means you can use a MAC Address filter to issue a permanent IP address. This works on ethernet, but often not on wifi. You only need to set up this profile ONCE, and it's easiest to do BEFORE creating the container. Since the container doesn't exist yet, all of these commands are done on the Host.

   # Get the real ethernet device (enp3s5 or some such)
   ip route show default 0.0.0.0/0

   # Make mistakes on a copy
   lxc profile copy default lanprofile

   # Change nictype field to macvlan
   #  'eth0' is a virtual device, not a real eth device
   lxc profile device set lanprofile eth0 nictype macvlan

   # Change parent field to real eth interface
   lxc profile device set lanprofile eth0 parent enp3s5

Create the Container

Create a new container named 'ha'. This command is done on the Host.

   # Create the container named "ha"
   lxc launch -p lanprofile ubuntu:focal ha

   # Learn the container's IP address. Need this for the web browser. 
   lxc list | grep ha

   # Get a root shell prompt inside the container
   lxc shell ha

Initial Setup in the Container

Let's get a shell set up timezone and apt. These commands are done on the Container root prompt.


   // This is one way to set the timezone
   dpkg-reconfigure tzdata

   // Reduce apt sources to Main and Universe only
   cat <<EOF > /etc/apt/sources.list
   deb http://us.archive.ubuntu.com/ubuntu/ focal main universe
   deb http://us.archive.ubuntu.com/ubuntu/ focal-updates main universe
   deb http://security.ubuntu.com/ubuntu focal-security main universe
   EOF

   // Tweak: Remove openssh-server
   apt autoremove openssh-server

   // Populate the apt package database and bring the container packages up-to-date
   apt update
   apt upgrade
   
   // Install the python packages needed for HomeAssistant
   apt install python3-pip python3-venv

   # Setup the homeassistant venv in the root home dir (/root)
   # --system-site-packages allows the venv to use the many deb packages that are already
   #    installed as dependencies instead of donwnloading pip duplicates 
   python3 -m venv --system-site-packages /root

Install and Run HomeAssistant

Now we move into a virtual environment inside the container, build HomeAssistant, and give it a first run. If you try to build or run HomeAssistant outside the venv, it will fail with cryptic errors.

   // Activate the installed venv. Notice how the prompt changes.
   root@ha:~# source bin/activate
   (root) root@ha:~#
   
   // Initial build of HomeAssistant. This takes a few minutes.
   (root) root@ha:~# python3 -m pip install homeassistant

   // Instead of first build, this is where you would upgrade
   (root) root@ha:~# python3 -m pip install --upgrade homeassistant

   // Initial run to set up and test.
   (root) root@ha:~# hass

   // After a minute or two, open the IP Address (port 8123). Example: http://192.168.1.18:8123
   // Use the Web UI to shut down the application. Or use CTRL+C.

   // Exit the venv
   (root) root@ha:~# deactivate

   // Exit the container and return to the Host shell.
   root@ha:~# exit
   Host:~$


There's a lot more to talk about in future posts:

  • The systemd service that starts HomeAssistant at container startup.
  • Creating an LXD disk device to keep the HomeAssistant data in. If I rebuild the container for some reason, I can simply connect it to the data.
  • Adding a USBIP client. The Z-Wave controller is elsewhere in the building, and USBIP lets me control it like it's attached to the host. That also means adding a USB device to the container.
  • Collecting Host hearbeat statistics for the HomeAssistant dashboard, and pushing those into the container regularly.
  • Backing up and restoring HomeAssistant data and configurations.

Friday, August 14, 2020

LXD Containers on a Home Server

LXD Containers are very handy, and I use them for quite a few services on my home hobby & fun server. Here's how I set up my containers after a year of experimenting. Your mileage will vary, of course. You may have very different preferences than I do.

1. Networking:

I use macvlan networking. It's a simple, reliable, low-overhead way to pull an IP address from the network DHCP server (router). I set the IP address of many machines on my network at the router.

The container and server cannot communicate using TCP/UDP with each other. I don't mind that.

You only need to set up this profile once for all containers. Simply specify the profile when creating a new container.

   // 'Host:$' means the shell user prompt on the LXD host system. It's not a shell command

   // Learn the eth interface: enp3s5 in this example
   Host:$ ip route show default 0.0.0.0/0

   // Make mistakes on a copy
   Host:$ lxc profile copy default lanprofile

   // Change nictype field. 'eth0' is a virtual device, not a real eth device
   Host:$ lxc profile device set lanprofile eth0 nictype macvlan

   // Change parent field to real eth interface
   Host:$ lxc profile device set lanprofile eth0 parent enp3s5

   // Let's test the changes
   Host:$ lxc profile show lanprofile
     config: {}
     description: Default LXD profile  // This field is copied. Not really the default
     devices:
       eth0:                           // Virtual device
         nictype: macvlan              // Correct network type
         parent: enp3s5                // Correct real device
         type: nic
       root:
         path: /
         pool: containers-disk         // Your pool will be different, of course
         type: disk
     name: lanprofile


2. Creating a Container

Create a new container called 'newcon':

   Host:$ lxc launch -p lanprofile ubuntu:focal newcon
      // 'Host:$'        - user (non-root) shell prompt on the LXD host
      // '-p lanprofile' - use the macvlan networking profile
      // 'focal'         - Ubuntu 20.04. Substitute any release you like


3. Set the Time Zone

The default time zone is UTC. Let's fix that. Here are two easy ways to set the timezone: (source)

   // Get a root prompt within the container for configuration
   // Then use the classic Debian interactive tool:
   Host:$ lxc shell newcon
   newcon:# dpkg-reconfigure tzdata

   // Alternately, here's a non-interactive way to do it entirely on the host
   Host:$ lxc exec newcon -- ln -fs /usr/share/zoneinfo/US/Central /etc/localtime
   Host:$ lxc exec newcon -- dpkg-reconfigure -f noninteractive tzdata


4. Remove SSH Server

We can access the container from the server at anytime. So most containers don't need an SSH server. Here are two ways to remove it

   // Inside the container
   newcon:# apt autoremove openssh-server 
   
   // Or from the Host
   Host:$ lxc exec newcon -- apt autoremove openssh-server


5. Limit Apt sources to what the container will actually use

Unlike setting the timezone properly, this is *important*. If you do this right, the container will update itself automatically for as long as the release of Ubuntu is supported (mark your calendar!) If you don't get this right, you will leave yourself an ongoing maintenance headache.

   // Limit the apt sources to (in this example) main from within the container
   newcon:# nano /etc/apt/sources.list
         // The final product should look similar to:
         deb http://archive.ubuntu.com/ubuntu focal main           
         deb http://archive.ubuntu.com/ubuntu focal-updates main           
         deb http://security.ubuntu.com/ubuntu focal-security main 

   // Alternately, *push* a new sources.list file from the host.
   # Create the new sources.list file on the host in /tmp
   cat <<EOF > /tmp/container-sources.list
   deb http://us.archive.ubuntu.com/ubuntu/ focal main
   deb http://us.archive.ubuntu.com/ubuntu/ focal-updates main
   deb http://security.ubuntu.com/ubuntu focal-security main
   EOF
   
   // *Push* the file from host to container
   Host:$ lxc file push /tmp/container-sources.list newcon/etc/apt/sources.list


6. Install the Application

How you do this depends upon the application and how it's packaged.



7. Update Unattended Upgrades

This is the secret sauce that keeps your container up-to-date. First, let's look at a cleaned-up version of the first 20-or-so lines of /etc/apt/apt.conf.d/50unattended-upgrades inside the container:

                    What it says                             What it means
           ------------------------------------------      -----------------------
   Unattended-Upgrade::Allowed-Origins {
           "${distro_id}:${distro_codename}";              Ubuntu:focal
           "${distro_id}:${distro_codename}-security";     Ubuntu:focal-security
   //      "${distro_id}:${distro_codename}-updates";      Ubuntu:focal-updates
   //      "${distro_id}:${distro_codename}-proposed";     Ubuntu:focal-proposed
   //      "${distro_id}:${distro_codename}-backports";    Ubuntu:focal-backports
   };

...why, those are just the normal repositories! -security is enabled (good), but -updates is disabled (bad). Let's fix that. Inside the container, that's just using an editor to remove the commenting ("//"). From the host, it's a substitution job for sed:

   Host:$ lxc exec newcon -- sed "s\/\ \g" /etc/apt/apt.conf.d/50unattended-upgrades

Third-party sources need to be updated, too. This is usually easiest from within the container. See this post for how and where to update Unattended Upgrades with the third-party source information.



8. Mounting External Media

Some containers need disk access. A classic example is a media server that needs access to that hard drive full of disorganized music.

If the disk is available across the network instead of locally, then use plain old sshfs or samba to mount the network share in /etc/fstab.

If the disk is local, then first mount it on the Host. After it's mounted, use an lxd disk device inside the container. A disk device is an all-in-one service: It creates the mount point inside the container and does the mounting. It's persistent across reboots...as long as the disk is mounted on the host.

   // Mount disk on the host and test
   Host:$ sudo mount /dev/sda1 /media
   Host:$ ls /media
      books         movies       music

   // Create disk device called "media_mount" and test
   Host:$ lxc config device add newcon media_mount disk source=/media path=/Shared_Media
   Host:$ lxc exec newcon -- ls /Shared_Media
      books         movies       music

If the ownership of files on the disk is confused, and you get "permisson denied" errors, then use shiftfs to do the equivalent of remounting without suid

   Host:$ lxc exec newcon -- ls /Shared_Media/books
      permission denied

   // Enable shiftfs in LXD, reload the lxd daemon, and test
   Host$ sudo snap set lxd shiftfs.enable=true
   Host$ sudo systemctl reload snap.lxd.daemon
   Host$ lxc info | grep shiftfs
    shiftfs: "true"

   // Add shiftsfs to the disk device
   Host$ lxc config device set newcon media_mount shift=true

   Host:$ lxc exec newcon -- ls /Shared_Media/books
      boring_books       exciting_books        comic_books        cookbooks

Friday, May 8, 2020

Testing Ubuntu Core with Containers on VirtualBox

I want to try out Ubuntu Core to see if it's appropriate for running a small server with a couple containers.

The current OS is Ubuntu Server 20.04...but I'm really not using most of the Server features. Those are in the LXD containers. So this is an experiment to see if Ubuntu Core can function as the server OS.

Prerequisites: If you are looking to try this, you should already be familiar (not expert) with:
  • Using SSH
  • Using the vi text editor (Ubuntu Core lacks nano)
  • Basic networking concepts like dhcp
  • Basic VM and Container concepts


Download Ubuntu Core:
  • Create Ubuntu SSO Account (if you don't have one already)
  • Create a SSH Key (if you don't have one already)
  • Import your SSH Public Key to Ubuntu SSO.
  • Download an Ubuntu core .img file from https://ubuntu.com/download/iot#core
  • Convert the Ubuntu Core .img to a Virtualbox .vdi:

         me@desktop:~$ VBoxManage convertdd ubuntu-core-18-amd64.img ubuntu-core.vdi


Set up a new machine in VirtualBox:
  • Install VirtualBox (if you haven't already):

         me@desktop:~$ sudo apt install virtualbox

  • In the Virtualbox Settings, File -> Virtual Media Manager. Add the ubuntu-core.vdi
  • Create a New Machine. Use an existing Hard Disk File --> ubuntu-core.vdi
  • Check the network settings. You want a network that you will be able to access. I chose bridged networking so I could play with the new system from different locations, and set up a static IP address on the router. ENABLE promiscuous mode, so containers can get IP addresses from the router. Otherwise, VirtualBox will filter out the dhcp requests.
  • OPTIONAL: Additional tweaks to enhance performance.


Take a snapshot of your current network neighborhood:
  • Use this to figure out Ubuntu Core's IP address later on:
     me@Desktop:~$ ip neigh
     192.168.1.227 dev enp3s0 lladdr 00:1c:b3:75:23:a3 STALE
     192.168.1.234 dev enp3s0 lladdr d8:31:34:2c:b8:3a STALE
     192.168.1.246 dev enp3s0 lladdr f4:f5:d8:29:e5:90 REACHABLE
     192.168.1.213 dev enp3s0 lladdr 98:e0:d9:77:5d:6b STALE
     192.168.1.1 dev enp3s0 lladdr 2c:fd:a1:67:2a:d0 STALE
     fe80::2efd:a1ff:fe67:2ad0 dev enp3s0 lladdr 2c:fd:a1:67:2a:d0 router DELAY


Boot the image in VirtualBox:
  • The first boot of Ubuntu Core requires a screen and keyboard (one reason we're trying this in VirtualBox). Subsequent logins will be done by ssh.
  • Answer the couple setup questions.
  • Use your Ubuntu One login e-mail address.
  • The VM will reboot itself (perhaps more than once) when complete.
  • Note you cannot login to the VM's TTY. Ubuntu Core's default login is via ssh. Instead, the VM's TTY tells you the IP address to use for ssh.
  • Since we are using a VM, this is a convenient place to take an initial snapshot. If you make a mess of networking in the next step, you can revert the snapshot.


Let's do some initial configuration:
  • After the VM reboots, the Virtualbox screen only shows the IP address.

  • // SSH into the Ubuntu Core Guest
    me@desktop:~$ ssh my-Ubuntu-One-login-name@IP-address
     [...Welcome message and MOTD...]
    me@localhost:~$
    
    // The default name is "localhost"
    // Let's change that. Takes effect after reboot.
    me@localhost:~$ sudo hostnamectl set-hostname 'ubuntu-core-vm'
    
    // Set the timezone. Takes effect immediately.
    me@localhost:~$ sudo timedatectl set-timezone 'America/Chicago'
    
    // OPTIONAL: Create a TTY login
    // This can be handy if you have networking problems.
    me@localhost:~$ sudo passwd my-Ubuntu-One-login-name


Let's set up the network bridge so containers can draw their IP address from the router:

  • We use vi to edit the netplan configuration. When we apply the changes, the ssh connection will be severed so we must discover the new IP address to login again.

  • me@localhost:~$ sudo vi /writable/system-data/etc/netplan/00-snapd-config.yaml
    
         #// The following seven lines are the original file. Commented instead of deleted.
         # This is the network config written by 'console_conf'
         #network:
         #  ethernets:
         #    eth0:
         #      addresses: []
         #      dhcp4: true
         #  version: 2
    
         #// The following lines are the new config 
         network:
           version: 2
           renderer: networkd
    
           ethernets:
             eth0:
               dhcp4: no
               dhcp6: no
    
           bridges:
             # br0 is the name that containers use as the parent
             br0:
               interfaces:
                 # eth0 is the device name in 'ip addr'
                 - eth0
               dhcp4: yes
               dhcp6: yes
         #// End
         
    
    // After the file is ready, implement it:
    me@localhost:~$ sudo netplan generate
    me@localhost:~$ sudo netplan apply
    
    // If all goes well...your ssh session just terminated without warning.
    


Test our new network settings:
  • The Ubuntu Core VM window will NOT change the displayed IP address after the netplan change...but that IP won't work anymore.
  • If you happen to reboot (not necessary) you will see that the TTY window displays no IP address when bridged...unless you have created an optional TTY login.
  • Instead of rebooting, let's take another network snapshot and compare to earlier:

         me@Desktop:~$ ip neigh
         192.168.1.226 dev enp3s0 lladdr c6:12:89:22:56:e4 STALE
         192.168.1.227 dev enp3s0 lladdr 00:1c:b3:75:23:a3 STALE
         192.168.1.234 dev enp3s0 lladdr d8:31:34:2c:b8:3a STALE  <---- NEW
         192.168.1.235 dev enp3s0 lladdr DELAY                    <-----NEW
         192.168.1.246 dev enp3s0 lladdr f4:f5:d8:29:e5:90 REACHABLE
         192.168.1.213 dev enp3s0 lladdr 98:e0:d9:77:5d:6b STALE
         192.168.1.1 dev enp3s0 lladdr 2c:fd:a1:67:2a:d0 STALE
         fe80::2efd:a1ff:fe67:2ad0 dev enp3s0 lladdr 2c:fd:a1:67:2a:d0 router DELAY

  • We have two new lines: .226 and .235 One of those was the old IP address and one is the new. SSH into the new IP address, and you're back in.

    me@desktop:~$ ssh my-Ubuntu-One-user-name@192.168.1.226
    Welcome to Ubuntu Core 18 (GNU/Linux 4.15.0-99-generic x86_64)
     [...Welcome message and MOTD...]
    Last login: Thu May  7 16:11:38 2020 from 192.168.1.6
    me@localhost:~$

  • Let's take a closer look at our new, successful network settings.

    me@localhost:~$ ip addr
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
        inet6 ::1/128 scope host
           valid_lft forever preferred_lft forever
    
    2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
        link/ether c6:12:89:22:56:e4 brd ff:ff:ff:ff:ff:ff
        inet 192.168.1.226/24 brd 192.168.1.255 scope global dynamic br0
           valid_lft 9545sec preferred_lft 9545sec
        inet6 2683:4000:a450:1678:c412:89ff:fe22:56e4/64 scope global dynamic mngtmpaddr noprefixroute
           valid_lft 600sec preferred_lft 600sec
        inet6 fe80::c412:89ff:fe22:56e4/64 scope link
           valid_lft forever preferred_lft forever
    
    3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master br0 state UP group default qlen 1000
        link/ether 08:00:27:fd:20:92 brd ff:ff:ff:ff:ff:ff
    
    // Note that ubuntu-core-vm now uses the br0 address, and lacks an eth0 address.
    // That's what we want.


Set up static IP addresses on the Router and then reboot to use the new IP address.
  • Remember, the whole point of bridged networking is for the router to issue all the IP addresses and avoid doing a lot of NATing and Port Forwarding.
  • So now is the time to login to the Router and have it issue a constant IP address to the Bridge MAC address (in this case c6:12:89:22:56:e4). After this, ubuntu-core-vm (the Ubuntu Core Guest VM) will always have a predictable IP address.
  • Use VirtualBox to ACPI shutdown the VM, then restart it headless. We're looking for two changes: The hostname and the login IP address.
  • Starting headless can be done two ways:

    1. GUI: Virtualbox Start button submenu
    2. me@Desktop:~$  VBoxHeadless --startvm name-of-vm

  • Success at rebooting headless and logging into the permanent IP address is a good point for another VM Snapshot. And maybe a sandwich. Well done!


Install LXD onto ubuntu-core-vm:
  • Install:

    me@ubuntu-core-vm:~$ snap install lxd
    lxd 4.0.1 from Canonical✓ installed
    me@ubuntu-core-vm:~$

  • Add myself to the `lxd` group so 'sudo' isn't necessary anymore. This SHOULD work, but doesn't due to a bug (discussion)

    host:~$ sudo adduser --extrausers me lxd     // Works on most Ubuntu; does NOT work on Ubuntu Core even with --extrausers
    host:~$ newgrp lxd                           // New group takes effect without logout/login

  • Instead, edit the groups file directly using vi:

    // Use vi to edit the file:
    me@ubuntu-core-vm:~$ sudo vi /var/lib/extrausers/group
    
         // Change the lxd line:
         lxd:x:999:               // Old Line
         lxd:x:999:my-login-name  // New Line
    
    
    // Apply the new group settings without logout
    me@ubuntu-core-vm:~$ newgrp lxd
Configure LXD:
  • LXD is easy to configure. We need to make three changes from the default settings since we already have a bridge (br0) set up that we want to use.

    me@ubuntu-core-vm:~$ lxd init
    
    Would you like to use LXD clustering? (yes/no) [default=no]:
    Do you want to configure a new storage pool? (yes/no) [default=yes]:
    Name of the new storage pool [default=default]:
    Name of the storage backend to use (dir, lvm, ceph, btrfs) [default=btrfs]:
    Create a new BTRFS pool? (yes/no) [default=yes]:
    Would you like to use an existing block device? (yes/no) [default=no]:
    Size in GB of the new loop device (1GB minimum) [default=15GB]:
    Would you like to connect to a MAAS server? (yes/no) [default=no]:
    Would you like to create a new local network bridge? (yes/no) [default=yes]: no    <------------------------- CHANGE
    Would you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]: yes   <-- CHANGE
    Name of the existing bridge or host interface: br0     <----------------------------------------------------- CHANGE
    Would you like LXD to be available over the network? (yes/no) [default=no]:
    Would you like stale cached images to be updated automatically? (yes/no) [default=yes]
    Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]:
    
    me@ubuntu-core-vm:~$
  • Next, we change the networking profile so containers use the bridge:

    // Open the default container profile in vi
    me@ubuntu-core-vm:~$ lxc profile edit default
    
         config: {}
         description: Default LXD profile
         devices:
           # Container eth0, not ubuntu-core-vm eth0
           eth0:
             name: eth0
             nictype: bridged
             # This is the ubuntu-core-vm br0, the real network connection
             parent: br0
             type: nic
           root:
             path: /
             pool: default
             type: disk
         name: default
         used_by: []
  • Add the Ubuntu-Minimal stream for cloud-images, so our test container is small:

    me@ubuntu-core-vm:~$ lxc remote add --protocol simplestreams ubuntu-minimal https://cloud-images.ubuntu.com/minimal/releases/
Create and start a Minimal container:
    me@ubuntu-core-vm:~$ lxc launch ubuntu-minimal:20.04 test1
    Creating test1
    Starting test1
    
    me@ubuntu-core-vm:~$ lxc list
    +-------+---------+----------------------+-----------------------------------------------+-----------+-----------+
    | NAME  |  STATE  |         IPV4         |                      IPV6                     |   TYPE    | SNAPSHOTS |
    +-------+---------+----------------------+-----------------------------------------------+-----------+-----------+
    | test1 | RUNNING | 192.168.1.248 (eth0) | 2603:6000:a540:1678:216:3eff:fef0:3a6f (eth0) | CONTAINER | 0         |
    +-------+---------+----------------------+-----------------------------------------------+-----------+-----------+
    
    
    // Let's test outbound connectivity from the container
    me@ubuntu-core-vm:~$ lxc shell test1
    root@test1:~# apt update
    Get:1 http://archive.ubuntu.com/ubuntu focal InRelease [265 kB]
    [...lots of succesful server connections...]
    Get:26 http://archive.ubuntu.com/ubuntu focal-backports/universe Translation-en [1280 B]
    Fetched 16.3 MB in 5s (3009 kB/s)
    Reading package lists... Done
    Building dependency tree...
    Reading state information... Done
    5 packages can be upgraded. Run 'apt list --upgradable' to see them.
    root@test1:~#

Wednesday, February 19, 2020

Pushing a file from Host into an to LXD Container

One of the little (and deliberate) papercuts of using unprivileged LXD containers is that unless data flows in from a network connection, it likely has the wrong owner and permissions.

Here are two examples in the HomeAssistant container.

1. The HA container needs to talk to a USB dongle elsewhere in the building. It does so using USBIP, and I discussed how to make it work in this previous post.

2. I want the HA container to display some performance data about the host (uptime, RAM used, similar excitements). Of course, it's a container, so it simply cannot do that natively without using lots of jiggerypokery to escape the container. Instead, a script collects the information and pushes the information into the container every few minutes.

     $ sudo lxc file push /path/to/host/file.json container-name/path/to/container/

Easy enough, right.

Well, not quite. Home Assistant, when installed, creates a non-root user, and puts all of it's files in a subdirectory. Add another directory to keep things simple, and you get:

     /home/homeassistant/.homeassistant/external_files/

And, unfortunately, all those subdirectories are owned by a non-root user. So lxc cannot 'push' all the way into them (result: permission error).

    -rw-r--r-- 1   root root  154 Feb 19 15:34 file.json

The pushed file can only be pushed to in the wrong location, and gets there with the wrong ownership.



Systemd to the rescue: Let's create a systemd job on the container that listens for a push, then fixes the location and the ownership.

The feature is called a systemd.path.

Like a systemd timer it consists of two parts, a trigger (.path) and a service that gets triggered.

The .path file is very simple. Here's what I used for the trigger:

[Unit]
# /etc/systemd/system/server_status.path
Description=Listener for a new server status file

[Path]
PathModified=/home/homeassistant/.homeassistant/file.json

[Install]
WantedBy=multi-user.target

The service file is almost as simple. Here's what I used:

[Unit]
# /etc/systemd/system/server_status.service
Description=Move and CHOWN the server status file

[Service]
Type=oneshot
User=root
ExecStartPre=mv /home/homeassistant/.homeassistant/file.json /home/homeassistant/.homeassistant/external_files/
ExecStart=chown homeassistant:homeassistant /home/homeassistant/.homeassistant/external_files/file.json

[Install]
WantedBy=multi-user.target

Finally, enable and start the path (not the service)

sudo systemctl daemon-reload
sudo systemctl enable server_status.path
sudo systemctl start server_status.path


Saturday, August 17, 2019

USBIP into an LXD container

In a previous post, I used USBIP to forawrd GPS data from A to B. 'A' was a USB GPS dongle pluged into a Raspberry Pi (Raspbian). 'B' was my laptop.

Now let's take it another step. Let's move 'B' to an LXD container sitting on a headless Ubuntu 19.04 server. No other changes: Same GPS data, same use of USBIP. 'A' is the same USB GPS dongle, the same Raspberry Pi, and the same Raspbian.

Setting up usbip on the server ('B') is identical to setting it up on my laptop. Recall that this particular dongle creates a /dev/ttyUSB_X device upon insertion, and it's the same on the Pi, the Laptop, and the Server

    me@server:~$ lsusb
        Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
        Bus 003 Device 006: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port
        Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
        Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
        Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

    me@server:~$ ls -l /dev/ttyUSB0
        crw-rw---- 1 root dialout 188, 0 Aug 17 21:13 /dev/ttyUSB0

LXD has a USB Hotplug feature that works for many, but not all USB devices, connecting USB devices on the host to the container. Devices that create a custom entry in /dev (like /dev/ttyUSB_X) generally cannot use the USB Hotplug...but CAN instead use 'usb-char' forwarding which (seems to be) NOT hotpluggable.

Here's that LXD magic at work. In this case, I'm using a container called 'ha-test2', and let's simply name the dongle 'gps'. Do this while the container is stopped, or restart the container afterward

    me@server:~$ lxc config device add ha-test2 gps unix-char path=/dev/ttyUSB0
        Device gps added to ha-test2

Now we start the container, and then jump into a shell inside. We see that /dev/ttyUSB0 has indeed been forwarded. And we test to ensure data is flowing -- that we can read from /dev/tty/USB0.

    me@server:~$ lxc start ha-test2
    me@server:~$ lxc shell ha-test2
        mesg: ttyname failed: No such device

        root@ha-test2:~# ls -l /dev/ | grep tty
            crw-rw-rw- 1 nobody nogroup   5,   0 Aug 18 02:11 tty
            crw-rw---- 1 root   root    188,   0 Aug 18 02:25 ttyUSB0

        root@ha-test2:~# apt install gpsd-clients   // Get the gpsmon application
        root@ha-test2:~# gpsmon /dev/ttyUSB0


Making it permanent

It is permanent already. The 'lxc config' command will edit the config of the container, which is persistent across a reboot.


Cleaning up


There are two options for cleanup of the container.
  • You can simply throw it away (it's a container)
  • Alternately,
     root@ha-test2:~# apt autoremove gpsd-clients

On the Server:

    me@server:~$ lxc config device remove ha-test2 gps
    me@server:~$ sudo apt autoremove gpsd-clients    // If you installed gpsmon to test connectivity

Also remember to detach USBIP, and uninstall usbip packages.

Monday, August 12, 2019

Experimenting with USB devices across the LAN with USBIP

USBIP is a Linux tool for accessing USB devices across a network. I'm trying it out.


At one end of the room, I have a Raspberry Pi with
  • A Philips USB Webcam
  • A no-name USB GPS dongle
  • A Nortek USB Z-Wave/Zigbee network controller dongle
At the other end of the room is my laptop.

Before starting anything, I plugged all three into another system to ensure that they worked properly.


Raspberry Pi Server Setup

The Pi is running stock Raspbian Buster, with the default "pi" user replaced by a new user ("me") with proper ssh keys.

Before we start, here's what the 'lsusb' looks like on the Pi

    me@pi:~ $ lsusb
        Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
        Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
        Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Now we plug in the three USB devices and see what changed

    me@pi:~ $ lsusb
        Bus 001 Device 004: ID 10c4:8a2a Cygnal Integrated Products, Inc. 
        Bus 001 Device 005: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port
        Bus 001 Device 006: ID 0471:0329 Philips (or NXP) SPC 900NC PC Camera / ORITE CCD Webcam(PC370R)
        Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
        Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
        Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

And here are the new devices created or modified

    me@pi:~ $ ls -l /dev | grep 12    // 12 is today's date
        drwxr-xr-x 4 root root          80 Aug 12 00:46 serial
        lrwxrwxrwx 1 root root           7 Aug 12 00:46 serial0 -> ttyAMA0
        drwxr-xr-x 4 root root         220 Aug 12 00:47 snd
        crw--w---- 1 root tty     204,  64 Aug 12 00:46 ttyAMA0
        crw-rw---- 1 root dialout 188,   0 Aug 12 00:46 ttyUSB0
        drwxr-xr-x 4 root root          80 Aug 12 00:47 v4l
        crw-rw---- 1 root video    81,   3 Aug 12 00:47 video0

Looks like...
  • /dev/ttyAMA0 is the Nortek Z-Wave controller
  • /dev/ttyUSB0 is the GPS stick
  • /dev/video0 is the webcam

Installing USBIP onto Raspbian Buster is easy. However, it is DIFFERENT from stock Debian or Ubuntu. This step is Raspbian-only

    me@pi:~$ sudo apt install usbip
Now load the kernel module. The SERVER always uses the module 'usbip_host'.

    me@pi:~$ sudo modprobe usbip_host     // does not persist across reboot

List the devices the usbip can see. Note each Bus ID - we'll need those later

    me@pi:~ $ usbip list --local
 - busid 1-1.1 (0424:ec00)
   Standard Microsystems Corp. : SMSC9512/9514 Fast Ethernet Adapter (0424:ec00)

 - busid 1-1.2 (0471:0329)
   Philips (or NXP) : SPC 900NC PC Camera / ORITE CCD Webcam(PC370R) (0471:0329)

 - busid 1-1.4 (067b:2303)
   Prolific Technology, Inc. : PL2303 Serial Port (067b:2303)

 - busid 1-1.5 (10c4:8a2a)
   Cygnal Integrated Products, Inc. : unknown product (10c4:8a2a)

  • We can ignore the Ethernet adapter
  • The Webcam is at 1-1.2
  • The GPS dongle is at 1-1.4
  • The Z-Wave Controller is at 1-1.5

Bind the devices.

    me@pi:~$ sudo usbip bind --busid=1-1.2        // does not persist across reboot
        usbip: info: bind device on busid 1-1.2: complete

    me@pi:~$ sudo usbip bind --busid=1-1.4        // does not persist across reboot
        usbip: info: bind device on busid 1-1.4: complete

    me@pi:~$ sudo usbip bind --busid=1-1.5        // does not persist across reboot
        usbip: info: bind device on busid 1-1.5: complete

The USB dongle will now appear to any client on the network just as though it was plugged in locally.

If you want to STOP serving a USB device:

    me@pi:~$ sudo usbip unbind --busid=1-1.2

The server (usbipd) process may or may not actually be running, serving on port 3240. Let's check:
    me@pi:~ $ ps -e | grep usbipd
        18966 ?        00:00:00 usbipd

    me@:~ $ sudo netstat -tulpn | grep 3240
        tcp        0      0 0.0.0.0:3240            0.0.0.0:*               LISTEN      18966/usbipd        
        tcp6       0      0 :::3240                 :::*                    LISTEN      18966/usbipd

We know that usbipd is active and listening. If not, start usbipd with:

    me@:~ $ sudo usbipd -D

You can run it more than one; only one daemon will start. The usbipd server does NOT need to be running to bind/unbind USB devices - you can start the server and bind/unbind in any order you wish. If you need to debug a connection, omit the -D (daemonize; fork into the background) so you can see the debug messages. See 'man usbipd' for the startup options to change port, IPv4, IPv6, etc.


Laptop Client Setup

Let's look at the USB devices on my laptop before starting:

    me@laptop:~$ lsusb
        Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
        Bus 001 Device 003: ID 04f2:b56c Chicony Electronics Co., Ltd 
        Bus 001 Device 002: ID 05e3:0608 Genesys Logic, Inc. Hub
        Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

In stock Debian (not Raspbian) and Ubuntu, usbip is NOT a separate package. It's included in the 'linux-tools-generic' package, which many folks already have installed...

    me@laptop:~$ apt list linux-tools-generic
        Listing... Done
        linux-tools-generic/disco-updates 5.0.0.23.24 amd64   // Doesn't say "[installed]"

...but apparently I don't. Let's install it.

    me@laptop:~$ sudo apt install linux-tools-generic

Now load the kernel module. The CLIENT always uses the kernel module 'vhci-hcd'.

    me@laptop:~$ sudo modprobe vhci-hcd     // does not persist across reboot

List the available USB devices on the Pi server (IP addr aa.bb.cc.dd). Those Bus IDs should look familiar.

    me@laptop:~$ usbip list -r aa.bb.cc.dd                        // List available on the IP address
        usbip: error: failed to open /usr/share/hwdata//usb.ids   // Ignore this error
        Exportable USB devices
        ======================
         - aa.bb.cc.dd
              1-1.5: unknown vendor : unknown product (10c4:8a2a)
                   : /sys/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5
                   : (Defined at Interface level) (00/00/00)
                   :  0 - unknown class / unknown subclass / unknown protocol (ff/00/00)
                   :  1 - unknown class / unknown subclass / unknown protocol (ff/00/00)


              1-1.4: unknown vendor : unknown product (067b:2303)
                   : /sys/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.4
                   : (Defined at Interface level) (00/00/00)

              1-1.2: unknown vendor : unknown product (0471:0329)
                   : /sys/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.2
                   : (Defined at Interface level) (00/00/00)

Now we attach the three USB devices. This will not persist across a reboot.

    me@laptop:~$ sudo usbip attach --remote=aa.bb.cc.dd --busid=1-1.2
    me@desktop:~$ sudo usbip attach --remote=aa.bb.cc.dd --busid=1-1.4
    me@desktop:~$ sudo usbip attach --remote=aa.bb.cc.dd --busid=1-1.5
    // No feedback upon success

The remote USB devices now show in 'lsusb'

    me@laptop:~$ lsusb
        Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
        Bus 003 Device 004: ID 10c4:8a2a Cygnal Integrated Products, Inc. 
        Bus 003 Device 003: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port
        Bus 003 Device 002: ID 0471:0329 Philips (or NXP) SPC 900NC PC Camera / ORITE CCD Webcam(PC370R)
        Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
        Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
        Bus 001 Device 003: ID 04f2:b56c Chicony Electronics Co., Ltd 
        Bus 001 Device 002: ID 05e3:0608 Genesys Logic, Inc. Hub
        Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

And we can see that new devices have appeared in /dev. Based upon the order we attached, it's likely that
  • The webcam 1-1.2 is at /dev/video2
  • The GPS dongle 1-1.4 is probably at /dev/ttyUSB0
  • The Z-Wave controller 1-1.5 is at /dev/ttyUSB1
  • The same dongle includes a Zigbee controller, too, at /dev/ttyUSB2
The Z-Wave/Zigbee controller has had it's major number changed from 204 to 188. We don't know if that's important or not yet.

    me@laptop:~$ ls -l /dev | grep 12
        drwxr-xr-x  4 root root            80 Aug 12 00:56 serial
        crw-rw----  1 root dialout 188,     0 Aug 12 00:56 ttyUSB0
        crw-rw----  1 root dialout 188,     1 Aug 12 00:56 ttyUSB1
        crw-rw----  1 root dialout 188,     2 Aug 12 00:56 ttyUSB2
        crw-rw----+ 1 root video    81,     2 Aug 12 00:56 video2


Testing Results

I tested the GPS using the 'gpsmon' application, included with the 'gpsd-clients' package. We don't actually need gpsd, we can connect gpsmon directly to the remote USB device.

    me@laptop:~$ gpsmon /dev/ttyUSB0
        gpsmon:ERROR: SER: device open of /dev/ttyUSB0 failed: Permission denied - retrying read-only
        gpsmon:ERROR: SER: read-only device open of /dev/ttyUSB0 failed: Permission denied

Aha, a permission issue, not a usbip failure!
Add myself to the 'dialout' group, and then it works. A second test across a VPN connection, from a remote location, was also successful.

    me@laptop:~$ ls -la /dev/ttyUSB0
        crw-rw---- 1 root dialout 188, 0 Aug 11 21:41 /dev/ttyUSB0    // 'dialout' group

    me@laptop:~$ sudo adduser me dialout
        Adding user `me' to group `dialout' ...
        Adding user me to group dialout
        Done.

    me@laptop:~$ newgrp dialout    // Prevents need to logout/login for new group to take effect

    me@laptop:~$ gpsmon /dev/ttyUSB0
    // Success!

The webcam is immediately recognized in both Cheese and VLC, and plays across the LAN instantly. There is a noticeable half-second lag. A second test, across a VPN connection from a remote location, had the USB device recognized but not enough signal was arriving in timely order for the applications to show the video.

There were a few hiccups along the way. The --debug flag helps a lot to track down the problems:
  • Client failed to connect with "system error" - turns out usbipd was not running on the server.
  • Client could see the list, but failed to attach with "attach failed" - needed to reboot the server (not sure why)
  • An active usbip connection prevents my laptop from sleeping properly
  • The Z-wave controller require HomeAssistant or equivalent to run, a bit more that I want to install onto the testing laptop. Likely to have permission issues, too.


Cleaning up

To tell a CLIENT to cease using a remote USB (virtual unplug), you need to know the usbip port number. Well, not really: We have made only one persistent change; we could simply reboot instead.

    me@laptop:~$ usbip port   // Not using sudo - errors, but still port numbers
        Imported USB devices
        ====================
        libusbip: error: fopen
        libusbip: error: read_record
        Port 00:  at Full Speed(12Mbps)
               Philips (or NXP) : SPC 900NC PC Camera / ORITE CCD Webcam(PC370R) (0471:0329)
               5-1 -> unknown host, remote port and remote busid
                   -> remote bus/dev 001/007
        libusbip: error: fopen
        libusbip: error: read_record
        Port 01:  at Full Speed(12Mbps)
               Prolific Technology, Inc. : PL2303 Serial Port (067b:2303)
               5-2 -> unknown host, remote port and remote busid
                   -> remote bus/dev 001/005
        libusbip: error: fopen
        libusbip: error: read_record
        Port 02:  at Full Speed(12Mbps)
               Cygnal Integrated Products, Inc. : unknown product (10c4:8a2a)
               5-3 -> unknown host, remote port and remote busid
                   -> remote bus/dev 001/006

    me@laptop:~$ sudo usbip port    // Using sudo, no errors and same port numbers
        Imported USB devices
        ====================
        Port 00: <port in use> at Full Speed(12Mbps)
               Philips (or NXP) : SPC 900NC PC Camera / ORITE CCD Webcam(PC370R) (0471:0329)
               5-1 -> usbip://aa.bb.cc.dd:3240/1-1.2
                   -> remote bus/dev 001/007
        Port 01: <port in use> at Full Speed(12Mbps)
               Prolific Technology, Inc. : PL2303 Serial Port (067b:2303)
               5-2 -> usbip://aa.bb.cc.dd:3240/1-1.4
                   -> remote bus/dev 001/005
        Port 02: <port in use> at Full Speed(12Mbps)
               Cygnal Integrated Products, Inc. : unknown product (10c4:8a2a)
               5-3 -> usbip://aa.bb.cc.dd:3240/1-1.5
                   -> remote bus/dev 001/006
 
    me@laptop:~$ sudo usbip detach --port 00
        usbip: info: Port 0 is now detached!

    me@laptop:~$ sudo usbip detach --port 01
        usbip: info: Port 1 is now detached!

    me@laptop:~$ sudo usbip detach --port 02
        usbip: info: Port 2 is now detached!

    me@laptop:~$ lsusb              // The remote USB devices are gone now
        Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
        Bus 001 Device 003: ID 04f2:b56c Chicony Electronics Co., Ltd 
        Bus 001 Device 002: ID 05e3:0608 Genesys Logic, Inc. Hub
        Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

    me@laptop:~$ sudo modprobe -r vhci-hcd    // Remove the kernel module

The only two persistent change we made on the CLIENT were adding myself to the 'dialout' group and installing the 'linux-tools-generic' package, so let's remove that. If you ALREADY were in the 'dialout' group, or had the package installed for other reasons, then obviously don't remove it. It's not the system's responsibility to keep track of why you have certain permissions or packages -- that's the human's job. After this step, my CLIENT is back to stock Ubuntu.

    me@laptop:~$ sudo deluser me dialout                  // Takes effect after logout
    me@laptop:~$ sudo apt autoremove linux-tools-generic  // Immediate

Telling a SERVER to stop sharing a USB device (virtual unplug) and shut down the server is much easier. Of course, this is also a Pi, and we did make any changes permanent, so it might be easier to simply reboot it.

    me@pi:~$ usbip list -l
         - busid 1-1.1 (0424:ec00)
           Standard Microsystems Corp. : SMSC9512/9514 Fast Ethernet Adapter (0424:ec00)

         - busid 1-1.2 (0471:0329)
           Philips (or NXP) : SPC 900NC PC Camera / ORITE CCD Webcam(PC370R) (0471:0329)

         - busid 1-1.4 (067b:2303)
           Prolific Technology, Inc. : PL2303 Serial Port (067b:2303)

         - busid 1-1.5 (10c4:8a2a)
           Cygnal Integrated Products, Inc. : unknown product (10c4:8a2a)

    me@pi:~$ sudo usbip unbind --busid=1-1.2
        usbip: info: unbind device on busid 1-1.2: complete
    me@pi:~$ sudo usbip unbind --busid=1-1.4
        usbip: info: unbind device on busid 1-1.4: complete
    me@pi:~$ sudo usbip unbind --busid=1-1.5
        usbip: info: unbind device on busid 1-1.5: complete

    me@pi:~$ sudo pkill usbipd

The only persistent change we made on the Pi is installing the 'usbip' package. Once removed, we're back to stock Raspbian.

    me@pi:~$ sudo apt autoremove usbip


Making it permanent

There are two additional steps to making a permanent server, and essentially the same two steps to make a permanent client. This means a USBIP server that begins serving automatically upon boot, and a client that automatically connects to the server upon boot.

Add the kernel modules to /etc/modules so that the USBIP kernel modules will be automatically loaded at boot. To remove a client or server, delete the line from /etc/modules. You don't need to use 'nano' - use any text editor you wish, obviously.

    me@pi:~$ sudo nano /etc/modules     // usbipd SERVER

        usbip_host

    me@laptop:~$ sudo nano /etc/modules     // usbip CLIENT

        usbip_vhci-hcd

    // Another way to add the USBIP kernel modules to /etc/modules on the SERVER
    me@pi:~$ sudo -s                            // "sudo echo" won't work
    me@pi:~# echo 'usbip_host' >> /etc/modules
    me@pi:~# exit

    // Another way to add the USBIP kernel modules to /etc/modules on the CLIENT
    me@pi:~$ sudo -s                            // "sudo echo" won't work
    me@pi:~# echo 'vhci-hcd' >> /etc/modules
    me@pi:~# exit

Add a systemd job to the SERVER to automatically bind the USB devices. You can use systemd to start, stop, and restart the server conveniently, and for to start serving at startup automatically.

    me@pi:~$ sudo nano /lib/systemd/system/usbipd.service

        [Unit]
        Description=usbip host daemon
        After=network.target

        [Service]
        Type=forking
        ExecStart=/usr/sbin/usbipd -D
        ExecStartPost=/bin/sh -c "/usr/sbin/usbip bind --$(/usr/sbin/usbip list -p -l | grep '#usbid=10c4:8a2a#' | cut '-d#' -f1)"
        ExecStop=/bin/sh -c "/usr/lib/linux-tools/$(uname -r)/usbip detach --port=$(/usr/lib/linux-tools/$(uname -r)/usbip port | grep '<port in use>' | sed -E 's/^Port ([0-9][0-9]).*/\1/')"

        [Install]
        WantedBy=multi-user.target

To start the new SERVER:
    me@pi:~$ sudo pkill usbipd                          // End the current server daemon (if any)
    me@pi:~$ sudo systemctl --system daemon-reload      // Reload system jobs because one changed
    me@pi:~$ sudo systemctl enable usbipd.service       // Set to run at startup
    me@pi:~$ sudo systemctl start usbipd.service        // Run now

Add a systemd job to the CLIENT to automatically attach the remote USB devices at startup. You can use systemd to unplug conveniently before sleeping, and to reset the connection of needed. Note: On the "ExecStart" line, substitute your server's IP address for aa.bb.cc.dd in two places.

    me@laptop:~$ sudo nano /lib/systemd/system/usbip.service

        [Unit]
        Description=usbip client
        After=network.target

        [Service]
        Type=oneshot
        RemainAfterExit=yes
        ExecStart=/bin/sh -c "/usr/bin/usbip attach -r aa.bb.cc.dd -b $(/usr/bin/usbip list -r aa.bb.cc.dd | grep '10c4:8a2a' | cut -d: -f1)"
        ExecStop=/bin/sh -c "/usr/bin/usbip detach --port=$(/usr/bin/usbip port | grep '<port in use>' | sed -E 's/^Port ([0-9][0-9]).*/\1/')"

        [Install]
        WantedBy=multi-user.target

To start the new CLIENT attachment(s):

    me@laptop:~$ sudo systemctl --system daemon-reload      // Reload system jobs because one changed
    me@laptop:~$ sudo systemctl enable usbip.service       // Set to run at startup
    me@laptop:~$ sudo systemctl start usbip.service        // Run now

Saturday, August 10, 2019

Experiment: Home Assistant in an LXD container without a venv

Update: August 2020 (one year later)

Here's a slightly different way of doing it entirely from the host. Tested with HomeAssistant version 114.

lxc launch -p lanprofile ubuntu:focal ha-test

# Update apt so we can install pip
cat <<EOF > /tmp/container-sources.list
deb http://us.archive.ubuntu.com/ubuntu/ focal main universe
deb http://us.archive.ubuntu.com/ubuntu/ focal-updates main universe
deb http://security.ubuntu.com/ubuntu focal-security main universe
EOF

lxc file push /tmp/container-sources.list ha-test/etc/apt/sources.list
lxc exec ha-test -- apt update
lxc exec ha-test -- apt upgrade

# Here's the meat: Installing pip3, then using pip3 to install HA and dependencies.
lxc exec ha-test -- apt install python3-pip
lxc exec ha-test -- pip3 install aiohttp_cors defusedxml emoji hass_nabucasa home-assistant-frontend homeassistant mutagen netdisco sqlalchemy zeroconf

# Example of fixing a version error message that occurs during pip install:
# ERROR: homeassistant 0.114.2 has requirement cryptography==2.9.2, but you'll have cryptography 2.8 which is incompatible.
lxc exec ha-test -- pip3 install --upgrade cryptography==2.9.2

# Can't start the web browser without knowing the container's IP address. 
lxc list | grep ha-test
   | ha-test       | RUNNING | 192.168.2.248 (eth0) |      | CONTAINER | 0         |

# Run Hass
lxc exec ha-test -- hass
   Unable to find configuration. Creating default one in /root/.homeassistant

# Web browser: http://192.168.2.248:8123....and there it is!


Home Assistant usually runs in a Python 3 virtual environment (venv). The developers wisely chose Python 3 because it has all the libraries they need. The developers wisely chose venv to create an effective single, predictable platform upon which Home Assistant can run. Users like it because just a couple extra shell incantations is the difference between success and cryptic-error hell.

Let's see if I can get HA 0.97 to run on Ubuntu 19.04. In this case, I'm running it in a disposable LXD container so I can just throw it away after the experiment is complete. This experiment turned out to be about 75% successful - Home Assistant installs and runs outside the venv, but logging and sqlalchemy failed to install, so the final product had some limitations.


Setup

First, let's create the LXD container. Step 1. Step 2. I use a networking profile ("lanprofile") that uses DHCP to request an IP address from my router instead of the local server. I'm using an Ubuntu 19.04 ("Disco") image for the container. And I'm calling the container "ha-test2," second in a line of Home Assistant test containers.

    me@host:~$ lxc launch -p lanprofile ubuntu:disco ha-test2

After a minute or two, the container is running and has picked up an IP address from the router.

    me@host:~$ lxc list
        +----------+---------+----------------------+-----
        |   NAME   |  STATE  |        IPV4          |
        +----------+---------+----------------------+-----
        | ha-test2 | RUNNING | 192.168.1.252 (eth0) |
        +----------+---------+----------------------+-----

Let's enter the container. Note that change to a root prompt within the container. This is an unprivileged container (LXD's default), so root within the container is NOT root for the rest of the system. Note also the mysterious "ttyname failed: No such device" error, due to a very minor bug but does not affect our use of the container in any way.

    me@host:~$ lxc shell ha-test2
        mesg: ttyname failed: No such device
        root@ha-test2:~#

OPTIONAL: Limit the Ubuntu sources. We don't need -resrticted or -multiverse or -proposed or -backports, etc. I replaced the entire file with the following three lines. Proper format is important!

    root@ha-test2:~# nano /etc/apt/sources.list

        deb http://archive.ubuntu.com/ubuntu disco main universe
        deb http://archive.ubuntu.com/ubuntu disco-updates main universe
        deb http://security.ubuntu.com/ubuntu disco-security main universe

OPTIONAL: Expand Unattended Upgrades to handle 100% of the limited sources. I replaced the entire file with the following five lines.

    root@ha-test2:~# nano /etc/apt/apt.conf.d/50unattended-upgrades 

        Unattended-Upgrade::Allowed-Origins {
            "${distro_id}:${distro_codename}";
            "${distro_id}:${distro_codename}-security";
            "${distro_id}:${distro_codename}-updates";
        };

Since this is the first run of the package manager...

    root@ha-test2:~# apt update
    root@ha-test2:~# apt upgrade

Home Assistant uses Python 3's pip, not debs. So we install pip.

    root@ha-test2:~# apt install python3-pip


First Try - Learning Curve

Now we can use pip to install Home Assistant. This command will run for a few minutes, and will produce a lot of output as it downloads many dependencies. Some of those installs output, at first glance, messages that seem like errors -- read them carefully, they are probably uninstall errors if packages were being upgraded...which they are not, of course.

    root@ha-test2:~# pip3 install homeassistant

The first run of 'hass' (the Home Assistant program name) is where we start to encounter errors that need to be investigated and fixed. When the system ground to a halt for several minutes, I used CTRL+C to end the process and return to a shell prompt.

    root@ha-test2:~# hass

        // Lots of success...but then:

        2019-08-09 22:35:57 INFO (MainThread) [homeassistant.bootstrap] Setting up {'system_log'}
        2019-08-09 22:35:57 INFO (SyncWorker_2) [homeassistant.util.package] Attempting install of aiohttp_cors==0.7.0
        2019-08-09 22:36:01 INFO (MainThread) [homeassistant.setup] Setting up http
        2019-08-09 22:36:01 ERROR (MainThread) [homeassistant.setup] Error during setup of component http
        Traceback (most recent call last):
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/setup.py", line 168, in _async_setup_component
            hass, processed_config
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/http/__init__.py", line 178, in async_setup
            ssl_profile=ssl_profile,
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/http/__init__.py", line 240, in __init__
            setup_cors(app, cors_origins)
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/http/cors.py", line 22, in setup_cors
            import aiohttp_cors
        ModuleNotFoundError: No module named 'aiohttp_cors'
        2019-08-09 22:36:01 ERROR (MainThread) [homeassistant.setup] Unable to set up dependencies of system_log. Setup failed for dependencies: http
        2019-08-09 22:36:01 ERROR (MainThread) [homeassistant.setup] Setup failed for system_log: Could not set up all dependencies.
        2019-08-09 22:36:01 INFO (SyncWorker_4) [homeassistant.util.package] Attempting install of sqlalchemy==1.3.5
        2019-08-09 22:36:11 INFO (MainThread) [homeassistant.setup] Setting up recorder
        Exception in thread Recorder:
        Traceback (most recent call last):
          File "/usr/lib/python3.7/threading.py", line 917, in _bootstrap_inner
            self.run()
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/recorder/__init__.py", line 211, in run
            from .models import States, Events
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/recorder/models.py", line 6, in 
            from sqlalchemy import (
        ModuleNotFoundError: No module named 'sqlalchemy'

        2019-08-09 22:36:21 WARNING (MainThread) [homeassistant.setup] Setup of recorder is taking over 10 seconds.

        // Thread hangs here. Use CTRL+C to abort back to a shell prompt

There are two errors there. Both are simply bugs in Home Assistant's list of dependencies. The developers neglected to include dependencies upon "aiohttp_cors" and "sqlalchemy". Let's uninstall all the pip packages and dependencies and start over. The dependencies are listed in the 'pip3 show' command. Remember to delete pip from the list of removals, and to add homeassistant. The pip3 uninstall command asks a lot of questions about deleting files and directories -- as long as the offered removals are in /usr/local, it won't break anything.

    root@ha-test2:~# pip3 show homeassistant
        Name: homeassistant
        Version: 0.97.1
        Summary: Open-source home automation platform running on Python 3.
        Home-page: https://home-assistant.io/
        Author: The Home Assistant Authors
        Author-email: hello@home-assistant.io
        License: Apache License 2.0
        Location: /usr/local/lib/python3.7/dist-packages
        Requires: pyyaml, async-timeout, bcrypt, voluptuous, voluptuous-serialize, importlib-metadata, ruamel.yaml, jinja2, cryptography, python-slugify, pip, PyJWT, requests, aiohttp, certifi, attrs, astral, pytz
        Required-by: 

    root@ha-test2:~# pip3 uninstall homeassistant pyyaml async-timeout bcrypt voluptuous voluptuous-serialize importlib-metadata ruamel.yaml jinja2 cryptography python-slugify PyJWT requests aiohttp certifi attrs astral pytz


Second Try - Getting closer

For the second try, let's add those two missing dependencies. This time, we successfully started logging and sqalchemy (success), and progressed to the next errors. The web server started, but the Home Assistant front end hosted on the webserver failed. The .homeassistant config directory was created and populated.

    root@ha-test2:~# pip3 install homeassistant aiohttp_cors sqlalchemy

        [lots of installing]

    root@ha-test2:~# hass

        2019-08-09 23:56:16 INFO (MainThread) [homeassistant.setup] Setting up onboarding
        2019-08-09 23:56:16 INFO (MainThread) [homeassistant.setup] Setup of domain config took 0.9 seconds.
        2019-08-09 23:56:16 INFO (MainThread) [homeassistant.setup] Setting up automation
        2019-08-09 23:56:16 INFO (MainThread) [homeassistant.setup] Setup of domain automation took 0.0 seconds.
        2019-08-09 23:56:16 INFO (MainThread) [homeassistant.setup] Setup of domain onboarding took 0.0 seconds.
        2019-08-09 23:56:20 ERROR (MainThread) [homeassistant.config] Unable to import ssdp: No module named 'netdisco'
        2019-08-09 23:56:20 ERROR (MainThread) [homeassistant.setup] Setup failed for ssdp: Invalid config.
        2019-08-09 23:56:20 INFO (SyncWorker_3) [homeassistant.util.package] Attempting install of distro==1.4.0
        2019-08-09 23:56:24 INFO (MainThread) [homeassistant.setup] Setting up updater
        2019-08-09 23:56:24 INFO (MainThread) [homeassistant.setup] Setup of domain updater took 0.0 seconds.
        2019-08-09 23:56:24 INFO (SyncWorker_1) [homeassistant.util.package] Attempting install of mutagen==1.42.0
        2019-08-09 23:56:29 INFO (SyncWorker_2) [homeassistant.loader] Loaded google_translate from homeassistant.components.google_translate
        2019-08-09 23:56:29 INFO (SyncWorker_3) [homeassistant.util.package] Attempting install of hass-nabucasa==0.16
        2019-08-09 23:56:50 INFO (MainThread) [homeassistant.setup] Setting up cloud
        2019-08-09 23:56:50 ERROR (MainThread) [homeassistant.setup] Error during setup of component cloud
        Traceback (most recent call last):
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/setup.py", line 168, in _async_setup_component
            hass, processed_config
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/cloud/__init__.py", line 167, in async_setup
            from hass_nabucasa import Cloud
        ModuleNotFoundError: No module named 'hass_nabucasa'
        2019-08-09 23:56:50 INFO (MainThread) [homeassistant.setup] Setting up mobile_app
        2019-08-09 23:56:50 ERROR (MainThread) [homeassistant.config] Unable to import zeroconf: No module named 'zeroconf'
        2019-08-09 23:56:50 ERROR (MainThread) [homeassistant.setup] Setup failed for zeroconf: Invalid config.
        2019-08-09 23:56:50 INFO (SyncWorker_2) [homeassistant.util.package] Attempting install of home-assistant-frontend==20190805.0
        2019-08-09 23:56:50 INFO (MainThread) [homeassistant.setup] Setup of domain mobile_app took 0.0 seconds.
        2019-08-09 23:56:50 INFO (SyncWorker_3) [homeassistant.loader] Loaded notify from homeassistant.components.notify
        2019-08-09 23:56:50 INFO (MainThread) [homeassistant.setup] Setting up notify
        2019-08-09 23:56:50 INFO (MainThread) [homeassistant.setup] Setup of domain notify took 0.0 seconds.
        2019-08-09 23:56:50 INFO (MainThread) [homeassistant.components.notify] Setting up notify.mobile_app
        2019-08-09 23:57:24 INFO (MainThread) [homeassistant.setup] Setting up frontend
        2019-08-09 23:57:24 ERROR (MainThread) [homeassistant.setup] Error during setup of component frontend
        Traceback (most recent call last):
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/setup.py", line 168, in _async_setup_component
            hass, processed_config
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/frontend/__init__.py", line 267, in async_setup
            root_path = _frontend_root(repo_path)
          File "/usr/local/lib/python3.7/dist-packages/homeassistant/components/frontend/__init__.py", line 244, in _frontend_root
            import hass_frontend
        ModuleNotFoundError: No module named 'hass_frontend'
        2019-08-09 23:57:24 INFO (SyncWorker_0) [homeassistant.util.package] Attempting install of gTTS-token==1.1.3
        2019-08-09 23:57:24 ERROR (MainThread) [homeassistant.setup] Unable to set up dependencies of logbook. Setup failed for dependencies: frontend
        2019-08-09 23:57:24 ERROR (MainThread) [homeassistant.setup] Setup failed for logbook: Could not set up all dependencies.
        2019-08-09 23:57:24 ERROR (MainThread) [homeassistant.setup] Unable to set up dependencies of map. Setup failed for dependencies: frontend
        2019-08-09 23:57:24 ERROR (MainThread) [homeassistant.setup] Setup failed for map: Could not set up all dependencies.
        2019-08-09 23:57:24 ERROR (MainThread) [homeassistant.setup] Unable to set up dependencies of default_config. Setup failed for dependencies: cloud, frontend, logbook, map, ssdp, zeroconf
        2019-08-09 23:57:24 ERROR (MainThread) [homeassistant.setup] Setup failed for default_config: Could not set up all dependencies.
        2019-08-09 23:57:30 INFO (MainThread) [homeassistant.setup] Setting up tts
        2019-08-09 23:57:30 INFO (SyncWorker_1) [homeassistant.components.tts] Create cache dir /root/.homeassistant/tts.
        2019-08-09 23:57:30 INFO (MainThread) [homeassistant.setup] Setup of domain tts took 0.0 seconds.
        2019-08-09 23:57:30 INFO (MainThread) [homeassistant.bootstrap] Home Assistant initialized in 87.48s
        2019-08-09 23:57:30 INFO (MainThread) [homeassistant.core] Starting Home Assistant
        2019-08-09 23:57:30 INFO (MainThread) [homeassistant.core] Timer:starting

We have two missing dependencies (netdisco and zeroconf), and a bunch of missing internal homeassistant functions. This looks a bit like a race condition - the setup script is expecting functions that aren't-quite-ready yet. This also explains why many of these errors do not appear during a subsequent run of hass.

Let's delete and try again with those two additional dependencies....
    root@ha-test2:~# pip3 uninstall homeassistant pyyaml async-timeout bcrypt voluptuous voluptuous-serialize importlib-metadata ruamel.yaml jinja2 cryptography python-slugify PyJWT requests aiohttp certifi attrs astral pytz aiohttp_cors sqlalchemy
    root@ha-test2:~# rm -r .homeassistant/


Third Try - Close enough to call it success

For the second try, let's add those two missing dependencies. This time, we successfully started logging and sqalchemy (success), and progressed to the next errors. The web server started, but the Home Assistant front end hosted on the webserver failed. The .homeassistant config directory was created and populated.

    root@ha-test2:~# pip3 install homeassistant aiohttp_cors sqlalchemy netdisco zeroconf

        [lots of installing]

    root@ha-test2:~# hass

        // No missing dependencies
        // Same setup errors

On the first run of hass, the dependency errors are gone, but the setup errors remain and the website is still unavailable. One the second run of hass, no errors at all, the website and all features work. The system is ready for systemd integration to bring hass up and down with the system.


Substituting Debs for Pips

Many of those pip dependencies are also available in Debian and Ubuntu. Let's try adding the debs, one by one, and see if we can reduce the number of pip dependencies. This is a separate experiment, obviously.

The process here is to delete homeassistant, it's pip dependencies, and it's config files, then replace Pips with Debs. We want to see if homeassistant pulls in the relevant pip anyway. If so, we can delete that pip, then see if homeassistant installs and initializes properly. That means that this experiment is not persistent - Home Assistant updates (like 0.97 to 0.98) will pull in all the removed pips again.

Several packages are already installed in the default Ubuntu 19.04 image, but are superseded by pips:
  • python3-certifi, python3-cryptography, python3-jinja2, python3-multidict, python3-requests, python3-yarl
Some packages are not available as debs at all. These are all dependencies of homeassistant:
  • attrs, homeassistant, importlib-metadata, PyJWT, pyyaml, zipp
Several packages, once installed, no longer pull in the pip:

    root@ha-test2:~# apt install python3-async-timeout python3-voluptuous-serialize

These packages, after installed, continue to pull in the pip anyway:

    root@ha-test2:~# apt install python3-aiohttp python3-aiohttp-cors python3-astral python3-async-timeout python3-bcrypt python3-python-slugify python3-ruamel.yaml python3-tz python3-voluptuous python3-voluptuous-serialize


After intalling all those debs, the homeassistant install looks something like this:

    root@ha-test2:~# pip3 install homeassistant
    root@ha-test2:~# pip3 uninstall aiohttp aiohttp_cors astral bcrypt certifi cryptography jinja2 multidict python-slugify pytz requests ruamel.yaml voluptuous yarl
    root@ha-test2:~# hass      // first time - no new install errors
    root@ha-test2:~# hass      // frontend works, no startup errors

Of course, this was an experiment - your mileage may vary. You may encounter problems that I did not. But it IS clearly possible to install Home Assistant into a non-venv environment, clearly possible to install Home Assistant into an LXD container, and clearly possible to more closely integrate Home Assistant into a Debian-based system.

Thursday, August 8, 2019

Creating an LXD container on my Ubuntu 19.04 host

I just finished setting up LXD on my Ubuntu 19.04 server, and I'm ready to create a container.

Installing the service into the container is a separate step - this is just setting up and configuring the container itself.

Creating a disposable container:

Actually, we did this already with our test container:

    me@host:~$ lxc launch -p lanprofile ubuntu:disco test

Let's see if that container is still there:

    me@host:~$ lxc list
        +----------------+---------+---------------------+-----------------------------------------------+------------+-----------+
        |      NAME      |  STATE  |        IPV4         |                     IPV6                      |    TYPE    | SNAPSHOTS |
        +----------------+---------+---------------------+-----------------------------------------------+------------+-----------+
        | test           | RUNNING | 192.168.1.124 (eth0)| 2615:a000:141f:e267:215:3eef:fe2a:c55d (eth0) | PERSISTENT | 0         
        |
        +----------------+---------+---------------------+-----------------------------------------------+------------+-----------+

We can enter the container to run commands on it's shell. Note that root inside the container is not root (unprivileged) on the host. The container comes with a default "ubuntu" user, but we have root so we don't seem to need the user.

    me@host:~$ lxc shell test
        mesg: ttyname failed: No such device  // Ignore this message
        root@test:~#                          // Look, a root prompt within the container!
        root@test:~# exit
            logout
    me@host:~$                                // Back to the host

We can stop and then restart containers. No sudo needed, these are unprivileged containers:

    me@host:~$ lxc stop test
    me@host:~$ lxc stop test

And when we are done we can destroy the container:

    me@host:~$ lxc stop test
    me@host:~$ lxc destroy test

Creating a long-term container:

Now I want to create a container for a long-term service. Now we add security: This means adding non-root users, independent ssh access, and package upgrades. This container can function like a lightweight VM, though with rather less overhead.

    me@host:~$ lxc launch -p lanprofile ubuntu:disco test_2

We can login to our LAN router, and see the test_2 device on the network. This is a good opportunity to assign it a consistent IP address, so you can always find the container again. Stop and restart the container so it picks up the new IP address.

Let's create a user for me with ssh access

    me@host:~$ lxc shell test_2
        mesg: ttyname failed: No such device     // Ignore this message

        root@test_2:~# adduser me                // Includes creating a password
        root@test_2:~# adduser me sudo           // Add me to the "sudo" group for easy remote administration via ssh
        root@test_2:~# nano /etc/ssh/sshd_config

           PasswordAuthentication yes            // Temporary while we set up ssh keys

        root@test_2:~# systemctl restart sshd
        root@test_2:~# exit

Copy my key. Remember to do this from ALL systems you are going to SSH into this container from:

    me@desktop:~$ ssh-copy-id me@192.168.1.124

Now I can ssh directly into the container using keys, so let's end password login.

    me@test_2:~$ sudo nano /etc/ssh/sshd_config

           PermitRootLogin no
           PasswordAuthentication no

    me@test_2:~$ sudo systemctl restart sshd

Remove the default "ubuntu" user, since we won't be using it.

    me@test_2:~$ sudo deluser ubuntu
    me@test_2:~$ sudo rm -r /home/ubuntu

Moving on to package management, simplify the apt sources so only -main and -universe are seen in -updates and -security. We only need what the installed service requires.

    me@test_2:~$ sudo nano /etc/apt/sources.list

        deb http://archive.ubuntu.com/ubuntu disco main universe
        deb http://archive.ubuntu.com/ubuntu disco-updates main universe
        deb http://security.ubuntu.com/ubuntu disco-security main universe

    me@test_2:~$ sudo apt update                        // Since the sources have changed
    me@test_2:~$ sudo apt upgrade                       // Now is a good time

Finally, let's install unattended-upgrades and configure it to upgrade ALL packages from our limited apt sources. This means we are less likely to discover months of unapplied upgrades and security fixes. This is optional, merely my preference:

    me@test_2:~$ sudo apt install unattended-upgrades
    me@test_2:~$ sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

        // Uncomment the following two lines:
        "${distro_id}:${distro_codename}-security";
        "${distro_id}:${distro_codename}-updates";

And there we have it - a long-term container that is easily (but securely) accessed via ssh for maintenance and automatically pulls package updates. Lightweight VM-like behavior with a consistent IP address. Note that "lxc shell" on the host will still give a root prompt, but recall that the purpose of a container is to keep the service from getting out, not to keep host from getting in. Also note that, due to macvlan networking, the container cannot communicate across the networkk with the host.

How I set up LXD on my Ubuntu 19.04 server

I have a lovely little server that is slowly filling with LXD containers.

Here is how I set up LXD on the server (host).

Install LXD:

My host started as a 19.04 minimal install, so snapd wasn't included. LXD is packaged only for snap now (the deb simply installs the snap).
These references were extremely helpful. Read (or re-read) them: reference 1 reference 2

    host:~$ sudo apt install snapd
    host:~$ sudo snap install lxd
    host:~$ sudo adduser me lxd     // Add me to the LXD group
    host:~$ newgrp lxd              // New group takes effect without logout/login

First Run:

The very first time you run LXD, it must be initialized. It asks a set of questions to set up the default profile. I find that the defaults are quite satisfactory, with one exception - I named the storage:

    host:~$ lxd init               // First run of LXD only - creates profile
        Would you like to use LXD clustering? (yes/no) [default=no]:
        Do you want to configure a new storage pool? (yes/no) [default=yes]:
        Name of the new storage pool [default=default]: container_storage
        Name of the storage backend to use (btrfs, ceph, dir, lvm, zfs) [default=zfs]:
        Create a new ZFS pool? (yes/no) [default=yes]:
        Would you like to use an existing block device? (yes/no) [default=no]:
        Size in GB of the new loop device (1GB minimum) [default=15GB]:
        Would you like to connect to a MAAS server? (yes/no) [default=no]:
        Would you like to create a new local network bridge? (yes/no) [default=yes]:
        What should the new bridge be called? [default=lxdbr0]:
        What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
        What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
        Would you like LXD to be available over the network? (yes/no) [default=no]:
        Would you like stale cached images to be updated automatically? (yes/no) [default=yes]:
        Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]:

My Preferences:

Yours preferences may vary.
  1. I prefer nano over vi for the default text editor. I know it's silly to have such a preference, but I do.
  2. My containers get their IP address from the LAN router instead of the host, using macvlan. This means that containers can talk to the LAN, and to each other, but not to the host. Personally, I see this as a feature, not a bug.

Set default editor as nano (instead of vi). This is obviously nothing but catering to my personal taste, and has no effect on other steps:

    host:~$ echo 'export EDITOR=nano' >> ~/.profile
    host:~$ source ~/.profile

Change the networking profile from default (NAT) to instead pull IP addresses for each container from the LAN router (macvlan). This is a matter of personal taste - it simply means I have one place to set IP addresses, the router, for all devices and containers. This only works with wired networking...if you are using wifi to connect a server full of containers to the LAN, then you really should rethink your plan anyway! (Reference)

    host:~$ ip route show default 0.0.0.0/0                          // Learn the eth interface
        default via 192.168.2.1 dev enp0s3 proto dhcp metric 600     // Mine is enp0s3 

    host:~$ lxc profile copy default lanprofile                      // Make mistakes on a copy, not the original
    host:~$ lxc profile device set lanprofile eth0 nictype macvlan   // Change nictype field
    host:~$ lxc profile device set lanprofile eth0 parent enp0s3     // Change parent field to real eth interface

Test:

Now that LXD is installed and configured, we can set up an unprivileged test container. An "unprivileged" container means that the container runs as an ordinary user on the larger system - if a process escapes the container, it has only normal (non-sudo, non-root) user permissions. LXD creates unprivileged containers by default so this part is pretty easy. Let's use the "lanprofile" networking profile we just created. Let's use Ubuntu Disco (19.04). And let's call the container "test":

    host:~$ lxc launch -p lanprofile ubuntu:disco test

The container is now running. Login to the LAN's router (or wherever your DHCP server is), and see that it's there among the dhcp clients.

That's all for LXD setup, Now I'm ready to create containers and fill them with services.