openc3 7.0.0 → 7.1.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 +105 -13
- data/bin/pipinstall +38 -6
- data/data/config/command_modifiers.yaml +1 -0
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +12 -1
- data/data/config/parameter_modifiers.yaml +49 -7
- data/data/config/table_parameter_modifiers.yaml +3 -1
- data/data/config/target.yaml +11 -0
- data/data/config/target_config.yaml +6 -2
- data/lib/openc3/accessors/template_accessor.rb +9 -0
- data/lib/openc3/api/cmd_api.rb +2 -1
- data/lib/openc3/api/metrics_api.rb +11 -1
- data/lib/openc3/api/tlm_api.rb +21 -6
- data/lib/openc3/core_ext/faraday.rb +1 -1
- data/lib/openc3/interfaces/interface.rb +1 -6
- data/lib/openc3/io/json_api.rb +1 -1
- data/lib/openc3/logs/log_writer.rb +3 -1
- data/lib/openc3/microservices/decom_common.rb +128 -0
- data/lib/openc3/microservices/decom_microservice.rb +27 -96
- data/lib/openc3/microservices/interface_decom_common.rb +28 -10
- data/lib/openc3/microservices/interface_microservice.rb +16 -9
- data/lib/openc3/microservices/log_microservice.rb +1 -1
- data/lib/openc3/microservices/microservice.rb +3 -2
- data/lib/openc3/microservices/queue_microservice.rb +1 -1
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
- data/lib/openc3/microservices/text_log_microservice.rb +1 -2
- data/lib/openc3/models/cvt_model.rb +24 -13
- data/lib/openc3/models/db_sharded_model.rb +110 -0
- data/lib/openc3/models/interface_model.rb +9 -0
- data/lib/openc3/models/interface_status_model.rb +33 -3
- data/lib/openc3/models/metric_model.rb +96 -37
- data/lib/openc3/models/microservice_model.rb +7 -0
- data/lib/openc3/models/microservice_status_model.rb +30 -3
- data/lib/openc3/models/plugin_model.rb +9 -1
- data/lib/openc3/models/python_package_model.rb +1 -1
- data/lib/openc3/models/reaction_model.rb +27 -9
- data/lib/openc3/models/reingest_job_model.rb +153 -0
- data/lib/openc3/models/scope_model.rb +3 -2
- data/lib/openc3/models/script_status_model.rb +4 -20
- data/lib/openc3/models/target_model.rb +113 -100
- data/lib/openc3/models/trigger_model.rb +24 -7
- data/lib/openc3/packets/packet_config.rb +4 -1
- data/lib/openc3/script/api_shared.rb +39 -2
- data/lib/openc3/script/calendar.rb +32 -10
- data/lib/openc3/script/extract.rb +46 -13
- data/lib/openc3/script/script.rb +2 -2
- data/lib/openc3/script/script_runner.rb +4 -4
- data/lib/openc3/script/telemetry.rb +3 -3
- data/lib/openc3/script/web_socket_api.rb +29 -22
- data/lib/openc3/system/system.rb +20 -3
- data/lib/openc3/topics/command_decom_topic.rb +4 -2
- data/lib/openc3/topics/command_topic.rb +8 -5
- data/lib/openc3/topics/decom_interface_topic.rb +31 -11
- data/lib/openc3/topics/interface_topic.rb +88 -27
- data/lib/openc3/topics/limits_event_topic.rb +62 -41
- data/lib/openc3/topics/router_topic.rb +61 -21
- data/lib/openc3/topics/system_events_topic.rb +18 -1
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
- data/lib/openc3/topics/telemetry_topic.rb +4 -2
- data/lib/openc3/topics/topic.rb +77 -5
- data/lib/openc3/utilities/aws_bucket.rb +2 -0
- data/lib/openc3/utilities/cli_generator.rb +3 -2
- data/lib/openc3/utilities/ctrf.rb +231 -0
- data/lib/openc3/utilities/metric.rb +15 -1
- data/lib/openc3/utilities/questdb_client.rb +177 -40
- data/lib/openc3/utilities/reingest_job.rb +377 -0
- data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
- data/lib/openc3/utilities/store_autoload.rb +78 -52
- data/lib/openc3/utilities/store_queued.rb +20 -12
- data/lib/openc3/version.rb +5 -5
- data/templates/plugin/plugin.gemspec +13 -1
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -4
- data/templates/tool_vue/src/router.js +2 -2
- data/templates/widget/package.json +2 -2
- metadata +8 -3
|
@@ -99,7 +99,7 @@ module OpenC3
|
|
|
99
99
|
def self.destroy(name, scope:)
|
|
100
100
|
package_name, version = self.extract_name_and_version(name)
|
|
101
101
|
Logger.info "Uninstalling package: #{name}"
|
|
102
|
-
pip_args = [
|
|
102
|
+
pip_args = [package_name]
|
|
103
103
|
result = OpenC3::ProcessManager.instance.spawn(["/openc3/bin/pipuninstall"] + pip_args, "package_uninstall", name, Time.now + 3600.0, scope: scope)
|
|
104
104
|
return result.name
|
|
105
105
|
end
|
|
@@ -32,10 +32,12 @@ module OpenC3
|
|
|
32
32
|
ACTION_TYPES = [SCRIPT_REACTION, COMMAND_REACTION, NOTIFY_REACTION]
|
|
33
33
|
|
|
34
34
|
def self.create_unique_name(scope:)
|
|
35
|
-
reaction_names = self.names(scope: scope)
|
|
35
|
+
reaction_names = self.names(scope: scope)
|
|
36
36
|
num = 1 # Users count with 1
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
unless reaction_names.empty?
|
|
38
|
+
# Extract numeric suffixes and find the max to avoid lexicographic sort issues
|
|
39
|
+
max_num = reaction_names.map { |name| name[5..-1].to_i }.max
|
|
40
|
+
num = max_num + 1
|
|
39
41
|
end
|
|
40
42
|
return "REACT#{num}"
|
|
41
43
|
end
|
|
@@ -77,7 +79,7 @@ module OpenC3
|
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
attr_reader :name, :scope, :snooze, :triggers, :actions, :enabled, :trigger_level, :snoozed_until
|
|
80
|
-
attr_accessor :username, :shard
|
|
82
|
+
attr_accessor :username, :shard, :label
|
|
81
83
|
|
|
82
84
|
def initialize(
|
|
83
85
|
name:,
|
|
@@ -90,6 +92,7 @@ module OpenC3
|
|
|
90
92
|
snoozed_until: nil,
|
|
91
93
|
username: nil,
|
|
92
94
|
shard: 0,
|
|
95
|
+
label: nil,
|
|
93
96
|
updated_at: nil
|
|
94
97
|
)
|
|
95
98
|
super("#{scope}#{PRIMARY_KEY}", name: name, scope: scope)
|
|
@@ -102,6 +105,7 @@ module OpenC3
|
|
|
102
105
|
@triggers = validate_triggers(triggers)
|
|
103
106
|
@username = username
|
|
104
107
|
@shard = shard.to_i # to_i to handle nil
|
|
108
|
+
@label = label
|
|
105
109
|
@updated_at = updated_at
|
|
106
110
|
end
|
|
107
111
|
|
|
@@ -177,28 +181,40 @@ module OpenC3
|
|
|
177
181
|
return actions
|
|
178
182
|
end
|
|
179
183
|
|
|
180
|
-
|
|
184
|
+
# Validate that all triggers exist, but do not persist dependent changes yet.
|
|
185
|
+
# Returns the list of trigger models that need updating.
|
|
186
|
+
def validate_triggers_exist
|
|
181
187
|
if @triggers.empty?
|
|
182
188
|
raise ReactionInputError.new "reaction must contain at least one valid trigger: #{@triggers}"
|
|
183
189
|
end
|
|
184
190
|
|
|
191
|
+
models_to_update = []
|
|
185
192
|
@triggers.each do | trigger |
|
|
186
193
|
model = TriggerModel.get(name: trigger['name'], group: trigger['group'], scope: @scope)
|
|
187
194
|
if model.nil?
|
|
188
195
|
raise ReactionInputError.new "failed to find trigger: #{trigger}"
|
|
189
196
|
end
|
|
190
|
-
model.
|
|
191
|
-
|
|
197
|
+
unless model.dependents.include?(@name)
|
|
198
|
+
model.update_dependents(dependent: @name)
|
|
199
|
+
models_to_update << model
|
|
200
|
+
end
|
|
192
201
|
end
|
|
202
|
+
models_to_update
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Persist dependent changes to trigger models
|
|
206
|
+
def commit_trigger_dependents(models)
|
|
207
|
+
models.each { |model| model.update() }
|
|
193
208
|
end
|
|
194
209
|
|
|
195
210
|
def create
|
|
196
211
|
unless Store.hget(@primary_key, @name).nil?
|
|
197
212
|
raise ReactionInputError.new "existing reaction found: #{@name}"
|
|
198
213
|
end
|
|
199
|
-
|
|
214
|
+
models = validate_triggers_exist()
|
|
200
215
|
@updated_at = Time.now.to_nsec_from_epoch
|
|
201
216
|
Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
|
|
217
|
+
commit_trigger_dependents(models)
|
|
202
218
|
notify(kind: 'created')
|
|
203
219
|
end
|
|
204
220
|
|
|
@@ -222,9 +238,10 @@ module OpenC3
|
|
|
222
238
|
end
|
|
223
239
|
end
|
|
224
240
|
|
|
225
|
-
|
|
241
|
+
models = validate_triggers_exist()
|
|
226
242
|
@updated_at = Time.now.to_nsec_from_epoch
|
|
227
243
|
Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
|
|
244
|
+
commit_trigger_dependents(models)
|
|
228
245
|
# No notification as this is only called via reaction_controller which already notifies
|
|
229
246
|
end
|
|
230
247
|
|
|
@@ -281,6 +298,7 @@ module OpenC3
|
|
|
281
298
|
'actions' => @actions,
|
|
282
299
|
'username' => @username,
|
|
283
300
|
'shard' => @shard,
|
|
301
|
+
'label' => @label,
|
|
284
302
|
'updated_at' => @updated_at
|
|
285
303
|
}
|
|
286
304
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# encoding: ascii-8bit
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 OpenC3, Inc.
|
|
4
|
+
# All Rights Reserved.
|
|
5
|
+
#
|
|
6
|
+
# This program is distributed in the hope that it will be useful,
|
|
7
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
8
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
9
|
+
# See LICENSE.md for more details.
|
|
10
|
+
#
|
|
11
|
+
# This file may also be used under the terms of a commercial license
|
|
12
|
+
# if purchased from OpenC3, Inc.
|
|
13
|
+
|
|
14
|
+
require 'openc3/models/model'
|
|
15
|
+
|
|
16
|
+
module OpenC3
|
|
17
|
+
# Tracks one run of OpenC3::ReingestJob. The job updates this record from a
|
|
18
|
+
# background thread; the storage_controller status endpoint reads it.
|
|
19
|
+
# `updated_at` doubles as the heartbeat — if a Running record hasn't been
|
|
20
|
+
# touched in STALE_THRESHOLD_SEC, the status endpoint surfaces it as 'Stale'.
|
|
21
|
+
class ReingestJobModel < Model
|
|
22
|
+
PRIMARY_KEY = 'openc3_reingest_job'
|
|
23
|
+
STALE_THRESHOLD_SEC = 60
|
|
24
|
+
|
|
25
|
+
STATES = %w[Queued Running Complete Crashed Stale].freeze
|
|
26
|
+
PHASES = %w[downloading enabling_dedup ingesting dedup_cooldown disabling_dedup].freeze
|
|
27
|
+
|
|
28
|
+
attr_accessor :state
|
|
29
|
+
attr_accessor :files
|
|
30
|
+
attr_accessor :bucket
|
|
31
|
+
attr_accessor :path
|
|
32
|
+
attr_accessor :table_names
|
|
33
|
+
attr_accessor :target_version
|
|
34
|
+
attr_accessor :versions_used
|
|
35
|
+
attr_accessor :warnings
|
|
36
|
+
attr_accessor :progress_phase
|
|
37
|
+
attr_accessor :progress_current
|
|
38
|
+
attr_accessor :progress_total
|
|
39
|
+
attr_accessor :packets_written
|
|
40
|
+
attr_accessor :dedup_enabled_by_us
|
|
41
|
+
attr_accessor :dedup_preexisting
|
|
42
|
+
attr_accessor :dedup_disabled_tables
|
|
43
|
+
attr_accessor :dedup_cooldown_seconds
|
|
44
|
+
attr_accessor :dedup_enabled_at
|
|
45
|
+
attr_accessor :dedup_disabled_at
|
|
46
|
+
attr_accessor :error
|
|
47
|
+
attr_accessor :started_at
|
|
48
|
+
attr_accessor :finished_at
|
|
49
|
+
|
|
50
|
+
def self.get(name:, scope:)
|
|
51
|
+
super("#{scope}__#{PRIMARY_KEY}", name: name)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.names(scope:)
|
|
55
|
+
super("#{scope}__#{PRIMARY_KEY}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.all(scope:)
|
|
59
|
+
all = super("#{scope}__#{PRIMARY_KEY}")
|
|
60
|
+
all.sort_by { |_key, value| value['updated_at'] }.reverse
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(
|
|
64
|
+
name:,
|
|
65
|
+
state: 'Queued',
|
|
66
|
+
files: [],
|
|
67
|
+
bucket: nil,
|
|
68
|
+
path: nil,
|
|
69
|
+
table_names: [],
|
|
70
|
+
target_version: 'as_logged',
|
|
71
|
+
versions_used: [],
|
|
72
|
+
warnings: [],
|
|
73
|
+
progress_phase: nil,
|
|
74
|
+
progress_current: 0,
|
|
75
|
+
progress_total: 0,
|
|
76
|
+
packets_written: 0,
|
|
77
|
+
dedup_enabled_by_us: [],
|
|
78
|
+
dedup_preexisting: [],
|
|
79
|
+
dedup_disabled_tables: [],
|
|
80
|
+
dedup_cooldown_seconds: 60,
|
|
81
|
+
dedup_enabled_at: nil,
|
|
82
|
+
dedup_disabled_at: nil,
|
|
83
|
+
error: nil,
|
|
84
|
+
started_at: nil,
|
|
85
|
+
finished_at: nil,
|
|
86
|
+
updated_at: nil,
|
|
87
|
+
plugin: nil,
|
|
88
|
+
scope:
|
|
89
|
+
)
|
|
90
|
+
super("#{scope}__#{PRIMARY_KEY}", name: name, updated_at: updated_at, plugin: plugin, scope: scope)
|
|
91
|
+
@state = state
|
|
92
|
+
@files = files
|
|
93
|
+
@bucket = bucket
|
|
94
|
+
@path = path
|
|
95
|
+
@table_names = table_names
|
|
96
|
+
@target_version = target_version
|
|
97
|
+
@versions_used = versions_used
|
|
98
|
+
@warnings = warnings
|
|
99
|
+
@progress_phase = progress_phase
|
|
100
|
+
@progress_current = progress_current
|
|
101
|
+
@progress_total = progress_total
|
|
102
|
+
@packets_written = packets_written
|
|
103
|
+
@dedup_enabled_by_us = dedup_enabled_by_us
|
|
104
|
+
@dedup_preexisting = dedup_preexisting
|
|
105
|
+
@dedup_disabled_tables = dedup_disabled_tables
|
|
106
|
+
@dedup_cooldown_seconds = dedup_cooldown_seconds
|
|
107
|
+
@dedup_enabled_at = dedup_enabled_at
|
|
108
|
+
@dedup_disabled_at = dedup_disabled_at
|
|
109
|
+
@error = error
|
|
110
|
+
@started_at = started_at
|
|
111
|
+
@finished_at = finished_at
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# True if state is Running but the heartbeat (updated_at) is older than
|
|
115
|
+
# STALE_THRESHOLD_SEC. Callers should surface state as 'Stale' in that case.
|
|
116
|
+
def stale?
|
|
117
|
+
return false unless @state == 'Running'
|
|
118
|
+
return false unless @updated_at
|
|
119
|
+
age_nsec = Time.now.to_nsec_from_epoch - @updated_at.to_i
|
|
120
|
+
age_nsec > STALE_THRESHOLD_SEC * 1_000_000_000
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def as_json(*_a)
|
|
124
|
+
{
|
|
125
|
+
'name' => @name,
|
|
126
|
+
'state' => stale? ? 'Stale' : @state,
|
|
127
|
+
'files' => @files,
|
|
128
|
+
'bucket' => @bucket,
|
|
129
|
+
'path' => @path,
|
|
130
|
+
'table_names' => @table_names,
|
|
131
|
+
'target_version' => @target_version,
|
|
132
|
+
'versions_used' => @versions_used,
|
|
133
|
+
'warnings' => @warnings,
|
|
134
|
+
'progress_phase' => @progress_phase,
|
|
135
|
+
'progress_current' => @progress_current,
|
|
136
|
+
'progress_total' => @progress_total,
|
|
137
|
+
'packets_written' => @packets_written,
|
|
138
|
+
'dedup_enabled_by_us' => @dedup_enabled_by_us,
|
|
139
|
+
'dedup_preexisting' => @dedup_preexisting,
|
|
140
|
+
'dedup_disabled_tables' => @dedup_disabled_tables,
|
|
141
|
+
'dedup_cooldown_seconds' => @dedup_cooldown_seconds,
|
|
142
|
+
'dedup_enabled_at' => @dedup_enabled_at,
|
|
143
|
+
'dedup_disabled_at' => @dedup_disabled_at,
|
|
144
|
+
'error' => @error,
|
|
145
|
+
'started_at' => @started_at,
|
|
146
|
+
'finished_at' => @finished_at,
|
|
147
|
+
'updated_at' => @updated_at,
|
|
148
|
+
'plugin' => @plugin,
|
|
149
|
+
'scope' => @scope,
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -389,8 +389,9 @@ module OpenC3
|
|
|
389
389
|
end
|
|
390
390
|
|
|
391
391
|
# Delete the topics we created for the scope
|
|
392
|
-
|
|
393
|
-
Topic.del("#{@scope}
|
|
392
|
+
db_shard = Store.db_shard_for_target('UNKNOWN', scope: @scope)
|
|
393
|
+
Topic.del("#{@scope}__COMMAND__{UNKNOWN}__UNKNOWN", db_shard: db_shard)
|
|
394
|
+
Topic.del("#{@scope}__TELEMETRY__{UNKNOWN}__UNKNOWN", db_shard: db_shard)
|
|
394
395
|
Topic.del("#{@scope}__openc3_targets")
|
|
395
396
|
Topic.del("#{@scope}__CONFIG")
|
|
396
397
|
end
|
|
@@ -66,17 +66,9 @@ module OpenC3
|
|
|
66
66
|
keys = self.store.zrevrange("#{RUNNING_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
67
67
|
return [] if keys.empty?
|
|
68
68
|
result = []
|
|
69
|
-
|
|
70
|
-
# No pipelining for cluster mode
|
|
71
|
-
# because it requires using the same shard for all keys
|
|
69
|
+
result = self.store.redis_pool.pipelined do
|
|
72
70
|
keys.each do |key|
|
|
73
|
-
|
|
74
|
-
end
|
|
75
|
-
else
|
|
76
|
-
result = self.store.redis_pool.pipelined do
|
|
77
|
-
keys.each do |key|
|
|
78
|
-
self.store.hget("#{RUNNING_PRIMARY_KEY}__#{scope}", key)
|
|
79
|
-
end
|
|
71
|
+
self.store.hget("#{RUNNING_PRIMARY_KEY}__#{scope}", key)
|
|
80
72
|
end
|
|
81
73
|
end
|
|
82
74
|
result = result.map do |r|
|
|
@@ -91,17 +83,9 @@ module OpenC3
|
|
|
91
83
|
keys = self.store.zrevrange("#{COMPLETED_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
92
84
|
return [] if keys.empty?
|
|
93
85
|
result = []
|
|
94
|
-
|
|
95
|
-
# No pipelining for cluster mode
|
|
96
|
-
# because it requires using the same shard for all keys
|
|
86
|
+
result = self.store.redis_pool.pipelined do
|
|
97
87
|
keys.each do |key|
|
|
98
|
-
|
|
99
|
-
end
|
|
100
|
-
else
|
|
101
|
-
result = self.store.redis_pool.pipelined do
|
|
102
|
-
keys.each do |key|
|
|
103
|
-
self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
|
|
104
|
-
end
|
|
88
|
+
self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
|
|
105
89
|
end
|
|
106
90
|
end
|
|
107
91
|
result = result.map do |r|
|