###############################################################################################
before_action :can_read, only: [:index, :show]
before_action :can_write, only: [:create, :update, :destroy, :remove]
before_action :check_admin, except: [:index, :state, :show, :ping]
before_action :check_support, only: [:state, :show, :ping]
###############################################################################################
@[AC::Route::Filter(:before_action, except: [:index, :create])]
def find_current_module(id : String)
Log.context.set(module_id: id)
# Find will raise a 404 (not found) if there is an error
@current_module = Model::Module.find!(id)
end
getter! current_module : Model::Module
# Permissions
###############################################################################################
@[AC::Route::Filter(:before_action, only: [:index])]
def check_view_permissions
return if user_support?
# find the org zone
authority = current_authority.as(Model::Authority)
@org_zone_id = org_zone_id = authority.config["org_zone"]?.try(&.as_s?)
raise Error::Forbidden.new unless org_zone_id
access = check_access(current_user.groups, [org_zone_id])
raise Error::Forbidden.new unless access.admin?
end
getter org_zone_id : String? = nil
# Response helpers
###############################################################################################
record ControlSystemDetails, name : String, zone_data : Array(Model::Zone) do
include JSON::Serializable
end
record DriverDetails, name : String, description : String?, module_name : String? do
include JSON::Serializable
end
# extend the ControlSystem model to handle our return values
class Model::Module
@[JSON::Field(key: "driver")]
property driver_details : Api::Modules::DriverDetails? = nil
property compiled : Bool? = nil
@[JSON::Field(key: "control_system")]
property control_system_details : Api::Modules::ControlSystemDetails? = nil
end
###############################################################################################
# return a list of modules configured on the cluster
@[AC::Route::GET("/")]
def index(
@[AC::Param::Info(description: "only return modules updated before this time (unix epoch)")]
as_of : Int64? = nil,
@[AC::Param::Info(description: "only return modules running in this system (query params are ignored if this is provided)", example: "sys-1234")]
control_system_id : String? = nil,
@[AC::Param::Info(description: "only return modules with a particular connected state", example: "true")]
connected : Bool? = nil,
@[AC::Param::Info(description: "only return instances of this driver", example: "driver-1234")]
driver_id : String? = nil,
@[AC::Param::Info(description: "do not return logic modules (return only modules that can exist in multiple systems)", example: "true")]
no_logic : Bool = false,
@[AC::Param::Info(description: "return only running modules", example: "true")]
running : Bool? = nil
) : Array(Model::Module)
# if a system id is present we query the database directly
if control_system_id
cs = Model::ControlSystem.find!(control_system_id)
# Include subset of association data with results
results = Model::Module.find_all(cs.modules).compact_map do |mod|
next if (driver = mod.driver).nil?
# Most human readable module data is contained in driver
mod.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
mod.compiled = Api::Modules.driver_compiled?(mod, request_id)
mod
end.to_a
set_collection_headers(results.size, Model::Module.table_name)
return results
end
# we use Elasticsearch
elastic = Model::Module.elastic
query = elastic.query(search_params)
query.minimum_should_match(1)
# TODO:: we can remove this once there is a tenant_id field on modules
# which will make this much simpler to filter
if filter_zone_id = org_zone_id
# we only want to show modules in use by systems that include this zone
no_logic = true
# find all the non-logic modules that this user can access
# 1. grabs all the module ids in the systems of the provided org zone
# 2. select distinct modules ids which are not logic modules (99)
sql_query = %[
WITH matching_rows AS (
SELECT unnest(modules) AS module_id
FROM sys
WHERE $1 = ANY(zones)
)
SELECT ARRAY_AGG(DISTINCT m.module_id)
FROM matching_rows m
JOIN mod ON m.module_id = mod.id
WHERE mod.role <> 99;
]
module_ids = PgORM::Database.connection do |conn|
conn.query_one(sql_query, args: [filter_zone_id], &.read(Array(String)))
end
query.must({
"id" => module_ids,
})
end
if no_logic
query.must_not({"role" => [Model::Driver::Role::Logic.to_i]})
end
if driver_id
query.filter({"driver_id" => [driver_id]})
end
unless connected.nil?
query.filter({
"ignore_connected" => [false],
"connected" => [connected],
})
end
unless running.nil?
query.should({"running" => [running]})
end
if as_of
query.range({
"updated_at" => {
:lte => as_of,
},
})
end
query.has_parent(parent: Model::Driver, parent_index: Model::Driver.table_name)
search_results = paginate_results(elastic, query)
# Include subset of association data with results
search_results.compact_map do |d|
sys = d.control_system
driver = d.driver
next unless driver
# Include control system on Logic modules so it is possible
# to display the inherited settings
sys_field = if sys
ControlSystemDetails.new(sys.name, Model::Zone.find_all(sys.zones).to_a)
else
nil
end
d.control_system_details = sys_field
d.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
d
end
end
# return the details of a module
@[AC::Route::GET("/:id")]
def show(
@[AC::Param::Info(description: "return the driver details along with the module?", example: "true")]
complete : Bool = false
) : Model::Module
if complete && (driver = current_module.driver)
current_module.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
current_module
else
current_module
end
end
# update the details of a module
@[AC::Route::PATCH("/:id", body: :mod)]
@[AC::Route::PUT("/:id", body: :mod)]
def update(mod : Model::Module) : Model::Module
current = current_module
current.assign_attributes(mod)
raise Error::ModelValidation.new(current.errors) unless current.save
if driver = current.driver
current.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
end
current
end
# add a new module / instance of a driver
@[AC::Route::POST("/", body: :mod, status_code: HTTP::Status::CREATED)]
def create(mod : Model::Module) : Model::Module
raise Error::ModelValidation.new(mod.errors) unless mod.save
mod
end
# remove a module
@[AC::Route::DELETE("/:id", status_code: HTTP::Status::ACCEPTED)]
def destroy : Nil
current_module.destroy
end
# Receive the collated settings for a module
@[AC::Route::GET("/:id/settings")]
def settings : Array(PlaceOS::Model::Settings)
Api::Settings.collated_settings(current_user, current_module)
end
# Starts a module
@[AC::Route::POST("/:id/start")]
def start : Nil
return if current_module.running == true
current_module.update_fields(running: true)
# Changes cleared on a successful update
if current_module.running_changed?
Log.error { {controller: "Modules", action: "start", module_id: current_module.id, event: "failed"} }
raise "failed to update database to start module #{current_module.id}"
end
end
# Stops a module
@[AC::Route::POST("/:id/stop")]
def stop : Nil
return unless current_module.running
current_module.update_fields(running: false)
# Changes cleared on a successful update
if current_module.running_changed?
Log.error { {controller: "Modules", action: "stop", module_id: current_module.id, event: "failed"} }
raise "failed to update database to stop module #{current_module.id}"
end
end
# Executes a command on a module
# The `/systems/` route can be used to introspect modules for the list of methods and argument requirements
@[AC::Route::POST("/:id/exec/:method", body: :args)]
def execute(
id : String,
@[AC::Param::Info(description: "the name of the methodm we want to execute")]
method : String,
@[AC::Param::Info(description: "the arguments we want to provide to the method")]
args : Array(JSON::Any)
) : Nil
sys_id = current_module.control_system_id || ""
result, status_code = Driver::Proxy::RemoteDriver.new(
module_id: id,
sys_id: sys_id,
module_name: current_module.name,
discovery: self.class.core_discovery,
user_id: current_user.id,
) { |module_id|
Model::Module.find!(module_id).edge_id.as(String)
}.exec(
security: driver_clearance(user_token),
function: method,
args: args,
request_id: request_id,
)
# customise the response based on the execute results
response.content_type = "application/json"
render text: result, status: status_code
rescue e : Driver::Proxy::RemoteDriver::Error
which will make this much simpler to filter
grabs all the module ids in the systems of the provided org zone
select distinct modules ids which are not logic modules (99)
to display the inherited settings
to display the inherited settings
get("/:id/settings", :settings) do
rest-api/src/placeos-rest-api/controllers/modules.cr
Line 114 in e912723