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
Generating Initial Models
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: **
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 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
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