Merge branch 'master' into global_messages
commit
f60a56c894
|
@ -2,15 +2,18 @@ class SendNervesHubInfoJob < ApplicationJob
|
|||
queue_as :default
|
||||
|
||||
def perform(device_id:, serial_number:, tags:)
|
||||
device = Device.find(device_id)
|
||||
resp_data = NervesHub.create_or_update(serial_number, tags)
|
||||
certs = NervesHub.sign_device(resp_data.fetch(:identifier))
|
||||
device = Device.find(device_id)
|
||||
resp_data = NervesHub.maybe_create_or_update(serial_number, tags)
|
||||
unless resp_data # Probably has bad tags if nil
|
||||
return
|
||||
end
|
||||
certs = NervesHub.sign_device(resp_data.fetch(:identifier))
|
||||
Transport.current.amqp_send(certs.to_json, device_id, "nerves_hub")
|
||||
rescue => error
|
||||
NervesHub.report_problem({ error: error,
|
||||
device_id: device_id,
|
||||
NervesHub.report_problem({ error: error,
|
||||
device_id: device_id,
|
||||
serial_number: serial_number,
|
||||
tags: tags, })
|
||||
tags: tags })
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
require "net/http"
|
||||
require "openssl"
|
||||
require "base64"
|
||||
|
||||
class NervesHub
|
||||
class NervesHubHTTPError < StandardError; end
|
||||
|
||||
# There is a lot of configuration available in this class to support:
|
||||
# * Self Hosters
|
||||
# * Running a local instance of Nerves-Hub
|
||||
|
@ -41,27 +43,27 @@ class NervesHub
|
|||
# via AMQP.
|
||||
# * FarmBot burns that cert into internal storage on it's SD card.
|
||||
|
||||
NERVES_HUB_HOST = ENV.fetch("NERVES_HUB_HOST") { "api.nerves-hub.org" }
|
||||
NERVES_HUB_PORT = ENV.fetch("NERVES_HUB_PORT") { 443 }
|
||||
NERVES_HUB_ORG = ENV.fetch("NERVES_HUB_ORG") { "farmbot" }
|
||||
NERVES_HUB_BASE_URL = "https://#{NERVES_HUB_HOST}:#{NERVES_HUB_PORT}"
|
||||
NERVES_HUB_URI = URI.parse(NERVES_HUB_BASE_URL)
|
||||
NERVES_HUB_HOST = ENV.fetch("NERVES_HUB_HOST") { "api.nerves-hub.org" }
|
||||
NERVES_HUB_PORT = ENV.fetch("NERVES_HUB_PORT") { 443 }
|
||||
NERVES_HUB_ORG = ENV.fetch("NERVES_HUB_ORG") { "farmbot" }
|
||||
NERVES_HUB_BASE_URL = "https://#{NERVES_HUB_HOST}:#{NERVES_HUB_PORT}"
|
||||
NERVES_HUB_URI = URI.parse(NERVES_HUB_BASE_URL)
|
||||
|
||||
# Locations of where files _may_ exist.
|
||||
NERVES_HUB_CERT_PATH = "nerves_hub_cert.#{Rails.env}.pem"
|
||||
NERVES_HUB_KEY_PATH = "nerves_hub_key.#{Rails.env}.pem"
|
||||
NERVES_HUB_CA_PATH = "nerves_hub_ca.#{Rails.env}.pem"
|
||||
NERVES_HUB_CERT_PATH = "nerves_hub_cert.#{Rails.env}.pem"
|
||||
NERVES_HUB_KEY_PATH = "nerves_hub_key.#{Rails.env}.pem"
|
||||
NERVES_HUB_CA_PATH = "nerves_hub_ca.#{Rails.env}.pem"
|
||||
|
||||
# This file is for loading the CA from ENV.
|
||||
# net/http doesn't support loading this as a X509::Certificate
|
||||
# So it needs to be written to a path.
|
||||
NERVES_HUB_CA_HACK = "/tmp/nerves_hub_ca.#{Rails.env}.pem"
|
||||
NERVES_HUB_ERROR = "NervesHub request failed: %s: %s"
|
||||
COLON = ":"
|
||||
BAD_TAG = "A device sent a malformed tag"
|
||||
NERVES_HUB_CA_HACK = "/tmp/nerves_hub_ca.#{Rails.env}.pem"
|
||||
NERVES_HUB_ERROR = "NervesHub request failed: %s: %s"
|
||||
COLON = ":"
|
||||
BAD_TAG = "A device sent a malformed tag"
|
||||
|
||||
# HEADERS for HTTP requests to NervesHub
|
||||
HEADERS = {"Content-Type" => "application/json"}
|
||||
HEADERS = { "Content-Type" => "application/json" }
|
||||
DEFAULT_HTTP = Net::HTTP.new(NERVES_HUB_URI.host, NERVES_HUB_URI.port)
|
||||
|
||||
# Raises an exception for when NervesHub API requests fail.
|
||||
|
@ -70,7 +72,7 @@ class NervesHub
|
|||
end
|
||||
|
||||
APPLICATION = "application"
|
||||
CHANNEL = "channel"
|
||||
CHANNEL = "channel"
|
||||
|
||||
def self.update_channel(serial_number, channel)
|
||||
dev = device(serial_number)
|
||||
|
@ -80,19 +82,19 @@ class NervesHub
|
|||
# NEVER DUPLICATE TAG PREFIXES (thing before COLON). Must be unique!
|
||||
tag_map = dev.fetch(:tags).map { |x| x.split(COLON) }.to_h
|
||||
tag_map[CHANNEL] = channel
|
||||
next_tags = tag_map.to_a.map { |x| x.join(COLON) }
|
||||
next_tags = tag_map.to_a.map { |x| x.join(COLON) }
|
||||
update(serial_number, next_tags)
|
||||
end
|
||||
|
||||
# Checks if a device exists in NervesHub
|
||||
# if it does -> does a PUT request updating the tags.
|
||||
# if it does not -> does a POST request creating the device with given tags.
|
||||
def self.create_or_update(serial_number, tags)
|
||||
def self.maybe_create_or_update(serial_number, tags)
|
||||
# Hash | nil
|
||||
current_nerves_hub_device = device(serial_number)
|
||||
|
||||
# It's really hard to debug malformed tags; Catch them here:
|
||||
if tags.detect{|x| !x.include?(COLON) }
|
||||
if tags.detect { |x| !x.include?(COLON) }
|
||||
report_problem(error: BAD_TAG, serial_number: serial_number, tags: tags)
|
||||
return
|
||||
end
|
||||
|
@ -119,7 +121,7 @@ class NervesHub
|
|||
|
||||
# PUT request to a device to update it's tags.
|
||||
def self.update(serial_number, tags)
|
||||
data = {tags: tags}
|
||||
data = { tags: tags }
|
||||
resp = conn.put(device_path(serial_number), data.to_json, HEADERS)
|
||||
bad_http(resp.code, resp.body) if resp.code != "201"
|
||||
JSON(resp.body)["data"].deep_symbolize_keys
|
||||
|
@ -129,8 +131,8 @@ class NervesHub
|
|||
# to identify the ENV that FarmBotOS is running in.
|
||||
def self.new_device(serial_number, tags)
|
||||
data = { description: "farmbot-#{serial_number}",
|
||||
identifier: serial_number,
|
||||
tags: tags }
|
||||
identifier: serial_number,
|
||||
tags: tags }
|
||||
resp = conn.post(devices_path, data.to_json, HEADERS)
|
||||
bad_http(resp.code, resp.body) if resp.code != "201"
|
||||
JSON(resp.body)["data"].deep_symbolize_keys
|
||||
|
@ -146,14 +148,14 @@ class NervesHub
|
|||
csr_safe = Base64.strict_encode64(csr.to_pem)
|
||||
|
||||
data = { identifier: serial_number,
|
||||
csr: csr_safe, }
|
||||
csr: csr_safe }
|
||||
resp = conn.post(device_sign_path(serial_number), data.to_json, HEADERS)
|
||||
bad_http(resp.code, resp.body) if resp.code != "200"
|
||||
cert = JSON(resp.body)["data"].deep_symbolize_keys[:cert]
|
||||
|
||||
return { cert: Base64.strict_encode64(cert),
|
||||
csr: csr_safe,
|
||||
key: key_safe, }
|
||||
csr: csr_safe,
|
||||
key: key_safe }
|
||||
end
|
||||
|
||||
# Is the NervesHub module configured.
|
||||
|
@ -163,16 +165,16 @@ class NervesHub
|
|||
end
|
||||
|
||||
def self.set_conn(obj = DEFAULT_HTTP)
|
||||
@conn = obj
|
||||
@conn = obj
|
||||
# Setting the contents of this
|
||||
# in the CA store doesn't work for some reason?
|
||||
@conn.ca_file = self.current_ca_file
|
||||
@conn.ca_file = self.current_ca_file
|
||||
# Don't think this is absolutely needed.
|
||||
@conn.cert_store = nil
|
||||
@conn = obj
|
||||
@conn.use_ssl = true
|
||||
@conn.cert = current_cert
|
||||
@conn.key = current_key
|
||||
@conn = obj
|
||||
@conn.use_ssl = true
|
||||
@conn.cert = current_cert
|
||||
@conn.key = current_key
|
||||
@conn
|
||||
end
|
||||
|
||||
|
@ -181,7 +183,7 @@ class NervesHub
|
|||
(active? && !@conn) ? set_conn : @conn
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
# Helper for making requests to a device url on NervesHub
|
||||
def self.devices_path(*chunks)
|
||||
|
@ -216,7 +218,7 @@ private
|
|||
request = OpenSSL::X509::Request.new
|
||||
request.version = 0
|
||||
request.subject = OpenSSL::X509::Name.new([
|
||||
['O', options[:organization], OpenSSL::ASN1::UTF8STRING],
|
||||
["O", options[:organization], OpenSSL::ASN1::UTF8STRING],
|
||||
])
|
||||
request.public_key = real_public_key(key)
|
||||
request.sign(key, OpenSSL::Digest::SHA1.new)
|
||||
|
@ -236,7 +238,7 @@ private
|
|||
|
||||
# Cert for authenticating Farmbot API (NOT FARMBOT OS) to NervesHub
|
||||
def self.try_env_cert
|
||||
OpenSSL::X509::Certificate.new(ENV['NERVES_HUB_CERT']) if ENV['NERVES_HUB_CERT']
|
||||
OpenSSL::X509::Certificate.new(ENV["NERVES_HUB_CERT"]) if ENV["NERVES_HUB_CERT"]
|
||||
end
|
||||
|
||||
# Cert for authenticating Farmbot API (NOT FARMBOT OS) to NervesHub
|
||||
|
@ -249,7 +251,7 @@ private
|
|||
|
||||
# Cert for authenticating Farmbot API (NOT FARMBOT OS) to NervesHub
|
||||
def self.try_env_key
|
||||
OpenSSL::PKey::EC.new(ENV['NERVES_HUB_KEY']) if ENV['NERVES_HUB_KEY']
|
||||
OpenSSL::PKey::EC.new(ENV["NERVES_HUB_KEY"]) if ENV["NERVES_HUB_KEY"]
|
||||
end
|
||||
|
||||
# Private Key for authenticating Farmbot API (NOT FARMBOT OS) to NervesHub
|
||||
|
@ -276,9 +278,9 @@ private
|
|||
# loading a file from the filesystem.
|
||||
# https://stackoverflow.com/questions/36993208/how-to-enumerate-through-multiple-certificates-in-a-bundle
|
||||
def self.try_env_ca_file
|
||||
if ENV['NERVES_HUB_CA']
|
||||
file = File.open(NERVES_HUB_CA_HACK, 'w')
|
||||
file.write(ENV['NERVES_HUB_CA'])
|
||||
if ENV["NERVES_HUB_CA"]
|
||||
file = File.open(NERVES_HUB_CA_HACK, "w")
|
||||
file.write(ENV["NERVES_HUB_CA"])
|
||||
file.close
|
||||
NERVES_HUB_CA_HACK
|
||||
end
|
||||
|
|
|
@ -4,18 +4,32 @@ describe SendNervesHubInfoJob do
|
|||
let(:device) { FactoryBot.create(:device) }
|
||||
|
||||
it "handles failure" do
|
||||
params = { device_id: device.id,
|
||||
serial_number: "xyz",
|
||||
tags: [],
|
||||
error: StandardError.new("Hello!"), }
|
||||
not_work = \
|
||||
receive(:create_or_update).with(any_args).and_raise(params.fetch(:error))
|
||||
params = { device_id: device.id,
|
||||
serial_number: "xyz",
|
||||
tags: [],
|
||||
error: StandardError.new("Hello!") }
|
||||
not_work = receive(:maybe_create_or_update)
|
||||
.with(any_args)
|
||||
.and_raise(params.fetch(:error))
|
||||
expect(NervesHub).to not_work
|
||||
old_logger = ActiveJob::Base.logger
|
||||
old_logger = ActiveJob::Base.logger
|
||||
ActiveJob::Base.logger = Logger.new(nil)
|
||||
expect do
|
||||
SendNervesHubInfoJob.perform_now(**params.except(:error))
|
||||
end.to raise_error(params.fetch(:error))
|
||||
ActiveJob::Base.logger = old_logger
|
||||
end
|
||||
|
||||
it "returns early if create/update is nil" do
|
||||
params = { device_id: device.id,
|
||||
serial_number: "xyz",
|
||||
tags: [],
|
||||
error: StandardError.new("Hello!") }
|
||||
return_nil = receive(:maybe_create_or_update)
|
||||
.with(any_args)
|
||||
.and_return(nil)
|
||||
expect(NervesHub).to return_nil
|
||||
expect(NervesHub).not_to receive(:sign_device)
|
||||
SendNervesHubInfoJob.perform_now(**params.except(:error))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,14 +2,15 @@ require "spec_helper"
|
|||
|
||||
describe NervesHub do
|
||||
def stub_connection
|
||||
double(SecureRandom.hex.first(6), :ca_file= => nil,
|
||||
:cert_store => nil,
|
||||
double(SecureRandom.hex.first(6), :ca_file= => nil,
|
||||
:cert_store => nil,
|
||||
:cert_store= => nil,
|
||||
:use_ssl => nil,
|
||||
:use_ssl= => nil,
|
||||
:cert= => nil,
|
||||
:key= => nil)
|
||||
:use_ssl => nil,
|
||||
:use_ssl= => nil,
|
||||
:cert= => nil,
|
||||
:key= => nil)
|
||||
end
|
||||
|
||||
before(:each) { NervesHub.set_conn(stub_connection) }
|
||||
|
||||
# reset to default.
|
||||
|
@ -20,14 +21,13 @@ describe NervesHub do
|
|||
|
||||
it "generates HTTP failure messages" do
|
||||
status = "800"
|
||||
msg = "failed to reticulate splines."
|
||||
expect { NervesHub.bad_http(status, msg) }
|
||||
.to raise_error(NervesHub::NervesHubHTTPError)
|
||||
msg = "failed to reticulate splines."
|
||||
expect { NervesHub.bad_http(status, msg) }.to raise_error(NervesHub::NervesHubHTTPError)
|
||||
end
|
||||
|
||||
it "generates URL paths" do
|
||||
expect(NervesHub.devices_path).to eq "/orgs/farmbot/devices"
|
||||
expect(NervesHub.device_path("foo")).to eq "/orgs/farmbot/devices/foo"
|
||||
expect(NervesHub.devices_path).to eq "/orgs/farmbot/devices"
|
||||
expect(NervesHub.device_path("foo")).to eq "/orgs/farmbot/devices/foo"
|
||||
expect(NervesHub.device_sign_path(123)).to eq "/orgs/farmbot/devices/123/certificates/sign"
|
||||
end
|
||||
|
||||
|
@ -53,21 +53,20 @@ describe NervesHub do
|
|||
end
|
||||
|
||||
it "handles failed updates to a device" do
|
||||
resp = StubResp.new("500", { "data" => { } }.to_json)
|
||||
resp = StubResp.new("500", { "data" => {} }.to_json)
|
||||
expected_args = [NervesHub.device_path(ser),
|
||||
{"tags":["foo"]}.to_json,
|
||||
{ "tags": ["foo"] }.to_json,
|
||||
NervesHub::HEADERS]
|
||||
|
||||
expect(NervesHub.conn).to receive(:put).with(*expected_args).and_return(resp)
|
||||
|
||||
expect { NervesHub.update(ser, ["foo"]) }
|
||||
.to raise_error(NervesHub::NervesHubHTTPError)
|
||||
expect { NervesHub.update(ser, ["foo"]) }.to raise_error(NervesHub::NervesHubHTTPError)
|
||||
end
|
||||
|
||||
it "updates the device via REST" do
|
||||
resp = StubResp.new("201", { "data" => {x: "y"} }.to_json)
|
||||
resp = StubResp.new("201", { "data" => { x: "y" } }.to_json)
|
||||
expected_args = [NervesHub.device_path(ser),
|
||||
{"tags":["foo"]}.to_json,
|
||||
{ "tags": ["foo"] }.to_json,
|
||||
NervesHub::HEADERS]
|
||||
|
||||
expect(NervesHub.conn).to receive(:put).with(*expected_args).and_return(resp)
|
||||
|
@ -85,18 +84,18 @@ describe NervesHub do
|
|||
it "calls `new_device` if device does not exist" do
|
||||
expect(NervesHub.conn)
|
||||
xpect_args = "/orgs/farmbot/devices/X"
|
||||
resp = StubResp.new("404", { "data" => { } }.to_json)
|
||||
resp = StubResp.new("404", { "data" => {} }.to_json)
|
||||
expect(NervesHub.conn).to receive(:get).with(xpect_args).and_return(resp)
|
||||
tags = [ "A:B", "C:D" ]
|
||||
xpect_args2 = [ "/orgs/farmbot/devices",
|
||||
{ "description": "farmbot-X",
|
||||
"identifier": "X",
|
||||
"tags": tags }.to_json,
|
||||
NervesHub::HEADERS ]
|
||||
data = {fake: "Farmbot"}
|
||||
resp2 = StubResp.new("201", { "data" => data }.to_json)
|
||||
tags = ["A:B", "C:D"]
|
||||
xpect_args2 = ["/orgs/farmbot/devices",
|
||||
{ "description": "farmbot-X",
|
||||
"identifier": "X",
|
||||
"tags": tags }.to_json,
|
||||
NervesHub::HEADERS]
|
||||
data = { fake: "Farmbot" }
|
||||
resp2 = StubResp.new("201", { "data" => data }.to_json)
|
||||
expect(NervesHub.conn).to receive(:post).with(*xpect_args2).and_return(resp2)
|
||||
expect(NervesHub.create_or_update("X", tags)).to eq(data)
|
||||
expect(NervesHub.maybe_create_or_update("X", tags)).to eq(data)
|
||||
end
|
||||
|
||||
it "sometimes performs the NERVES_HUB_CA_HACK" do
|
||||
|
@ -111,16 +110,16 @@ describe NervesHub do
|
|||
it "detects malformed tags" do
|
||||
tags = ["wrong", "also_wrong", "ok:tag"].shuffle
|
||||
serial_number = "0xCAFEF00D"
|
||||
expected = { error: NervesHub::BAD_TAG,
|
||||
expected = { error: NervesHub::BAD_TAG,
|
||||
serial_number: serial_number,
|
||||
tags: tags, }
|
||||
tags: tags }
|
||||
resp = StubResp.new("200", {
|
||||
"data" => { hello: :world, identifier: "?" }
|
||||
"data" => { hello: :world, identifier: "?" },
|
||||
}.to_json)
|
||||
do_it = \
|
||||
do_it =
|
||||
receive(:get).with("/orgs/farmbot/devices/#{serial_number}").and_return(resp)
|
||||
expect(NervesHub.conn).to do_it
|
||||
expect(NervesHub).to receive(:report_problem).with(expected)
|
||||
NervesHub.create_or_update(serial_number, tags)
|
||||
NervesHub.maybe_create_or_update(serial_number, tags)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue