After one year working with an amazing dev team building interactive demos for large events and running into administration issues dealing with the complexity, it started to make sense to look into bootstrapping a tool to help keep track. It started while troubleshooting a permanent physical installation in a new skyscraper: two US National Park style viewfinders wired to 360 cameras on the roof. The install uses two Samsung phones locked into an enclosure with too many security screws. It's hard to access, it lives on a segmented network, and it goes down all the freaking time.
Our proposed architecture is a binary that we can deploy on devices and a Rails API to receive and store the information.
We need a laundry list of data for this to scale across multiple events, with multiple demos at each event, and each demo itself containing multiple devices that in turn contain multiple network interfaces. We have to network with all of them for any remote support. It's a nesting doll of systems administration pain. Ignoring the top few bits of the layer cake, we can focus on just these three objects: Demo, Device, and Nic (Network Interface Controller). Demos can have many devices, and devices can have many Nics.
The binary is written in golang for easy cross-compiling (this specific event used Android, iOS, Windows, and Linux). Currently the "agent" collects a hostname, screenshot, mac addresses and IP addresses and hands them off either directly to our API or to a Lambda function middleware.
This quick case study will focus mostly on the associations required to handle the complex relations.
rails new <app name> --database=postgresql
Using the custom multiple database postgres docker image:
docker run -id --name rails-db -e POSTGRES_MULTIPLE_DATABASES="railyard-dev","railyard-test","railyard-prod" -e POSTGRES_USER=<database user> -e POSTGRES_PASSWORD=<password> -p 5432:5432 db:latest
#Gemfile
gem 'dotenv'
#/config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: 5
timeout: 5000
username: worker
password: foobarbat
host: 127.0.0.1
port: 5432
development:
<<: *default
database: railyard-dev
test:
<<: *default
database: railyard-test
production:
<<: *default
database: railyard-prod
The first step is generating the two models that we need: one for Demos, one for Devices, and one for Nics. Each has slightly different schema:
rails generate model Demo name:string location:string
rails generate model Device hostname:string
rails generate model Nic ip_addr:string mac_addr:string iface_name:string
The next step is to link the models using these "polymorphic" tables (I think). I'm not sure if this is the best way, but coming from Django it's the only thing that works and makes sense. It's kind of funky, but it seems to work pretty well.
rails generate model DemosDevice
rails generate model DevicesNic
Inside the migrations that these last two generate, we need to add:
# DemosDevice
class CreateDemosDevices < ActiveRecord::Migration[5.2]
def change
create_table :demos_devices do |t|
t.belongs_to :demo, index: true
t.belongs_to :device, index: true
t.timestamps
end
end
end
# DevicesNic
class CreateDevicesNics < ActiveRecord::Migration[5.2]
def change
create_table :devices_nics do |t|
t.belongs_to :device, index: true
t.belongs_to :nic, index: true
t.timestamps
end
end
end
** Define the Associations: **
Now inside app/models
, we need to add the polymorphic association to the individual models using a trick. The basic format is this:
# app/models/device.rb
class Device < ApplicationRecord
has_many :devices_nics
has_many :nics, through: :devices_nics
end
The two lines we add are key! The first creates a new has_many
relation (many-to-one), and then links it to the :nics
model.
Nics are slightly different, because they can only have one device:
# app/models/nic.rb
class Nic < ApplicationRecord
has_one :device, through: :devices_nics
end
And the last piece is the DevicesNic
middleware model:
# app/models/devices_nic.rb
class DevicesNic < ApplicationRecord
belongs_to :device
belongs_to :nic
end
The same thing gets repeated for DemosDevices
, with Demos having many devices
through demos_devices
, etc.
Generating the models and creating the associations manually is labor intensive and unintuitive, but creating objects in the Controllers is a little easier. Because I am passing a large data object with lots of information directly to the API, we just need the ability to create the nested associations in the parents. Of course, Rails has its own idiosyncracies with this as well. The idea is that we need to wait until the parent has been created and saved, and then construct the child from data within the same request.
It looks like this:
# app/controllers/device_controller.rb
# POST /devices
def create
body = JSON.parse request.raw_post # Get Raw Request Body
id = body['id']
ip_addrs = body['ip_addrs']
@device = Device.new(:hostname => id) # Create new Device
if @device.save # Wait for Device to Save
ip_addrs.each do |ip|
@device.nics.create(ip) # Create Nic
end
render json: @device.as_json(include:[:nics]), status: :created, location: @device
else
render json: @device.errors, status: :unprocessable_entity
end
end