openc3 7.2.1 → 7.3.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 +97 -6
- data/bin/pipinstall +5 -4
- data/bin/pipuninstall +5 -1
- data/bin/uvinstall +149 -0
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +5 -1
- data/data/config/screen.yaml +2 -1
- data/data/config/target.yaml +21 -0
- data/data/config/tool.yaml +3 -1
- data/data/config/widgets.yaml +16 -3
- data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
- data/data/xtce_schemas/xml.xsd +116 -0
- data/ext/openc3/ext/telemetry/telemetry.c +1 -1
- data/lib/openc3/accessors/template_accessor.rb +2 -2
- data/lib/openc3/api/interface_api.rb +2 -2
- data/lib/openc3/api/limits_api.rb +27 -13
- data/lib/openc3/api/router_api.rb +3 -3
- data/lib/openc3/api/tlm_api.rb +1 -1
- data/lib/openc3/interfaces/http_client_interface.rb +1 -1
- data/lib/openc3/io/json_drb.rb +24 -8
- data/lib/openc3/io/json_rpc.rb +0 -2
- data/lib/openc3/logs/log_writer.rb +43 -7
- data/lib/openc3/logs/packet_log_writer.rb +1 -2
- data/lib/openc3/microservices/interface_microservice.rb +1 -1
- data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
- data/lib/openc3/models/cvt_model.rb +5 -5
- data/lib/openc3/models/gem_model.rb +1 -1
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/microservice_model.rb +21 -0
- data/lib/openc3/models/plugin_model.rb +204 -50
- data/lib/openc3/models/python_package_model.rb +207 -11
- data/lib/openc3/models/scope_model.rb +34 -30
- data/lib/openc3/models/target_model.rb +180 -6
- data/lib/openc3/models/tool_model.rb +33 -20
- data/lib/openc3/operators/microservice_operator.rb +33 -4
- data/lib/openc3/packets/commands.rb +4 -4
- data/lib/openc3/packets/limits.rb +15 -7
- data/lib/openc3/packets/packet.rb +1 -1
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
- data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
- data/lib/openc3/packets/structure.rb +2 -2
- data/lib/openc3/packets/telemetry.rb +3 -3
- data/lib/openc3/script/commands.rb +1 -1
- data/lib/openc3/script/extract.rb +20 -25
- data/lib/openc3/script/web_socket_api.rb +44 -4
- data/lib/openc3/topics/command_decom_topic.rb +16 -1
- data/lib/openc3/topics/limits_event_topic.rb +1 -1
- data/lib/openc3/utilities/aws_bucket.rb +10 -2
- data/lib/openc3/utilities/csv.rb +5 -5
- data/lib/openc3/utilities/local_mode.rb +4 -1
- data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
- data/lib/openc3/utilities/running_script.rb +145 -90
- data/lib/openc3/utilities/script.rb +46 -10
- data/lib/openc3/version.rb +6 -6
- data/templates/plugin/plugin.gemspec +1 -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 -3
- data/templates/widget/package.json +2 -2
- metadata +5 -3
- data/templates/tool_vue/.nycrc +0 -3
- data/templates/widget/.nycrc +0 -3
|
@@ -43,6 +43,38 @@ module OpenC3
|
|
|
43
43
|
class ScopeModel < Model
|
|
44
44
|
PRIMARY_KEY = "openc3_scopes"
|
|
45
45
|
|
|
46
|
+
# Default thresholds used by the System Health tool and the metrics
|
|
47
|
+
# microservices. Stored in the 'system_health' setting as a JSON string.
|
|
48
|
+
SYSTEM_HEALTH_DEFAULTS = {
|
|
49
|
+
"cpu" => {
|
|
50
|
+
"redThreshold" => 90.0,
|
|
51
|
+
"yellowThreshold" => 80.0,
|
|
52
|
+
"snoozeMinutes" => 15,
|
|
53
|
+
"lastTriggerTimeRed" => nil, # timestamp or nil
|
|
54
|
+
"lastTriggerTimeYellow" => nil, # timestamp or nil
|
|
55
|
+
"sustainedSeconds" => 15
|
|
56
|
+
},
|
|
57
|
+
"memory" => {
|
|
58
|
+
"redThreshold" => 90.0,
|
|
59
|
+
"yellowThreshold" => 80.0,
|
|
60
|
+
"snoozeMinutes" => 15,
|
|
61
|
+
"lastTriggerTimeRed" => nil, # timestamp or nil
|
|
62
|
+
"lastTriggerTimeYellow" => nil, # timestamp or nil
|
|
63
|
+
"sustainedSeconds" => 15
|
|
64
|
+
},
|
|
65
|
+
"disk" => {
|
|
66
|
+
"redThreshold" => 90.0,
|
|
67
|
+
"yellowThreshold" => 80.0,
|
|
68
|
+
"snoozeMinutes" => 720, # 12 hours
|
|
69
|
+
"lastTriggerTimeRed" => nil, # timestamp or nil
|
|
70
|
+
"lastTriggerTimeYellow" => nil, # timestamp or nil
|
|
71
|
+
"sustainedSeconds" => 60
|
|
72
|
+
},
|
|
73
|
+
"global" => {
|
|
74
|
+
"enableAlerts" => true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
46
78
|
attr_accessor :children
|
|
47
79
|
attr_accessor :text_log_cycle_time
|
|
48
80
|
attr_accessor :text_log_cycle_size
|
|
@@ -407,36 +439,8 @@ module OpenC3
|
|
|
407
439
|
SettingModel.set({name: "news_feed", data: true}, scope: @scope)
|
|
408
440
|
|
|
409
441
|
setting = SettingModel.get(name: "system_health")
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
"redThreshold" => 90.0,
|
|
413
|
-
"yellowThreshold" => 80.0,
|
|
414
|
-
"snoozeMinutes" => 15,
|
|
415
|
-
"lastTriggerTimeRed" => nil, # timestamp or nil
|
|
416
|
-
"lastTriggerTimeYellow" => nil, # timestamp or nil
|
|
417
|
-
"sustainedSeconds" => 15
|
|
418
|
-
},
|
|
419
|
-
"memory" => {
|
|
420
|
-
"redThreshold" => 90.0,
|
|
421
|
-
"yellowThreshold" => 80.0,
|
|
422
|
-
"snoozeMinutes" => 15,
|
|
423
|
-
"lastTriggerTimeRed" => nil, # timestamp or nil
|
|
424
|
-
"lastTriggerTimeYellow" => nil, # timestamp or nil
|
|
425
|
-
"sustainedSeconds" => 15
|
|
426
|
-
},
|
|
427
|
-
"disk" => {
|
|
428
|
-
"redThreshold" => 90.0,
|
|
429
|
-
"yellowThreshold" => 80.0,
|
|
430
|
-
"snoozeMinutes" => 720, # 12 hours
|
|
431
|
-
"lastTriggerTimeRed" => nil, # timestamp or nil
|
|
432
|
-
"lastTriggerTimeYellow" => nil, # timestamp or nil
|
|
433
|
-
"sustainedSeconds" => 60
|
|
434
|
-
},
|
|
435
|
-
"global" => {
|
|
436
|
-
"enableAlerts" => true
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
SettingModel.set({name: "system_health", data: system_health_data}, scope: "DEFAULT") unless setting
|
|
442
|
+
# Settings are stored as JSON strings
|
|
443
|
+
SettingModel.set({name: "system_health", data: JSON.generate(SYSTEM_HEALTH_DEFAULTS)}, scope: "DEFAULT") unless setting
|
|
440
444
|
end
|
|
441
445
|
end
|
|
442
446
|
end
|
|
@@ -30,6 +30,7 @@ require 'openc3/topics/config_topic'
|
|
|
30
30
|
require 'openc3/system'
|
|
31
31
|
require 'openc3/utilities/local_mode'
|
|
32
32
|
require 'openc3/utilities/bucket'
|
|
33
|
+
require 'openc3/utilities/target_file'
|
|
33
34
|
require 'openc3/utilities/zip'
|
|
34
35
|
require 'fileutils'
|
|
35
36
|
require 'ostruct'
|
|
@@ -50,6 +51,7 @@ module OpenC3
|
|
|
50
51
|
ERB_EXTENSIONS = %w(.txt .rb .py .json .yaml .yml)
|
|
51
52
|
ITEM_MAP_CACHE_TIMEOUT = 10.0
|
|
52
53
|
PACKET_CACHE_TIMEOUT = 10.0
|
|
54
|
+
MAX_COLUMN_HEADER_LENGTH = 127 # QuestDB column header limit for item/param names
|
|
53
55
|
@@item_map_cache = {}
|
|
54
56
|
@@packet_cache = {}
|
|
55
57
|
@@packet_cache_mutex = Mutex.new
|
|
@@ -76,6 +78,7 @@ module OpenC3
|
|
|
76
78
|
attr_accessor :tlm_log_retain_time
|
|
77
79
|
attr_accessor :cmd_decom_retain_time
|
|
78
80
|
attr_accessor :tlm_decom_retain_time
|
|
81
|
+
attr_accessor :decom_flush_period
|
|
79
82
|
attr_accessor :cleanup_poll_time
|
|
80
83
|
attr_accessor :needs_dependencies
|
|
81
84
|
attr_accessor :target_microservices
|
|
@@ -123,6 +126,63 @@ module OpenC3
|
|
|
123
126
|
targets.sort.to_h
|
|
124
127
|
end
|
|
125
128
|
|
|
129
|
+
# Splits a plugin instance name ("openc3-cosmos-demo-7.2.0.gem__0") into
|
|
130
|
+
# [base_name, version] (["openc3-cosmos-demo", "7.2.0"]), or nil when the
|
|
131
|
+
# name doesn't carry a version segment. Drops the "__N" install-instance
|
|
132
|
+
# suffix and the ".gem" extension first.
|
|
133
|
+
def self.plugin_name_version(plugin_instance)
|
|
134
|
+
return nil if plugin_instance.nil? || plugin_instance.empty?
|
|
135
|
+
gem = plugin_instance.split('__')[0].sub(/\.gem\z/, '')
|
|
136
|
+
parts = gem.split('-')
|
|
137
|
+
return nil if parts.length < 2
|
|
138
|
+
[parts[0..-2].join('-'), parts[-1]]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# "name version" of the plugin that installed this target (e.g.
|
|
142
|
+
# "openc3-cosmos-demo 7.2.0"), derived from the plugin instance name
|
|
143
|
+
# ("openc3-cosmos-demo-7.2.0.gem__0"), or nil if unavailable. Used to
|
|
144
|
+
# annotate the Version History baseline with the file's origin.
|
|
145
|
+
def self.plugin_version_label(target_name, scope:)
|
|
146
|
+
model = get(name: target_name, scope: scope)
|
|
147
|
+
name, version = plugin_name_version(model && model['plugin'])
|
|
148
|
+
return nil unless name
|
|
149
|
+
"#{name} #{version}"
|
|
150
|
+
rescue
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Version-stripped plugin base name that owns this target, e.g.
|
|
155
|
+
# "openc3-cosmos-demo" from instance "openc3-cosmos-demo-7.2.0.gem__0".
|
|
156
|
+
# Used as the per-plugin Version History repo key: stable across upgrades
|
|
157
|
+
# (only the version segment changes), so history survives version bumps.
|
|
158
|
+
# Returns nil when the target has no owning plugin (e.g. a hand-created
|
|
159
|
+
# target); callers fall back to a no-plugin bucket.
|
|
160
|
+
def self.plugin_base_name(target_name, scope:)
|
|
161
|
+
model = get(name: target_name, scope: scope)
|
|
162
|
+
name, _version = plugin_name_version(model && model['plugin'])
|
|
163
|
+
name
|
|
164
|
+
rescue
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Uninstall-only: remove a plugin's entire Version History repo. Called
|
|
169
|
+
# from the CLI uninstall path with the plugin instance name; upgrade reuses
|
|
170
|
+
# the repo and must never call this. No-op in Core builds (or whenever
|
|
171
|
+
# OPENC3_VERSION_HISTORY_DIR is unset) since VersionStore is absent or
|
|
172
|
+
# disabled. Best-effort: a failure here must not abort the uninstall.
|
|
173
|
+
def self.destroy_script_versions(plugin_instance_name, scope:)
|
|
174
|
+
name, _version = plugin_name_version(plugin_instance_name)
|
|
175
|
+
return unless name
|
|
176
|
+
begin
|
|
177
|
+
require 'openc3-enterprise/utilities/version_store'
|
|
178
|
+
rescue LoadError
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
::VersionStore.destroy_repo(scope: scope, plugin: name)
|
|
182
|
+
rescue => e
|
|
183
|
+
Logger.warn("destroy_script_versions failed for #{scope}/#{plugin_instance_name}: #{e.message}")
|
|
184
|
+
end
|
|
185
|
+
|
|
126
186
|
# Given target's modified file list
|
|
127
187
|
def self.modified_files(target_name, scope:)
|
|
128
188
|
modified = []
|
|
@@ -145,7 +205,19 @@ module OpenC3
|
|
|
145
205
|
modified.sort
|
|
146
206
|
end
|
|
147
207
|
|
|
148
|
-
|
|
208
|
+
# files: optional list of specific modified files to delete, each a name
|
|
209
|
+
# relative to scope ("TARGET/sub/path", matching modified_files output).
|
|
210
|
+
# When given, only those files are removed (used by plugin upgrade to drop
|
|
211
|
+
# some modified files while keeping others); when nil, every modified file
|
|
212
|
+
# for the target is removed (the original behavior).
|
|
213
|
+
def self.delete_modified(target_name, scope:, files: nil)
|
|
214
|
+
if files && !files.empty?
|
|
215
|
+
files.each do |name|
|
|
216
|
+
# TargetFile.destroy handles both local mode and the bucket.
|
|
217
|
+
OpenC3::TargetFile.destroy(scope, name)
|
|
218
|
+
end
|
|
219
|
+
return
|
|
220
|
+
end
|
|
149
221
|
if ENV['OPENC3_LOCAL_MODE']
|
|
150
222
|
OpenC3::LocalMode.delete_modified(target_name, scope: scope)
|
|
151
223
|
end
|
|
@@ -224,7 +296,7 @@ module OpenC3
|
|
|
224
296
|
|
|
225
297
|
# Assume it exists and just try to get it to avoid an extra call to Store.exist?
|
|
226
298
|
json = store_for_target(target_name, scope: scope).hget("#{scope}__openc3#{type.to_s.downcase}__#{target_name}", packet_name)
|
|
227
|
-
raise "Packet '#{target_name} #{packet_name}' does not exist" if json.nil?
|
|
299
|
+
raise "Packet definition '#{target_name} #{packet_name}' does not exist" if json.nil?
|
|
228
300
|
|
|
229
301
|
packet = JSON.parse(json, allow_nan: true, create_additions: true)
|
|
230
302
|
|
|
@@ -245,7 +317,7 @@ module OpenC3
|
|
|
245
317
|
# @return [Array<Hash>] All packet hashes under the target_name
|
|
246
318
|
def self.packets(target_name, type: :TLM, scope:)
|
|
247
319
|
raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
|
|
248
|
-
raise "Target '#{target_name}' does not exist for scope: #{scope}" unless get(name: target_name, scope: scope)
|
|
320
|
+
raise "Target '#{target_name}' does not exist for scope: #{scope} (TargetModel)" unless get(name: target_name, scope: scope)
|
|
249
321
|
|
|
250
322
|
result = []
|
|
251
323
|
packets = store_for_target(target_name, scope: scope).hgetall("#{scope}__openc3#{type.to_s.downcase}__#{target_name}")
|
|
@@ -281,7 +353,7 @@ module OpenC3
|
|
|
281
353
|
def self.packet_item(target_name, packet_name, item_name, type: :TLM, scope:)
|
|
282
354
|
packet = packet(target_name, packet_name, type: type, scope: scope)
|
|
283
355
|
item = packet['items'].find { |item| item['name'] == item_name.to_s }
|
|
284
|
-
raise "Item '#{packet['target_name']} #{packet['packet_name']} #{item_name}' does not exist" unless item
|
|
356
|
+
raise "Item '#{packet['target_name']} #{packet['packet_name']} #{item_name}' does not exist (TargetModel)" unless item
|
|
285
357
|
item
|
|
286
358
|
end
|
|
287
359
|
|
|
@@ -400,6 +472,7 @@ module OpenC3
|
|
|
400
472
|
tlm_log_retain_time: nil,
|
|
401
473
|
cmd_decom_retain_time: nil,
|
|
402
474
|
tlm_decom_retain_time: nil,
|
|
475
|
+
decom_flush_period: 5.0,
|
|
403
476
|
cleanup_poll_time: 3600,
|
|
404
477
|
needs_dependencies: false,
|
|
405
478
|
target_microservices: {},
|
|
@@ -428,6 +501,7 @@ module OpenC3
|
|
|
428
501
|
@tlm_log_retain_time = tlm_log_retain_time
|
|
429
502
|
@cmd_decom_retain_time = cmd_decom_retain_time
|
|
430
503
|
@tlm_decom_retain_time = tlm_decom_retain_time
|
|
504
|
+
@decom_flush_period = decom_flush_period
|
|
431
505
|
@cleanup_poll_time = cleanup_poll_time
|
|
432
506
|
@needs_dependencies = needs_dependencies
|
|
433
507
|
@target_microservices = target_microservices
|
|
@@ -461,6 +535,7 @@ module OpenC3
|
|
|
461
535
|
'tlm_log_retain_time' => @tlm_log_retain_time,
|
|
462
536
|
'cmd_decom_retain_time' => @cmd_decom_retain_time,
|
|
463
537
|
'tlm_decom_retain_time' => @tlm_decom_retain_time,
|
|
538
|
+
'decom_flush_period' => @decom_flush_period,
|
|
464
539
|
'cleanup_poll_time' => @cleanup_poll_time,
|
|
465
540
|
'needs_dependencies' => @needs_dependencies,
|
|
466
541
|
'target_microservices' => @target_microservices.as_json(),
|
|
@@ -515,6 +590,9 @@ module OpenC3
|
|
|
515
590
|
if @tlm_decom_retain_time and !@tlm_decom_retain_time.match?(/^\d+[hdwMy]$/)
|
|
516
591
|
raise ConfigParser::Error.new(parser, "TLM_DECOM_RETAIN_TIME must be a number followed by h, d, w, M, or y (e.g., 24h, 7d, 1y)")
|
|
517
592
|
end
|
|
593
|
+
when 'DECOM_FLUSH_PERIOD'
|
|
594
|
+
parser.verify_num_parameters(1, 1, "#{keyword} <Flush period in seconds>")
|
|
595
|
+
@decom_flush_period = Float(parameters[0])
|
|
518
596
|
when 'REDUCED_MINUTE_LOG_RETAIN_TIME', 'REDUCED_HOUR_LOG_RETAIN_TIME', 'REDUCED_DAY_LOG_RETAIN_TIME', 'REDUCED_LOG_RETAIN_TIME'
|
|
519
597
|
# DEPRECATED
|
|
520
598
|
when 'REDUCER_DISABLE', 'REDUCER_DISABLED', 'REDUCER_MAX_CPU_UTILIZATION', 'REDUCED_MAX_CPU_UTILIZATION'
|
|
@@ -576,11 +654,23 @@ module OpenC3
|
|
|
576
654
|
return nil
|
|
577
655
|
end
|
|
578
656
|
|
|
579
|
-
|
|
657
|
+
# upgrade_context (plugin upgrades only) drives two modes, both keyed by
|
|
658
|
+
# the deployed file name "TARGET/sub/path" (matches modified_files output):
|
|
659
|
+
# { diff_collector: [] } — dry run (with validate_only): append the names
|
|
660
|
+
# of modified files whose live content differs from the rendered plugin
|
|
661
|
+
# content. Read-only; used to warn before an upgrade.
|
|
662
|
+
# { username:, plugin: "name version", version_files: [<name>...] } — for
|
|
663
|
+
# each listed file with a modified copy: record the modified copy in
|
|
664
|
+
# Version History, drop the shadow so the plugin content becomes current,
|
|
665
|
+
# then commit the incoming content as a plugin-upgrade version.
|
|
666
|
+
# nil (the default and Core builds without VersionStore) = no-op.
|
|
667
|
+
def deploy(gem_path, variables, validate_only: false, upgrade_context: nil)
|
|
580
668
|
variables["target_name"] = @name
|
|
581
669
|
start_path = "/targets/#{@folder_name}/"
|
|
582
670
|
temp_dir = Dir.mktmpdir
|
|
583
671
|
found = false
|
|
672
|
+
diff_collector = upgrade_context && upgrade_context[:diff_collector]
|
|
673
|
+
versioning = !validate_only && upgrade_context && upgrade_context[:version_files] && version_store_available?
|
|
584
674
|
begin
|
|
585
675
|
target_path = gem_path + start_path + "**/*"
|
|
586
676
|
Dir.glob(target_path) do |filename|
|
|
@@ -616,15 +706,30 @@ module OpenC3
|
|
|
616
706
|
File.open(local_path, 'wb') { |file| file.write(data) }
|
|
617
707
|
found = true
|
|
618
708
|
@bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: key, body: data) unless validate_only
|
|
709
|
+
|
|
710
|
+
# Plugin upgrade: either collect this file into the diff (dry run) or
|
|
711
|
+
# take it from the plugin while preserving the modified copy in
|
|
712
|
+
# Version History. Both skip files with no modified copy.
|
|
713
|
+
name = "#{@name}/#{target_folder_path}"
|
|
714
|
+
if diff_collector
|
|
715
|
+
collect_modified_diff(name, data, diff_collector)
|
|
716
|
+
elsif versioning && upgrade_context[:version_files].include?(name)
|
|
717
|
+
apply_upgrade_version(name, data, upgrade_context)
|
|
718
|
+
end
|
|
619
719
|
end
|
|
620
720
|
raise "No target files found at #{target_path}" unless found
|
|
621
721
|
|
|
622
722
|
target_folder = File.join(temp_dir, @name)
|
|
623
723
|
# Build a System for just this target
|
|
624
724
|
system = System.new([@name], temp_dir)
|
|
725
|
+
check_column_header_lengths(system)
|
|
625
726
|
if variables["xtce_output"]
|
|
626
727
|
puts "Converting target #{@name} to .xtce files in #{variables["xtce_output"]}/#{@name}"
|
|
627
|
-
|
|
728
|
+
if variables['time_association_name'].to_s.empty?
|
|
729
|
+
puts " No packet time item given, so no TimeAssociation will be written."
|
|
730
|
+
else
|
|
731
|
+
puts " Using mnemonic '#{variables['time_association_name']}' as the packet time item."
|
|
732
|
+
end
|
|
628
733
|
system.packet_config.to_xtce(variables["xtce_output"], variables['time_association_name'])
|
|
629
734
|
end
|
|
630
735
|
unless validate_only
|
|
@@ -638,6 +743,74 @@ module OpenC3
|
|
|
638
743
|
end
|
|
639
744
|
end
|
|
640
745
|
|
|
746
|
+
# Version History is an Enterprise feature. The plugin deploy runs in the
|
|
747
|
+
# cmd-tlm-api process, which (unlike script-runner-api) doesn't otherwise
|
|
748
|
+
# require the store, so lazily load it on demand. Returns false in Core
|
|
749
|
+
# builds where the Enterprise gem isn't present.
|
|
750
|
+
def version_store_available?
|
|
751
|
+
return true if defined?(::VersionStore)
|
|
752
|
+
require 'openc3-enterprise/utilities/version_store'
|
|
753
|
+
defined?(::VersionStore) ? true : false
|
|
754
|
+
rescue LoadError
|
|
755
|
+
false
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# See deploy/upgrade_context. Reads the modified shadow directly (not
|
|
759
|
+
# TargetFile.body, which would fall back to the just-overwritten pristine
|
|
760
|
+
# copy), commits it, removes it, then commits the incoming plugin content.
|
|
761
|
+
# Best-effort: a versioning failure must not abort the plugin upgrade.
|
|
762
|
+
def apply_upgrade_version(name, new_data, ctx)
|
|
763
|
+
modified_key = "#{@scope}/targets_modified/#{name}"
|
|
764
|
+
resp = @bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: modified_key)
|
|
765
|
+
return unless resp && resp.body
|
|
766
|
+
old_body = resp.body.read
|
|
767
|
+
# Nothing actually changed — the modification already matches the plugin.
|
|
768
|
+
# Leave the (identical) shadow in place and create no version churn.
|
|
769
|
+
return if old_body.b == new_data.b
|
|
770
|
+
old_body = old_body.force_encoding('UTF-8') unless File.extname(name) == '.bin'
|
|
771
|
+
# Preserve the pre-upgrade (user-modified) content as a version.
|
|
772
|
+
# Idempotent: a no-op when it already matches the latest version.
|
|
773
|
+
::VersionStore.commit(scope: @scope, name: name, text: old_body)
|
|
774
|
+
# Drop the modified shadow so the plugin file (already written to
|
|
775
|
+
# targets/) becomes the live content.
|
|
776
|
+
OpenC3::TargetFile.destroy(@scope, name)
|
|
777
|
+
# Commit the incoming plugin content as a plugin-upgrade version,
|
|
778
|
+
# attributed to the installing user.
|
|
779
|
+
::VersionStore.commit(scope: @scope, name: name, text: new_data,
|
|
780
|
+
username: ctx[:username], source: 'plugin-upgrade', plugin: ctx[:plugin])
|
|
781
|
+
rescue => e
|
|
782
|
+
Logger.warn("Version History upgrade capture failed for #{@scope}/#{name}: #{e.message}")
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Dry-run companion to apply_upgrade_version: append name to collector when
|
|
786
|
+
# a modified copy exists and differs (by bytes) from the rendered plugin
|
|
787
|
+
# content. Read-only.
|
|
788
|
+
def collect_modified_diff(name, new_data, collector)
|
|
789
|
+
resp = @bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: "#{@scope}/targets_modified/#{name}")
|
|
790
|
+
return unless resp && resp.body
|
|
791
|
+
collector << name if resp.body.read.b != new_data.b
|
|
792
|
+
rescue => e
|
|
793
|
+
Logger.warn("Modified diff check failed for #{@scope}/#{name}: #{e.message}")
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def check_column_header_lengths(system)
|
|
797
|
+
too_long = []
|
|
798
|
+
[system.packet_config.telemetry, system.packet_config.commands].each do |packets_by_target|
|
|
799
|
+
packets_by_target.each do |target_name, packets|
|
|
800
|
+
packets.each do |packet_name, packet|
|
|
801
|
+
packet.sorted_items.each do |item|
|
|
802
|
+
if item.name.length > MAX_COLUMN_HEADER_LENGTH
|
|
803
|
+
too_long << "#{target_name} #{packet_name} #{item.name} (#{item.name.length})"
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
unless too_long.empty?
|
|
810
|
+
raise "Item / parameter names must be #{MAX_COLUMN_HEADER_LENGTH} characters or less (QuestDB column header limit). The following are too long:\n #{too_long.join("\n ")}"
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
641
814
|
def undeploy
|
|
642
815
|
prefix = "#{@scope}/targets/#{@name}/"
|
|
643
816
|
objects = @bucket.list_objects(bucket: ENV['OPENC3_CONFIG_BUCKET'], prefix: prefix)
|
|
@@ -1032,6 +1205,7 @@ module OpenC3
|
|
|
1032
1205
|
options = []
|
|
1033
1206
|
options << ["CMD_DECOM_RETAIN_TIME", @cmd_decom_retain_time] if @cmd_decom_retain_time
|
|
1034
1207
|
options << ["TLM_DECOM_RETAIN_TIME", @tlm_decom_retain_time] if @tlm_decom_retain_time
|
|
1208
|
+
options << ["DECOM_FLUSH_PERIOD", @decom_flush_period] if @decom_flush_period
|
|
1035
1209
|
microservice = MicroserviceModel.new(
|
|
1036
1210
|
name: microservice_name,
|
|
1037
1211
|
folder_name: @folder_name,
|
|
@@ -25,6 +25,13 @@ module OpenC3
|
|
|
25
25
|
class ToolModel < Model
|
|
26
26
|
PRIMARY_KEY = 'openc3_tools'
|
|
27
27
|
|
|
28
|
+
# Tools are global to the entire COSMOS installation and are always stored
|
|
29
|
+
# in the DEFAULT scope. The frontend navigation always requests the tool
|
|
30
|
+
# list from DEFAULT, so a tool installed into any other scope would be
|
|
31
|
+
# inaccessible. Every scope parameter in this class is therefore ignored
|
|
32
|
+
# and replaced with TOOL_SCOPE.
|
|
33
|
+
TOOL_SCOPE = 'DEFAULT'
|
|
34
|
+
|
|
28
35
|
attr_accessor :folder_name
|
|
29
36
|
attr_accessor :icon
|
|
30
37
|
attr_accessor :url
|
|
@@ -39,21 +46,24 @@ module OpenC3
|
|
|
39
46
|
|
|
40
47
|
# NOTE: The following three class methods are used by the ModelController
|
|
41
48
|
# and are reimplemented to enable various Model class methods to work
|
|
49
|
+
# NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
|
|
42
50
|
def self.get(name:, scope: nil)
|
|
43
|
-
super("#{
|
|
51
|
+
super("#{TOOL_SCOPE}__#{PRIMARY_KEY}", name: name)
|
|
44
52
|
end
|
|
45
53
|
|
|
54
|
+
# NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
|
|
46
55
|
def self.names(scope: nil)
|
|
47
56
|
array = []
|
|
48
|
-
all(
|
|
57
|
+
all().each do |name, _tool|
|
|
49
58
|
array << name
|
|
50
59
|
end
|
|
51
60
|
array
|
|
52
61
|
end
|
|
53
62
|
|
|
63
|
+
# NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
|
|
54
64
|
def self.all(scope: nil)
|
|
55
65
|
ordered_array = []
|
|
56
|
-
tools = unordered_all(
|
|
66
|
+
tools = unordered_all()
|
|
57
67
|
tools.each do |_name, tool|
|
|
58
68
|
ordered_array << tool
|
|
59
69
|
end
|
|
@@ -65,14 +75,10 @@ module OpenC3
|
|
|
65
75
|
ordered_hash
|
|
66
76
|
end
|
|
67
77
|
|
|
78
|
+
# Tools only exist in the DEFAULT scope. Kept for backwards compatibility
|
|
79
|
+
# with the ToolsController importmap endpoint.
|
|
68
80
|
def self.all_scopes
|
|
69
|
-
|
|
70
|
-
scopes = OpenC3::ScopeModel.all
|
|
71
|
-
scopes.each do |key, _scope|
|
|
72
|
-
tools = unordered_all(scope: key)
|
|
73
|
-
result.merge!(tools)
|
|
74
|
-
end
|
|
75
|
-
result
|
|
81
|
+
unordered_all()
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
# Called by the PluginModel to allow this class to validate it's top-level keyword: "TOOL"
|
|
@@ -80,7 +86,8 @@ module OpenC3
|
|
|
80
86
|
case keyword
|
|
81
87
|
when 'TOOL'
|
|
82
88
|
parser.verify_num_parameters(2, 2, "TOOL <Folder Name> <Name>")
|
|
83
|
-
|
|
89
|
+
# NOTE: scope is intentionally ignored, see TOOL_SCOPE
|
|
90
|
+
return self.new(folder_name: parameters[0], name: parameters[1], plugin: plugin, needs_dependencies: needs_dependencies, scope: TOOL_SCOPE)
|
|
84
91
|
else
|
|
85
92
|
raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Tool: #{keyword} #{parameters.join(" ")}")
|
|
86
93
|
end
|
|
@@ -89,8 +96,10 @@ module OpenC3
|
|
|
89
96
|
|
|
90
97
|
# The ToolsTab.vue calls the ToolsController which uses this method to reorder the tools
|
|
91
98
|
# Position is index in the list starting with 0 = first
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
# NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
|
|
100
|
+
def self.set_position(name:, position:, scope: nil)
|
|
101
|
+
tool_scope = TOOL_SCOPE
|
|
102
|
+
moving = from_json(get(name: name, scope: tool_scope), scope: tool_scope)
|
|
94
103
|
old_pos = moving.position
|
|
95
104
|
new_pos = position
|
|
96
105
|
direction = :down
|
|
@@ -101,8 +110,8 @@ module OpenC3
|
|
|
101
110
|
end
|
|
102
111
|
|
|
103
112
|
# Go through all the tools and reorder
|
|
104
|
-
all(scope:
|
|
105
|
-
tool_model = from_json(tool, scope:
|
|
113
|
+
all(scope: tool_scope).each do |_tool_name, tool|
|
|
114
|
+
tool_model = from_json(tool, scope: tool_scope)
|
|
106
115
|
# Update the requested model to the new position
|
|
107
116
|
if tool_model.name == name
|
|
108
117
|
tool_model.position = position
|
|
@@ -134,9 +143,11 @@ module OpenC3
|
|
|
134
143
|
needs_dependencies: false,
|
|
135
144
|
disable_erb: nil,
|
|
136
145
|
import_map_items: nil,
|
|
137
|
-
scope:
|
|
146
|
+
scope: nil # NOTE: deprecated since 7.3.0 as tools are always created in the DEFAULT scope, see TOOL_SCOPE
|
|
138
147
|
)
|
|
139
|
-
|
|
148
|
+
# Tools are always created in the DEFAULT scope regardless of the scope
|
|
149
|
+
# the plugin is being installed into, see TOOL_SCOPE
|
|
150
|
+
super("#{TOOL_SCOPE}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: TOOL_SCOPE)
|
|
140
151
|
@folder_name = folder_name
|
|
141
152
|
@icon = icon
|
|
142
153
|
@url = url
|
|
@@ -156,14 +167,15 @@ module OpenC3
|
|
|
156
167
|
end
|
|
157
168
|
|
|
158
169
|
def create(update: false, force: false, queued: false)
|
|
159
|
-
tools = self.class.all(
|
|
170
|
+
tools = self.class.all()
|
|
160
171
|
|
|
161
172
|
# Make sure a tool with this folder_name doesn't already exist
|
|
173
|
+
# NOTE: Tools are global (TOOL_SCOPE) so this check is across all scopes
|
|
162
174
|
unless update
|
|
163
175
|
if @folder_name
|
|
164
176
|
tools.each do |_tool_name, tool|
|
|
165
177
|
if tool['folder_name'] == @folder_name
|
|
166
|
-
raise "Tool with folder_name #{@folder_name} already exists at create"
|
|
178
|
+
raise "Tool with folder_name #{@folder_name} already exists at create. Tools are global to the entire COSMOS installation and can only be installed once."
|
|
167
179
|
end
|
|
168
180
|
end
|
|
169
181
|
end
|
|
@@ -308,8 +320,9 @@ module OpenC3
|
|
|
308
320
|
##################################################
|
|
309
321
|
|
|
310
322
|
# Returns the list of tools or the default OpenC3 tool set if no tools have been created
|
|
323
|
+
# NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
|
|
311
324
|
def self.unordered_all(scope: nil)
|
|
312
|
-
tools = Store.hgetall("#{
|
|
325
|
+
tools = Store.hgetall("#{TOOL_SCOPE}__#{PRIMARY_KEY}")
|
|
313
326
|
tools.each do |key, value|
|
|
314
327
|
tools[key] = JSON.parse(value, allow_nan: true, create_additions: true)
|
|
315
328
|
end
|
|
@@ -47,10 +47,39 @@ module OpenC3
|
|
|
47
47
|
env = microservice_config["env"].dup
|
|
48
48
|
if microservice_config["needs_dependencies"]
|
|
49
49
|
env['GEM_HOME'] = '/gems'
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
50
|
+
|
|
51
|
+
# Resolve the Python virtual environment for this microservice.
|
|
52
|
+
# If the microservice belongs to a plugin that has a per-plugin UV venv
|
|
53
|
+
# (created by uvinstall during plugin install), use that isolated venv.
|
|
54
|
+
# Otherwise fall back to the shared PYTHONUSERBASE for legacy plugins.
|
|
55
|
+
plugin_name = microservice_config["plugin"]
|
|
56
|
+
plugin_venv_dir = nil
|
|
57
|
+
if plugin_name
|
|
58
|
+
scope = microservice_name.split("__")[0]
|
|
59
|
+
sanitized_name = "#{scope}__#{plugin_name}".tr('^a-zA-Z0-9_-', '_')
|
|
60
|
+
candidate = "/gems/plugin_venvs/#{sanitized_name}/.venv"
|
|
61
|
+
plugin_venv_dir = candidate if File.directory?(candidate)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if plugin_venv_dir
|
|
65
|
+
env['VIRTUAL_ENV'] = plugin_venv_dir
|
|
66
|
+
env['PATH'] = "#{plugin_venv_dir}/bin:#{ENV.fetch('PATH', '')}"
|
|
67
|
+
env['PYTHONUSERBASE'] = plugin_venv_dir
|
|
68
|
+
# Add the plugin venv's site-packages to PYTHONPATH so the base venv's
|
|
69
|
+
# Python binary can find plugin-specific packages. We keep the base binary
|
|
70
|
+
# because it has openc3 core installed; PYTHONPATH is always respected by
|
|
71
|
+
# CPython regardless of venv activation state.
|
|
72
|
+
site_packages = Dir.glob("#{plugin_venv_dir}/lib/python*/site-packages").first
|
|
73
|
+
existing_pythonpath = ENV.fetch('PYTHONPATH', '')
|
|
74
|
+
if site_packages
|
|
75
|
+
env['PYTHONPATH'] = existing_pythonpath.empty? ? site_packages : "#{site_packages}:#{existing_pythonpath}"
|
|
76
|
+
else
|
|
77
|
+
env['PYTHONPATH'] = existing_pythonpath.empty? ? nil : existing_pythonpath
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
env['PYTHONUSERBASE'] = '/gems/python_packages'
|
|
81
|
+
env['PYTHONPATH'] = ENV.fetch('PYTHONPATH', nil)
|
|
82
|
+
end
|
|
54
83
|
else
|
|
55
84
|
env['GEM_HOME'] = nil
|
|
56
85
|
env['PYTHONUSERBASE'] = nil
|
|
@@ -61,7 +61,7 @@ module OpenC3
|
|
|
61
61
|
# target name keyed by the packet name
|
|
62
62
|
def packets(target_name)
|
|
63
63
|
target_packets = @config.commands[target_name.to_s.upcase]
|
|
64
|
-
raise "Command target '#{target_name.to_s.upcase}' does not exist" unless target_packets
|
|
64
|
+
raise "Command target '#{target_name.to_s.upcase}' does not exist (packets lookup)" unless target_packets
|
|
65
65
|
|
|
66
66
|
target_packets
|
|
67
67
|
end
|
|
@@ -73,7 +73,7 @@ module OpenC3
|
|
|
73
73
|
def packet(target_name, packet_name)
|
|
74
74
|
target_packets = packets(target_name)
|
|
75
75
|
packet = target_packets[packet_name.to_s.upcase]
|
|
76
|
-
raise "Command packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist" unless packet
|
|
76
|
+
raise "Command packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist (packet lookup)" unless packet
|
|
77
77
|
|
|
78
78
|
packet
|
|
79
79
|
end
|
|
@@ -187,9 +187,9 @@ module OpenC3
|
|
|
187
187
|
|
|
188
188
|
# Lookup the command directly - avoid redundant upcase in packet()/packets()
|
|
189
189
|
target_packets = @config.commands[target_upcase]
|
|
190
|
-
raise "Command target '#{target_upcase}' does not exist" unless target_packets
|
|
190
|
+
raise "Command target '#{target_upcase}' does not exist (build_cmd)" unless target_packets
|
|
191
191
|
pkt = target_packets[packet_upcase]
|
|
192
|
-
raise "Command packet '#{target_upcase} #{packet_upcase}' does not exist" unless pkt
|
|
192
|
+
raise "Command packet '#{target_upcase} #{packet_upcase}' does not exist (build_cmd)" unless pkt
|
|
193
193
|
# Use deep_copy to avoid shared item modifications affecting the template
|
|
194
194
|
# This is critical for variable_bit_size items where handle_write_variable_bit_size
|
|
195
195
|
# modifies item.bit_offset and item.array_size during writes
|
|
@@ -159,16 +159,24 @@ module OpenC3
|
|
|
159
159
|
# @param packet_name [String] Packet Name
|
|
160
160
|
# @param item_name [String] Item Name
|
|
161
161
|
# @param state_name [String] State Name (e.g. 'CONNECTED')
|
|
162
|
-
# @param color [String or Symbol] New color: GREEN, YELLOW, or RED
|
|
163
|
-
#
|
|
162
|
+
# @param color [String or Symbol, nil] New color: GREEN, YELLOW, or RED.
|
|
163
|
+
# Pass nil to clear (remove) the state color.
|
|
164
|
+
# @return [Symbol, nil] The color that was set, or nil if the color was cleared
|
|
164
165
|
def set_state_color(target_name, packet_name, item_name, state_name, color)
|
|
165
166
|
packet = _get_packet(target_name, packet_name)
|
|
166
167
|
item = packet.get_item(item_name)
|
|
167
|
-
raise "Item #{target_name} #{packet_name} #{item_name} does not have any states" unless item.states
|
|
168
|
+
raise "Item '#{target_name} #{packet_name} #{item_name}' does not have any states" unless item.states
|
|
168
169
|
state_name = state_name.to_s.upcase
|
|
169
|
-
raise "State #{state_name} does not exist for #{target_name} #{packet_name} #{item_name}" unless item.states.key?(state_name)
|
|
170
|
+
raise "State '#{state_name}' does not exist for item '#{target_name} #{packet_name} #{item_name}'" unless item.states.key?(state_name)
|
|
171
|
+
if color.nil?
|
|
172
|
+
# Clear the state color while keeping state_colors as a Hash so limits
|
|
173
|
+
# checking (which indexes state_colors by value) continues to work.
|
|
174
|
+
item.state_colors.delete(state_name) if item.state_colors
|
|
175
|
+
packet.update_limits_items_cache(item)
|
|
176
|
+
return nil
|
|
177
|
+
end
|
|
170
178
|
color = color.to_s.upcase.intern
|
|
171
|
-
raise "Invalid state color #{color}. Must be one of #{PacketItem::STATE_COLORS.join(' ')}." unless PacketItem::STATE_COLORS.include?(color)
|
|
179
|
+
raise "Invalid state color '#{color}'. Must be one of #{PacketItem::STATE_COLORS.join(', ')}." unless PacketItem::STATE_COLORS.include?(color)
|
|
172
180
|
item.state_colors ||= {}
|
|
173
181
|
item.state_colors[state_name] = color
|
|
174
182
|
item.limits.enabled = true
|
|
@@ -182,10 +190,10 @@ module OpenC3
|
|
|
182
190
|
raise "LATEST packet not valid" if packet_name.upcase == LATEST_PACKET_NAME
|
|
183
191
|
|
|
184
192
|
packets = @config.telemetry[target_name.to_s.upcase]
|
|
185
|
-
raise "Telemetry target '#{target_name.to_s.upcase}' does not exist" unless packets
|
|
193
|
+
raise "Telemetry target '#{target_name.to_s.upcase}' does not exist (limits lookup)" unless packets
|
|
186
194
|
|
|
187
195
|
packet = packets[packet_name.to_s.upcase]
|
|
188
|
-
raise "Telemetry packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist" unless packet
|
|
196
|
+
raise "Telemetry packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist (limits lookup)" unless packet
|
|
189
197
|
|
|
190
198
|
return packet
|
|
191
199
|
end
|
|
@@ -616,7 +616,7 @@ module OpenC3
|
|
|
616
616
|
def get_item(name)
|
|
617
617
|
return super(name)
|
|
618
618
|
rescue ArgumentError
|
|
619
|
-
raise "
|
|
619
|
+
raise "Item '#{@target_name} #{@packet_name} #{name.upcase}' does not exist (Packet)"
|
|
620
620
|
end
|
|
621
621
|
|
|
622
622
|
# Read an item in the packet
|
|
@@ -124,7 +124,7 @@ module OpenC3
|
|
|
124
124
|
def get_state_colors(item)
|
|
125
125
|
color = @parser.parameters[2].upcase.to_sym
|
|
126
126
|
unless PacketItem::STATE_COLORS.include? color
|
|
127
|
-
raise @parser.error("Invalid state color #{color}. Must be one of #{PacketItem::STATE_COLORS.join(' ')}.", @usage)
|
|
127
|
+
raise @parser.error("Invalid state color '#{color}'. Must be one of #{PacketItem::STATE_COLORS.join(', ')}.", @usage)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
item.limits.enabled = true
|