openc3 5.20.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/openc3cli +12 -120
- data/data/config/command_modifiers.yaml +13 -1
- data/data/config/interface_modifiers.yaml +21 -4
- data/data/config/item_modifiers.yaml +1 -1
- data/data/config/microservice.yaml +15 -2
- data/data/config/param_item_modifiers.yaml +1 -1
- data/data/config/parameter_modifiers.yaml +1 -1
- data/data/config/table_manager.yaml +2 -2
- data/data/config/target.yaml +11 -0
- data/data/config/telemetry_modifiers.yaml +17 -1
- data/data/config/tool.yaml +12 -0
- data/data/config/widgets.yaml +13 -17
- data/lib/openc3/accessors/form_accessor.rb +4 -3
- data/lib/openc3/accessors/html_accessor.rb +3 -3
- data/lib/openc3/accessors/http_accessor.rb +13 -13
- data/lib/openc3/accessors/xml_accessor.rb +16 -4
- data/lib/openc3/api/target_api.rb +0 -30
- data/lib/openc3/config/config_parser.rb +6 -3
- data/lib/openc3/core_ext/array.rb +0 -16
- data/lib/openc3/core_ext.rb +0 -1
- data/lib/openc3/interfaces/file_interface.rb +198 -0
- data/lib/openc3/interfaces/http_client_interface.rb +71 -39
- data/lib/openc3/interfaces/http_server_interface.rb +0 -7
- data/lib/openc3/interfaces/interface.rb +2 -0
- data/lib/openc3/interfaces/mqtt_interface.rb +32 -15
- data/lib/openc3/interfaces/mqtt_stream_interface.rb +19 -4
- data/lib/openc3/interfaces/protocols/crc_protocol.rb +7 -0
- data/lib/openc3/interfaces/serial_interface.rb +1 -0
- data/lib/openc3/interfaces.rb +2 -4
- data/lib/openc3/microservices/multi_microservice.rb +3 -3
- data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +31 -0
- data/lib/openc3/migrations/20241208080001_no_trigger_group.rb +46 -0
- data/lib/openc3/models/interface_model.rb +9 -3
- data/lib/openc3/models/microservice_model.rb +8 -1
- data/lib/openc3/models/plugin_model.rb +6 -1
- data/lib/openc3/models/python_package_model.rb +6 -1
- data/lib/openc3/models/reaction_model.rb +14 -10
- data/lib/openc3/models/scope_model.rb +60 -42
- data/lib/openc3/models/target_model.rb +17 -1
- data/lib/openc3/models/timeline_model.rb +17 -5
- data/lib/openc3/models/tool_model.rb +15 -3
- data/lib/openc3/models/trigger_group_model.rb +6 -3
- data/lib/openc3/operators/microservice_operator.rb +8 -0
- data/lib/openc3/packets/commands.rb +17 -6
- data/lib/openc3/packets/limits.rb +0 -12
- data/lib/openc3/packets/packet.rb +1 -1
- data/lib/openc3/packets/packet_item.rb +30 -36
- data/lib/openc3/packets/structure_item.rb +2 -2
- data/lib/openc3/script/script.rb +0 -10
- data/lib/openc3/script/web_socket_api.rb +2 -2
- data/lib/openc3/streams/mqtt_stream.rb +41 -33
- data/lib/openc3/streams/serial_stream.rb +27 -27
- data/lib/openc3/streams/stream.rb +17 -17
- data/lib/openc3/streams/tcpip_client_stream.rb +1 -1
- data/lib/openc3/streams/tcpip_socket_stream.rb +19 -19
- data/lib/openc3/system/system.rb +1 -1
- data/lib/openc3/system.rb +2 -3
- data/lib/openc3/tools/table_manager/table.rb +2 -2
- data/lib/openc3/tools/table_manager/table_parser.rb +1 -1
- data/lib/openc3/top_level.rb +0 -5
- data/lib/openc3/topics/command_decom_topic.rb +0 -7
- data/lib/openc3/utilities/bucket_utilities.rb +1 -1
- data/lib/openc3/utilities/cli_generator.rb +0 -1
- data/lib/openc3/version.rb +6 -6
- data/templates/plugin/README.md +1 -1
- data/templates/target/targets/TARGET/lib/target.rb +1 -1
- data/templates/tool_angular/package.json +8 -8
- data/templates/tool_angular/src/app/app.component.html +4 -13
- data/templates/tool_angular/src/app/app.component.scss +5 -13
- data/templates/tool_angular/src/app/app.component.ts +5 -4
- data/templates/tool_angular/src/app/custom-overlay-container.ts +2 -2
- data/templates/tool_angular/src/app/openc3-api.d.ts +1 -1
- data/templates/tool_angular/src/main.single-spa.ts +1 -1
- data/templates/tool_react/package.json +1 -0
- data/templates/tool_react/src/root.component.js +1 -1
- data/templates/tool_svelte/package.json +11 -9
- data/templates/tool_svelte/rollup.config.js +2 -0
- data/templates/tool_svelte/src/App.svelte +2 -2
- data/templates/tool_vue/eslint.config.mjs +68 -0
- data/templates/tool_vue/jsconfig.json +1 -1
- data/templates/tool_vue/package.json +26 -43
- data/templates/tool_vue/src/App.vue +3 -5
- data/templates/tool_vue/src/main.js +12 -23
- data/templates/tool_vue/src/router.js +19 -18
- data/templates/tool_vue/src/tools/tool_name/tool_name.vue +2 -2
- data/templates/tool_vue/vite.config.js +52 -0
- data/templates/widget/package.json +19 -26
- data/templates/widget/src/Widget.vue +13 -15
- data/templates/widget/vite.config.js +26 -0
- metadata +10 -41
- data/lib/openc3/core_ext/hash.rb +0 -40
- data/lib/openc3/core_ext/httpclient.rb +0 -11
- data/lib/openc3/interfaces/linc_interface.rb +0 -480
- data/lib/openc3/interfaces/protocols/override_protocol.rb +0 -4
- data/lib/openc3/microservices/critical_cmd_microservice.rb +0 -74
- data/lib/openc3/microservices/reaction_microservice.rb +0 -607
- data/lib/openc3/microservices/timeline_microservice.rb +0 -398
- data/lib/openc3/microservices/trigger_group_microservice.rb +0 -698
- data/lib/openc3/migrations/20230615000000_autonomic.rb +0 -86
- data/lib/openc3/migrations/20240915000000_activity_uuid.rb +0 -28
- data/lib/openc3/migrations/20241016000000_scope_critical_cmd.rb +0 -24
- data/lib/openc3/system/system_config.rb +0 -413
- data/templates/tool_svelte/src/services/api.js +0 -92
- data/templates/tool_svelte/src/services/axios.js +0 -85
- data/templates/tool_svelte/src/services/cable.js +0 -65
- data/templates/tool_svelte/src/services/config-parser.js +0 -198
- data/templates/tool_svelte/src/services/openc3-api.js +0 -606
- data/templates/tool_vue/.eslintrc.js +0 -43
- data/templates/tool_vue/babel.config.json +0 -11
- data/templates/tool_vue/vue.config.js +0 -38
- data/templates/widget/.eslintrc.js +0 -43
- data/templates/widget/babel.config.json +0 -11
- data/templates/widget/vue.config.js +0 -28
- /data/templates/tool_vue/{.prettierrc.js → .prettierrc.cjs} +0 -0
- /data/templates/widget/{.prettierrc.js → .prettierrc.cjs} +0 -0
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# encoding: ascii-8bit
|
|
2
|
-
|
|
3
|
-
# Copyright 2022 OpenC3, Inc.
|
|
4
|
-
# All Rights Reserved.
|
|
5
|
-
#
|
|
6
|
-
# This program is free software; you can modify and/or redistribute it
|
|
7
|
-
# under the terms of the GNU Affero General Public License
|
|
8
|
-
# as published by the Free Software Foundation; version 3 with
|
|
9
|
-
# attribution addendums as found in the LICENSE.txt
|
|
10
|
-
#
|
|
11
|
-
# This program is distributed in the hope that it will be useful,
|
|
12
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
-
# GNU Affero General Public License for more details.
|
|
15
|
-
#
|
|
16
|
-
# This file may also be used under the terms of a commercial license
|
|
17
|
-
# if purchased from OpenC3, Inc.
|
|
18
|
-
|
|
19
|
-
require 'openc3/utilities/logger'
|
|
20
|
-
require 'openc3/microservices/microservice'
|
|
21
|
-
begin
|
|
22
|
-
require 'openc3-enterprise/models/critical_cmd_model'
|
|
23
|
-
rescue LoadError
|
|
24
|
-
module OpenC3
|
|
25
|
-
class CriticalCmdModel
|
|
26
|
-
def self.get_all_models(scope:)
|
|
27
|
-
[]
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
module OpenC3
|
|
34
|
-
class CriticalCmdMicroservice < Microservice
|
|
35
|
-
SLEEP_PERIOD_SECONDS = 3 # Check every 3 seconds
|
|
36
|
-
TWENTY_FOUR_HOURS_NSEC = 24 * 60 * 60 * 1_000_000_000
|
|
37
|
-
|
|
38
|
-
def run
|
|
39
|
-
@run_sleeper = Sleeper.new
|
|
40
|
-
critical_cmd_waiting = false
|
|
41
|
-
while true
|
|
42
|
-
models = CriticalCmdModel.get_all_models(scope: @scope)
|
|
43
|
-
pre_waiting = critical_cmd_waiting
|
|
44
|
-
critical_cmd_waiting = false
|
|
45
|
-
old_time = Time.now.to_nsec_from_epoch - TWENTY_FOUR_HOURS_NSEC
|
|
46
|
-
models.each do |name, model|
|
|
47
|
-
# Cleanup older than 24 hours
|
|
48
|
-
if model.updated_at < old_time
|
|
49
|
-
model.destroy
|
|
50
|
-
elsif model.status == 'WAITING'
|
|
51
|
-
# Tell the frontend about critical commands pending
|
|
52
|
-
critical_cmd_waiting = true
|
|
53
|
-
data = Logger.build_log_data(Logger::INFO_LEVEL, "Critical Cmd Waiting", user: model.username, type: Logger::EPHEMERAL, other: {"uuid" => model.name, "cmd_string" => model.cmd_hash["cmd_string"]})
|
|
54
|
-
EphemeralStoreQueued.write_topic("#{scope}__openc3_ephemeral_messages", data, '*', 100)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
if pre_waiting and not critical_cmd_waiting
|
|
58
|
-
data = Logger.build_log_data(Logger::INFO_LEVEL, "All Critical Cmds Handled", type: Logger::EPHEMERAL)
|
|
59
|
-
EphemeralStoreQueued.write_topic("#{scope}__openc3_ephemeral_messages", data, '*', 100)
|
|
60
|
-
end
|
|
61
|
-
@count += 1
|
|
62
|
-
break if @cancel_thread
|
|
63
|
-
break if @run_sleeper.sleep(SLEEP_PERIOD_SECONDS)
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def shutdown
|
|
68
|
-
@run_sleeper.cancel if @run_sleeper
|
|
69
|
-
super()
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
OpenC3::CriticalCmdMicroservice.run if __FILE__ == $0
|
|
@@ -1,607 +0,0 @@
|
|
|
1
|
-
# encoding: ascii-8bit
|
|
2
|
-
|
|
3
|
-
# Copyright 2022 Ball Aerospace & Technologies Corp.
|
|
4
|
-
# All Rights Reserved.
|
|
5
|
-
#
|
|
6
|
-
# This program is free software; you can modify and/or redistribute it
|
|
7
|
-
# under the terms of the GNU Affero General Public License
|
|
8
|
-
# as published by the Free Software Foundation; version 3 with
|
|
9
|
-
# attribution addendums as found in the LICENSE.txt
|
|
10
|
-
#
|
|
11
|
-
# This program is distributed in the hope that it will be useful,
|
|
12
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
-
# GNU Affero General Public License for more details.
|
|
15
|
-
|
|
16
|
-
# Modified by OpenC3, Inc.
|
|
17
|
-
# All changes Copyright 2022, OpenC3, Inc.
|
|
18
|
-
# All Rights Reserved
|
|
19
|
-
#
|
|
20
|
-
# This file may also be used under the terms of a commercial license
|
|
21
|
-
# if purchased from OpenC3, Inc.
|
|
22
|
-
|
|
23
|
-
require 'openc3/microservices/microservice'
|
|
24
|
-
require 'openc3/models/reaction_model'
|
|
25
|
-
require 'openc3/models/trigger_model'
|
|
26
|
-
require 'openc3/topics/autonomic_topic'
|
|
27
|
-
require 'openc3/utilities/authentication'
|
|
28
|
-
|
|
29
|
-
require 'openc3/script'
|
|
30
|
-
|
|
31
|
-
module OpenC3
|
|
32
|
-
# This should remain a thread safe implementation. This is the in memory
|
|
33
|
-
# cache that should mirror the database. This will update two hash
|
|
34
|
-
# variables and will track triggers to lookup what triggers link to what
|
|
35
|
-
# reactions.
|
|
36
|
-
class ReactionBase
|
|
37
|
-
attr_reader :reactions
|
|
38
|
-
|
|
39
|
-
def initialize(scope:)
|
|
40
|
-
@scope = scope
|
|
41
|
-
@reactions_mutex = Mutex.new
|
|
42
|
-
@reactions = Hash.new
|
|
43
|
-
@lookup_mutex = Mutex.new
|
|
44
|
-
@lookup = Hash.new
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# RETURNS an Array of actively snoozed reactions
|
|
48
|
-
def get_snoozed
|
|
49
|
-
data = nil
|
|
50
|
-
@reactions_mutex.synchronize do
|
|
51
|
-
data = Marshal.load( Marshal.dump(@reactions) )
|
|
52
|
-
end
|
|
53
|
-
ret = Array.new
|
|
54
|
-
return ret unless data
|
|
55
|
-
data.each do |_name, r_hash|
|
|
56
|
-
data = Marshal.load( Marshal.dump(r_hash) )
|
|
57
|
-
reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
|
|
58
|
-
ret << reaction if reaction.enabled && reaction.snoozed_until
|
|
59
|
-
end
|
|
60
|
-
return ret
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# RETURNS an Array of actively NOT snoozed reactions
|
|
64
|
-
def get_reactions(trigger_name:)
|
|
65
|
-
array_value = nil
|
|
66
|
-
@lookup_mutex.synchronize do
|
|
67
|
-
array_value = Marshal.load( Marshal.dump(@lookup[trigger_name]) )
|
|
68
|
-
end
|
|
69
|
-
ret = Array.new
|
|
70
|
-
return ret unless array_value
|
|
71
|
-
array_value.each do |name|
|
|
72
|
-
@reactions_mutex.synchronize do
|
|
73
|
-
data = Marshal.load( Marshal.dump(@reactions[name]) )
|
|
74
|
-
reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
|
|
75
|
-
ret << reaction if reaction.enabled && reaction.snoozed_until.nil?
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
return ret
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Update the memory database with a HASH of reactions from the external database
|
|
82
|
-
def setup(reactions:)
|
|
83
|
-
@reactions_mutex.synchronize do
|
|
84
|
-
@reactions = Marshal.load( Marshal.dump(reactions) )
|
|
85
|
-
end
|
|
86
|
-
@lookup_mutex.synchronize do
|
|
87
|
-
@lookup = Hash.new
|
|
88
|
-
reactions.each do |reaction_name, reaction|
|
|
89
|
-
reaction['triggers'].each do |trigger|
|
|
90
|
-
trigger_name = trigger['name']
|
|
91
|
-
if @lookup[trigger_name].nil?
|
|
92
|
-
@lookup[trigger_name] = [reaction_name]
|
|
93
|
-
else
|
|
94
|
-
@lookup[trigger_name] << reaction_name
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Pulls the latest reaction name from the in memory database to see
|
|
102
|
-
# if the reaction should be put to sleep.
|
|
103
|
-
def sleep(name:)
|
|
104
|
-
@reactions_mutex.synchronize do
|
|
105
|
-
data = Marshal.load( Marshal.dump(@reactions[name]) )
|
|
106
|
-
return unless data
|
|
107
|
-
reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
|
|
108
|
-
if reaction.snoozed_until.nil? || Time.now.to_i >= reaction.snoozed_until
|
|
109
|
-
reaction.sleep()
|
|
110
|
-
end
|
|
111
|
-
@reactions[name] = reaction.as_json(:allow_nan => true)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Pulls the latest reaction name from the in memory database to see
|
|
116
|
-
# if the reaction should be awaken.
|
|
117
|
-
def wake(name:)
|
|
118
|
-
@reactions_mutex.synchronize do
|
|
119
|
-
data = Marshal.load( Marshal.dump(@reactions[name]) )
|
|
120
|
-
return unless data
|
|
121
|
-
reaction = ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
|
|
122
|
-
reaction.awaken()
|
|
123
|
-
@reactions[name] = reaction.as_json(:allow_nan => true)
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Add a reaction to the in memory database
|
|
128
|
-
def add(reaction:)
|
|
129
|
-
reaction_name = reaction['name']
|
|
130
|
-
@reactions_mutex.synchronize do
|
|
131
|
-
@reactions[reaction_name] = reaction
|
|
132
|
-
end
|
|
133
|
-
reaction['triggers'].each do |trigger|
|
|
134
|
-
trigger_name = trigger['name']
|
|
135
|
-
@lookup_mutex.synchronize do
|
|
136
|
-
if @lookup[trigger_name].nil?
|
|
137
|
-
@lookup[trigger_name] = [reaction_name]
|
|
138
|
-
else
|
|
139
|
-
@lookup[trigger_name] << reaction_name
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Updates a reaction to the in memory database. This current does not
|
|
146
|
-
# update the lookup Hash for the triggers.
|
|
147
|
-
def update(reaction:)
|
|
148
|
-
@reactions_mutex.synchronize do
|
|
149
|
-
model = ReactionModel.from_json(reaction, name: reaction['name'], scope: reaction['scope'])
|
|
150
|
-
model.update()
|
|
151
|
-
@reactions[reaction['name']] = model.as_json(:allow_nan => true)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Removes a reaction to the in memory database.
|
|
156
|
-
def remove(reaction:)
|
|
157
|
-
@reactions_mutex.synchronize do
|
|
158
|
-
@reactions.delete(reaction['name'])
|
|
159
|
-
ReactionModel.delete(name: reaction['name'], scope: reaction['scope'])
|
|
160
|
-
end
|
|
161
|
-
reaction['triggers'].each do |trigger|
|
|
162
|
-
trigger_name = trigger['name']
|
|
163
|
-
@lookup_mutex.synchronize do
|
|
164
|
-
@lookup[trigger_name].delete(reaction['name'])
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# This should remain a thread safe implementation.
|
|
171
|
-
class QueueBase
|
|
172
|
-
attr_reader :queue
|
|
173
|
-
|
|
174
|
-
def initialize(scope:)
|
|
175
|
-
@queue = Queue.new
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def enqueue(kind:, data:)
|
|
179
|
-
@queue << [kind, data]
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# This should remain a thread safe implementation.
|
|
184
|
-
class SnoozeBase
|
|
185
|
-
def initialize(scope:)
|
|
186
|
-
# store the round robin watch
|
|
187
|
-
@watch_mutex = Mutex.new
|
|
188
|
-
@watch_size = 25
|
|
189
|
-
@watch_queue = Array.new(@watch_size)
|
|
190
|
-
@watch_index = 0
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def not_queued?(reaction:)
|
|
194
|
-
key = "#{reaction.name}__#{reaction.snoozed_until}"
|
|
195
|
-
@watch_mutex.synchronize do
|
|
196
|
-
return false if @watch_queue.index(key)
|
|
197
|
-
@watch_queue[@watch_index] = key
|
|
198
|
-
@watch_index = @watch_index + 1 >= @watch_size ? 0 : @watch_index + 1
|
|
199
|
-
return true
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Shared between the monitor thread and the manager thread to
|
|
205
|
-
# share the resources.
|
|
206
|
-
class ReactionShare
|
|
207
|
-
attr_reader :reaction_base, :queue_base, :snooze_base
|
|
208
|
-
|
|
209
|
-
def initialize(scope:)
|
|
210
|
-
@reaction_base = ReactionBase.new(scope: scope)
|
|
211
|
-
@queue_base = QueueBase.new(scope: scope)
|
|
212
|
-
@snooze_base = SnoozeBase.new(scope: scope)
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# The Reaction worker is a very simple thread pool worker. Once the manager
|
|
217
|
-
# queues a trigger to evaluate against the reactions. The worker will check
|
|
218
|
-
# the reactions to see if it needs to fire any reactions.
|
|
219
|
-
class ReactionWorker
|
|
220
|
-
attr_reader :name, :scope, :share
|
|
221
|
-
|
|
222
|
-
def initialize(name:, logger:, scope:, share:, ident:)
|
|
223
|
-
@name = name
|
|
224
|
-
@logger = logger
|
|
225
|
-
@scope = scope
|
|
226
|
-
@share = share
|
|
227
|
-
@ident = ident
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def get_token(username)
|
|
231
|
-
if ENV['OPENC3_API_CLIENT'].nil?
|
|
232
|
-
ENV['OPENC3_API_PASSWORD'] ||= ENV['OPENC3_SERVICE_PASSWORD']
|
|
233
|
-
return OpenC3Authentication.new().token
|
|
234
|
-
else
|
|
235
|
-
# Check for offline access token
|
|
236
|
-
model = nil
|
|
237
|
-
model = OpenC3::OfflineAccessModel.get_model(name: username, scope: @scope) if username and username != ''
|
|
238
|
-
if model and model.offline_access_token
|
|
239
|
-
auth = OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
|
|
240
|
-
return auth.get_token_from_refresh_token(model.offline_access_token)
|
|
241
|
-
else
|
|
242
|
-
return nil
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def reaction(data:)
|
|
248
|
-
return ReactionModel.from_json(data, name: data['name'], scope: data['scope'])
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def run
|
|
252
|
-
@logger.info "ReactionWorker-#{@ident} running"
|
|
253
|
-
loop do
|
|
254
|
-
begin
|
|
255
|
-
kind, data = @share.queue_base.queue.pop
|
|
256
|
-
break if kind.nil? || data.nil?
|
|
257
|
-
case kind
|
|
258
|
-
when 'reaction'
|
|
259
|
-
run_reaction(reaction: reaction(data: data))
|
|
260
|
-
when 'trigger'
|
|
261
|
-
process_true_trigger(data: data)
|
|
262
|
-
end
|
|
263
|
-
rescue StandardError => e
|
|
264
|
-
@logger.error "ReactionWorker-#{@ident} failed to evaluate kind: #{kind} data: #{data}\n#{e.formatted}"
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
@logger.info "ReactionWorker-#{@ident} exiting"
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def process_true_trigger(data:)
|
|
271
|
-
@share.reaction_base.get_reactions(trigger_name: data['name']).each do |reaction|
|
|
272
|
-
run_reaction(reaction: reaction)
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def run_reaction(reaction:)
|
|
277
|
-
reaction.actions.each do |action|
|
|
278
|
-
run_action(reaction: reaction, action: action)
|
|
279
|
-
end
|
|
280
|
-
@share.reaction_base.sleep(name: reaction.name)
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def run_action(reaction:, action:)
|
|
284
|
-
reaction.updated_at = Time.now.to_nsec_from_epoch
|
|
285
|
-
reaction_json = reaction.as_json(:allow_nan => true)
|
|
286
|
-
# Let the frontend know which action is being run
|
|
287
|
-
# because we can combine commands and scripts with notifications
|
|
288
|
-
reaction_json['action'] = action['type']
|
|
289
|
-
notification = {
|
|
290
|
-
'kind' => 'run',
|
|
291
|
-
'type' => 'reaction',
|
|
292
|
-
'data' => JSON.generate(reaction_json),
|
|
293
|
-
}
|
|
294
|
-
AutonomicTopic.write_notification(notification, scope: @scope)
|
|
295
|
-
|
|
296
|
-
case action['type']
|
|
297
|
-
when 'notify'
|
|
298
|
-
run_notify(reaction: reaction, action: action)
|
|
299
|
-
when 'command'
|
|
300
|
-
run_command(reaction: reaction, action: action)
|
|
301
|
-
when 'script'
|
|
302
|
-
run_script(reaction: reaction, action: action)
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def run_notify(reaction:, action:)
|
|
307
|
-
message = "ReactionWorker-#{@ident} #{reaction.name} notify action complete, body: #{action['value']}"
|
|
308
|
-
url = "/tools/autonomic/reactions"
|
|
309
|
-
case action['severity'].to_s.upcase
|
|
310
|
-
when 'FATAL'
|
|
311
|
-
@logger.fatal(message, url: url, type: Logger::ALERT)
|
|
312
|
-
when 'ERROR', 'CRITICAL'
|
|
313
|
-
@logger.error(message, url: url, type: Logger::ALERT)
|
|
314
|
-
when 'WARN', 'CAUTION', 'SERIOUS'
|
|
315
|
-
@logger.warn(message, url: url, type: Logger::NOTIFICATION)
|
|
316
|
-
when 'INFO', 'NORMAL', 'STANDBY', 'OFF'
|
|
317
|
-
@logger.info(message, url: url, type: Logger::NOTIFICATION)
|
|
318
|
-
when 'DEBUG'
|
|
319
|
-
level = @logger.level
|
|
320
|
-
begin
|
|
321
|
-
@logger.level = Logger::DEBUG
|
|
322
|
-
@logger.debug(message, url: url, type: Logger::NOTIFICATION)
|
|
323
|
-
ensure
|
|
324
|
-
@logger.level = level
|
|
325
|
-
end
|
|
326
|
-
else
|
|
327
|
-
raise "Unknown severity: #{action['severity']}"
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def run_command(reaction:, action:)
|
|
332
|
-
begin
|
|
333
|
-
username = reaction.username
|
|
334
|
-
token = get_token(username)
|
|
335
|
-
raise "No token available for username: #{username}" unless token
|
|
336
|
-
cmd_no_hazardous_check(action['value'], scope: @scope, token: token)
|
|
337
|
-
@logger.info "ReactionWorker-#{@ident} #{reaction.name} command action complete, command: #{action['value']}"
|
|
338
|
-
rescue StandardError => e
|
|
339
|
-
@logger.error "ReactionWorker-#{@ident} #{reaction.name} command action failed, #{action}\n#{e.message}"
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def run_script(reaction:, action:)
|
|
344
|
-
begin
|
|
345
|
-
username = reaction.username
|
|
346
|
-
token = get_token(username)
|
|
347
|
-
raise "No token available for username: #{username}" unless token
|
|
348
|
-
request = Net::HTTP::Post.new(
|
|
349
|
-
"/script-api/scripts/#{action['value']}/run?scope=#{@scope}",
|
|
350
|
-
'Content-Type' => 'application/json',
|
|
351
|
-
'Authorization' => token
|
|
352
|
-
)
|
|
353
|
-
request.body = JSON.generate({
|
|
354
|
-
'scope' => @scope,
|
|
355
|
-
'environment' => action['environment'],
|
|
356
|
-
'reaction' => reaction.name,
|
|
357
|
-
'id' => Time.now.to_i
|
|
358
|
-
})
|
|
359
|
-
hostname = ENV['OPENC3_SCRIPT_HOSTNAME'] || 'openc3-cosmos-script-runner-api'
|
|
360
|
-
response = Net::HTTP.new(hostname, 2902).request(request)
|
|
361
|
-
raise "failed to call #{hostname}, for script: #{action['value']}, response code: #{response.code}" if response.code != '200'
|
|
362
|
-
|
|
363
|
-
@logger.info "ReactionWorker-#{@ident} #{reaction.name} script action complete, #{action['value']} => #{response.body}"
|
|
364
|
-
rescue StandardError => e
|
|
365
|
-
@logger.error "ReactionWorker-#{@ident} #{reaction.name} script action failed, #{action}\n#{e.message}"
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
# The reaction snooze manager starts a thread pool and keeps track of when a
|
|
371
|
-
# reaction is activated and to evaluate triggers when the snooze is complete.
|
|
372
|
-
class ReactionSnoozeManager
|
|
373
|
-
attr_reader :name, :scope, :share, :thread_pool
|
|
374
|
-
|
|
375
|
-
def initialize(name:, logger:, scope:, share:)
|
|
376
|
-
@name = name
|
|
377
|
-
@logger = logger
|
|
378
|
-
@scope = scope
|
|
379
|
-
@share = share
|
|
380
|
-
@worker_count = 3
|
|
381
|
-
@thread_pool = nil
|
|
382
|
-
@cancel_thread = false
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def generate_thread_pool()
|
|
386
|
-
thread_pool = []
|
|
387
|
-
@worker_count.times do |i|
|
|
388
|
-
worker = ReactionWorker.new(name: @name, logger: @logger, scope: @scope, share: @share, ident: i)
|
|
389
|
-
thread_pool << Thread.new { worker.run }
|
|
390
|
-
end
|
|
391
|
-
return thread_pool
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
def run
|
|
395
|
-
@logger.info "ReactionSnoozeManager running"
|
|
396
|
-
@thread_pool = generate_thread_pool()
|
|
397
|
-
loop do
|
|
398
|
-
begin
|
|
399
|
-
current_time = Time.now.to_i
|
|
400
|
-
manage_snoozed_reactions(current_time: current_time)
|
|
401
|
-
rescue StandardError => e
|
|
402
|
-
@logger.error "ReactionSnoozeManager failed to snooze reactions.\n#{e.formatted}"
|
|
403
|
-
end
|
|
404
|
-
break if @cancel_thread
|
|
405
|
-
sleep(1)
|
|
406
|
-
break if @cancel_thread
|
|
407
|
-
end
|
|
408
|
-
@logger.info "ReactionSnoozeManager exiting"
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def active_triggers(reaction:)
|
|
412
|
-
reaction.triggers.each do |trigger|
|
|
413
|
-
t = TriggerModel.get(name: trigger['name'], group: trigger['group'], scope: @scope)
|
|
414
|
-
return true if t && t.state
|
|
415
|
-
end
|
|
416
|
-
return false
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def manage_snoozed_reactions(current_time:)
|
|
420
|
-
@share.reaction_base.get_snoozed.each do |reaction|
|
|
421
|
-
time_difference = reaction.snoozed_until - current_time
|
|
422
|
-
if time_difference <= 0 && @share.snooze_base.not_queued?(reaction: reaction)
|
|
423
|
-
# LEVEL triggers mean we run if the trigger is active
|
|
424
|
-
if reaction.triggerLevel == 'LEVEL' and active_triggers(reaction: reaction)
|
|
425
|
-
@share.queue_base.enqueue(kind: 'reaction', data: reaction.as_json(:allow_nan => true))
|
|
426
|
-
else
|
|
427
|
-
@share.reaction_base.wake(name: reaction.name)
|
|
428
|
-
end
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
def shutdown
|
|
434
|
-
@cancel_thread = true
|
|
435
|
-
@worker_count.times do |_i|
|
|
436
|
-
@share.queue_base.enqueue(kind: nil, data: nil)
|
|
437
|
-
end
|
|
438
|
-
end
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
# The reaction microservice starts a manager then gets the
|
|
442
|
-
# reactions and triggers from redis. It then monitors the
|
|
443
|
-
# AutonomicTopic for changes.
|
|
444
|
-
class ReactionMicroservice < Microservice
|
|
445
|
-
attr_reader :name, :scope, :share, :manager, :manager_thread
|
|
446
|
-
TOPIC_LOOKUP = {
|
|
447
|
-
'group' => {
|
|
448
|
-
'created' => :no_op,
|
|
449
|
-
'updated' => :no_op,
|
|
450
|
-
'deleted' => :no_op,
|
|
451
|
-
},
|
|
452
|
-
'trigger' => {
|
|
453
|
-
'error' => :no_op,
|
|
454
|
-
'created' => :no_op,
|
|
455
|
-
'updated' => :no_op,
|
|
456
|
-
'deleted' => :no_op,
|
|
457
|
-
'enabled' => :no_op,
|
|
458
|
-
'disabled' => :no_op,
|
|
459
|
-
'true' => :trigger_true_event,
|
|
460
|
-
'false' => :no_op,
|
|
461
|
-
},
|
|
462
|
-
'reaction' => {
|
|
463
|
-
'run' => :no_op,
|
|
464
|
-
'deployed' => :no_op,
|
|
465
|
-
'undeployed' => :no_op,
|
|
466
|
-
'created' => :reaction_created_event,
|
|
467
|
-
'updated' => :reaction_updated_event,
|
|
468
|
-
'deleted' => :reaction_deleted_event,
|
|
469
|
-
'enabled' => :reaction_enabled_event,
|
|
470
|
-
'disabled' => :reaction_disabled_event,
|
|
471
|
-
'snoozed' => :no_op,
|
|
472
|
-
'awakened' => :no_op,
|
|
473
|
-
'executed' => :reaction_execute_event,
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
def initialize(*args)
|
|
478
|
-
# The name is passed in via the reaction_model as "#{scope}__OPENC3__REACTION"
|
|
479
|
-
super(*args)
|
|
480
|
-
@share = ReactionShare.new(scope: @scope)
|
|
481
|
-
@manager = ReactionSnoozeManager.new(name: @name, logger: @logger, scope: @scope, share: @share)
|
|
482
|
-
@manager_thread = nil
|
|
483
|
-
@read_topic = true
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def run
|
|
487
|
-
@logger.info "ReactionMicroservice running"
|
|
488
|
-
# Let the frontend know that the microservice has been deployed and is running
|
|
489
|
-
notification = {
|
|
490
|
-
'kind' => 'deployed',
|
|
491
|
-
'type' => 'reaction',
|
|
492
|
-
# name and updated_at fields are required for Event formatting
|
|
493
|
-
'data' => JSON.generate({
|
|
494
|
-
'name' => @name,
|
|
495
|
-
'updated_at' => Time.now.to_nsec_from_epoch,
|
|
496
|
-
}),
|
|
497
|
-
}
|
|
498
|
-
AutonomicTopic.write_notification(notification, scope: @scope)
|
|
499
|
-
|
|
500
|
-
@manager_thread = Thread.new { @manager.run }
|
|
501
|
-
loop do
|
|
502
|
-
reactions = ReactionModel.all(scope: @scope)
|
|
503
|
-
@share.reaction_base.setup(reactions: reactions)
|
|
504
|
-
break if @cancel_thread
|
|
505
|
-
block_for_updates()
|
|
506
|
-
break if @cancel_thread
|
|
507
|
-
end
|
|
508
|
-
@logger.info "ReactionMicroservice exiting"
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
def block_for_updates
|
|
512
|
-
@read_topic = true
|
|
513
|
-
while @read_topic && !@cancel_thread
|
|
514
|
-
begin
|
|
515
|
-
AutonomicTopic.read_topics(@topics) do |_topic, _msg_id, msg_hash, _redis|
|
|
516
|
-
@logger.debug "ReactionMicroservice block_for_updates: #{msg_hash.to_s}"
|
|
517
|
-
public_send(TOPIC_LOOKUP[msg_hash['type']][msg_hash['kind']], msg_hash)
|
|
518
|
-
end
|
|
519
|
-
rescue StandardError => e
|
|
520
|
-
@logger.error "ReactionMicroservice failed to read topics #{@topics}\n#{e.formatted}"
|
|
521
|
-
end
|
|
522
|
-
end
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
def no_op(data)
|
|
526
|
-
@logger.debug "ReactionMicroservice web socket event: #{data}"
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
def reaction_updated_event(msg_hash)
|
|
530
|
-
@logger.debug "ReactionMicroservice reaction updated msg_hash: #{msg_hash}"
|
|
531
|
-
reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
|
|
532
|
-
@share.reaction_base.update(reaction: reaction)
|
|
533
|
-
@read_topic = false
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
def trigger_true_event(msg_hash)
|
|
537
|
-
@logger.debug "ReactionMicroservice trigger true msg_hash: #{msg_hash}"
|
|
538
|
-
@share.queue_base.enqueue(kind: 'trigger', data: JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true))
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
# Add the reaction to the shared data.
|
|
542
|
-
def reaction_created_event(msg_hash)
|
|
543
|
-
@logger.debug "ReactionMicroservice reaction created msg_hash: #{msg_hash}"
|
|
544
|
-
reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
|
|
545
|
-
@share.reaction_base.add(reaction: reaction)
|
|
546
|
-
|
|
547
|
-
# If the reaction triggerLevel is LEVEL we have to check its triggers
|
|
548
|
-
# on add because if the trigger is active it should run
|
|
549
|
-
if reaction['triggerLevel'] == 'LEVEL'
|
|
550
|
-
reaction['triggers'].each do |trigger_hash|
|
|
551
|
-
trigger = TriggerModel.get(name: trigger_hash['name'], group: trigger_hash['group'], scope: reaction['scope'])
|
|
552
|
-
if trigger && trigger.state
|
|
553
|
-
@logger.info "ReactionMicroservice reaction #{reaction['name']} created. Since triggerLevel is 'LEVEL' it was run due to #{trigger.name}."
|
|
554
|
-
@share.queue_base.enqueue(kind: 'reaction', data: reaction)
|
|
555
|
-
end
|
|
556
|
-
end
|
|
557
|
-
end
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
# Update the reaction to the shared data.
|
|
561
|
-
def reaction_enabled_event(msg_hash)
|
|
562
|
-
@logger.debug "ReactionMicroservice reaction enabled msg_hash: #{msg_hash}"
|
|
563
|
-
reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
|
|
564
|
-
@share.reaction_base.update(reaction: reaction)
|
|
565
|
-
|
|
566
|
-
# If the reaction triggerLevel is LEVEL we have to check its triggers
|
|
567
|
-
# on add because if the trigger is active it should run
|
|
568
|
-
if reaction['triggerLevel'] == 'LEVEL'
|
|
569
|
-
reaction['triggers'].each do |trigger_hash|
|
|
570
|
-
trigger = TriggerModel.get(name: trigger_hash['name'], group: trigger_hash['group'], scope: reaction['scope'])
|
|
571
|
-
if trigger && trigger.state
|
|
572
|
-
@logger.info "ReactionMicroservice reaction #{reaction['name']} enabled. Since triggerLevel is 'LEVEL' it was run due to #{trigger.name}."
|
|
573
|
-
@share.queue_base.enqueue(kind: 'reaction', data: reaction)
|
|
574
|
-
end
|
|
575
|
-
end
|
|
576
|
-
end
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
# Update the reaction to the shared data.
|
|
580
|
-
def reaction_disabled_event(msg_hash)
|
|
581
|
-
@logger.debug "ReactionMicroservice reaction disabled msg_hash: #{msg_hash}"
|
|
582
|
-
@share.reaction_base.update(reaction: JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true))
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
# Add the reaction to the shared data.
|
|
586
|
-
def reaction_execute_event(msg_hash)
|
|
587
|
-
@logger.debug "ReactionMicroservice reaction execute msg_hash: #{msg_hash}"
|
|
588
|
-
reaction = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
|
|
589
|
-
@share.reaction_base.update(reaction: reaction)
|
|
590
|
-
@share.queue_base.enqueue(kind: 'reaction', data: reaction)
|
|
591
|
-
end
|
|
592
|
-
|
|
593
|
-
# Remove the reaction from the shared data
|
|
594
|
-
def reaction_deleted_event(msg_hash)
|
|
595
|
-
@logger.debug "ReactionMicroservice reaction deleted msg_hash: #{msg_hash}"
|
|
596
|
-
@share.reaction_base.remove(reaction: JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true))
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
def shutdown
|
|
600
|
-
@read_topic = false
|
|
601
|
-
@manager.shutdown()
|
|
602
|
-
super
|
|
603
|
-
end
|
|
604
|
-
end
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
OpenC3::ReactionMicroservice.run if __FILE__ == $0
|