-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Labels
Description
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
| # TODO:: we can remove this once there is a tenant_id field on modules |
###############################################################################################
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