openc3 7.2.0 → 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 +108 -29
- data/bin/pipinstall +5 -4
- data/bin/pipuninstall +5 -1
- data/bin/uvinstall +149 -0
- data/data/config/interface_modifiers.yaml +15 -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 +38 -0
- data/data/config/tool.yaml +3 -1
- data/data/config/widgets.yaml +43 -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/binary_accessor.rb +4 -0
- data/lib/openc3/accessors/template_accessor.rb +2 -2
- data/lib/openc3/api/cmd_api.rb +5 -1
- data/lib/openc3/api/interface_api.rb +2 -2
- data/lib/openc3/api/limits_api.rb +89 -3
- 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/decom_common.rb +15 -4
- data/lib/openc3/microservices/decom_microservice.rb +6 -1
- data/lib/openc3/microservices/interface_microservice.rb +26 -12
- data/lib/openc3/microservices/queue_microservice.rb +4 -2
- data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
- data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
- data/lib/openc3/models/cvt_model.rb +7 -7
- data/lib/openc3/models/db_sharded_model.rb +2 -2
- data/lib/openc3/models/gem_model.rb +1 -1
- data/lib/openc3/models/interface_model.rb +17 -11
- data/lib/openc3/models/interface_status_model.rb +2 -2
- data/lib/openc3/models/metric_model.rb +2 -2
- data/lib/openc3/models/microservice_model.rb +21 -0
- data/lib/openc3/models/microservice_status_model.rb +2 -2
- data/lib/openc3/models/model.rb +2 -6
- data/lib/openc3/models/plugin_model.rb +204 -49
- data/lib/openc3/models/python_package_model.rb +210 -12
- data/lib/openc3/models/reingest_job_model.rb +1 -1
- data/lib/openc3/models/scope_model.rb +34 -30
- data/lib/openc3/models/script_status_model.rb +73 -38
- data/lib/openc3/models/target_model.rb +193 -6
- data/lib/openc3/models/tool_model.rb +33 -20
- data/lib/openc3/operators/microservice_operator.rb +33 -4
- data/lib/openc3/operators/operator.rb +15 -5
- data/lib/openc3/packets/commands.rb +7 -7
- data/lib/openc3/packets/limits.rb +33 -2
- data/lib/openc3/packets/packet.rb +6 -4
- data/lib/openc3/packets/packet_config.rb +5 -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 +16 -12
- 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/limits.rb +1 -1
- data/lib/openc3/script/suite_runner.rb +43 -9
- data/lib/openc3/script/web_socket_api.rb +45 -5
- data/lib/openc3/system/target.rb +4 -1
- data/lib/openc3/tools/table_manager/table_config.rb +4 -1
- data/lib/openc3/top_level.rb +1 -1
- data/lib/openc3/topics/command_decom_topic.rb +16 -1
- data/lib/openc3/topics/limits_event_topic.rb +63 -4
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
- data/lib/openc3/utilities/aws_bucket.rb +10 -2
- data/lib/openc3/utilities/cli_generator.rb +5 -1
- data/lib/openc3/utilities/csv.rb +5 -5
- data/lib/openc3/utilities/env_helper.rb +13 -1
- data/lib/openc3/utilities/local_mode.rb +4 -1
- data/lib/openc3/utilities/pypi_url.rb +39 -0
- data/lib/openc3/utilities/reingest_job.rb +1 -1
- data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
- data/lib/openc3/utilities/running_script.rb +160 -100
- data/lib/openc3/utilities/script.rb +60 -13
- data/lib/openc3/utilities/store_autoload.rb +39 -1
- data/lib/openc3/utilities/store_queued.rb +5 -1
- data/lib/openc3/version.rb +5 -5
- data/lib/openc3.rb +6 -1
- 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 +7 -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
|
|
@@ -62,48 +62,83 @@ module OpenC3
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def self.all(scope:, offset: 0, limit: 10, type: "running")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
result = result.map do |r|
|
|
75
|
-
if r.nil?
|
|
76
|
-
nil
|
|
77
|
-
else
|
|
78
|
-
JSON.parse(r, allow_nan: true, create_additions: true)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
return result
|
|
82
|
-
else
|
|
83
|
-
keys = self.store.zrevrange("#{COMPLETED_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
84
|
-
return [] if keys.empty?
|
|
85
|
-
result = []
|
|
86
|
-
result = self.store.redis_pool.pipelined do
|
|
87
|
-
keys.each do |key|
|
|
88
|
-
self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
|
|
89
|
-
end
|
|
65
|
+
primary_key = (type == "running") ? RUNNING_PRIMARY_KEY : COMPLETED_PRIMARY_KEY
|
|
66
|
+
keys = self.store.zrevrange("#{primary_key}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
67
|
+
return [] if keys.empty?
|
|
68
|
+
|
|
69
|
+
result = self.store.redis_pool.pipelined do
|
|
70
|
+
keys.each do |key|
|
|
71
|
+
self.store.hget("#{primary_key}__#{scope}", key)
|
|
90
72
|
end
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
end
|
|
74
|
+
result = result.map do |r|
|
|
75
|
+
if r.nil?
|
|
76
|
+
nil
|
|
77
|
+
else
|
|
78
|
+
JSON.parse(r, allow_nan: true, create_additions: true)
|
|
97
79
|
end
|
|
98
|
-
return result
|
|
99
80
|
end
|
|
81
|
+
return result
|
|
100
82
|
end
|
|
101
83
|
|
|
102
84
|
def self.count(scope:, type: "running")
|
|
103
|
-
|
|
104
|
-
|
|
85
|
+
primary_key = (type == "running") ? RUNNING_PRIMARY_KEY : COMPLETED_PRIMARY_KEY
|
|
86
|
+
return self.store.zcount("#{primary_key}__#{scope}__LIST", 0, Float::INFINITY)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Fields that a search term is matched against (case-insensitive substring)
|
|
90
|
+
SEARCH_FIELDS = ['name', 'filename', 'username', 'user_full_name', 'state']
|
|
91
|
+
|
|
92
|
+
# Return a single page of items along with the total count of matching items.
|
|
93
|
+
# When searching, this fetches, parses, and filters the full list only once
|
|
94
|
+
# (instead of doing that work separately for the page and the count).
|
|
95
|
+
# Returns [page_items, total]
|
|
96
|
+
def self.page(scope:, offset: 0, limit: 10, type: "running", search: nil)
|
|
97
|
+
primary_key = (type == "running") ? RUNNING_PRIMARY_KEY : COMPLETED_PRIMARY_KEY
|
|
98
|
+
if search and !search.to_s.empty?
|
|
99
|
+
items = self.filter_by_search(self.fetch_all(primary_key, scope), search)
|
|
100
|
+
return [items[offset.to_i, limit.to_i] || [], items.length]
|
|
105
101
|
else
|
|
106
|
-
|
|
102
|
+
total = self.store.zcount("#{primary_key}__#{scope}__LIST", 0, Float::INFINITY)
|
|
103
|
+
keys = self.store.zrevrange("#{primary_key}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
104
|
+
page_items = keys.empty? ? [] : self.fetch(primary_key, scope, keys)
|
|
105
|
+
return [page_items, total]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Fetch and parse the hash values for the given keys, preserving their order
|
|
110
|
+
def self.fetch(primary_key, scope, keys)
|
|
111
|
+
result = self.store.redis_pool.pipelined do
|
|
112
|
+
keys.each do |key|
|
|
113
|
+
self.store.hget("#{primary_key}__#{scope}", key)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
result.map do |r|
|
|
117
|
+
# NOTE: create_additions is intentionally omitted here. ScriptStatusModel
|
|
118
|
+
# records are written via JSON.generate of as_json (plain hashes/arrays/
|
|
119
|
+
# primitives) and never contain a "json_class" marker, so create_additions
|
|
120
|
+
# has nothing to reconstruct. Enabling it disables the parser's fast path
|
|
121
|
+
# and makes this parse ~3x slower - costly since this runs over every
|
|
122
|
+
# record when listing/searching.
|
|
123
|
+
r.nil? ? nil : JSON.parse(r, allow_nan: true)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Fetch and parse every item for the type, in list order (most recent first)
|
|
128
|
+
def self.fetch_all(primary_key, scope)
|
|
129
|
+
keys = self.store.zrevrange("#{primary_key}__#{scope}__LIST", 0, -1)
|
|
130
|
+
return [] if keys.empty?
|
|
131
|
+
|
|
132
|
+
self.fetch(primary_key, scope, keys).compact
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Select items where any SEARCH_FIELDS value contains the search term (case-insensitive)
|
|
136
|
+
def self.filter_by_search(items, search)
|
|
137
|
+
search = search.to_s.downcase
|
|
138
|
+
items.select do |item|
|
|
139
|
+
next false if item.nil?
|
|
140
|
+
|
|
141
|
+
SEARCH_FIELDS.any? { |field| item[field].to_s.downcase.include?(search) }
|
|
107
142
|
end
|
|
108
143
|
end
|
|
109
144
|
|
|
@@ -175,7 +210,7 @@ module OpenC3
|
|
|
175
210
|
|
|
176
211
|
# Update the Redis hash at primary_key and set the field "name"
|
|
177
212
|
# to the JSON generated via calling as_json
|
|
178
|
-
def create(update: false, force: false, queued: false
|
|
213
|
+
def create(update: false, force: false, queued: false)
|
|
179
214
|
@updated_at = Time.now.utc.to_nsec_from_epoch
|
|
180
215
|
|
|
181
216
|
if queued
|
|
@@ -198,9 +233,9 @@ module OpenC3
|
|
|
198
233
|
|
|
199
234
|
# Move to completed
|
|
200
235
|
@primary_key = "#{COMPLETED_PRIMARY_KEY}__#{@scope}"
|
|
201
|
-
create(update: false, force: force, queued: queued
|
|
236
|
+
create(update: false, force: force, queued: queued)
|
|
202
237
|
else
|
|
203
|
-
create(update: true, force: force, queued: queued
|
|
238
|
+
create(update: true, force: force, queued: queued)
|
|
204
239
|
end
|
|
205
240
|
end
|
|
206
241
|
|
|
@@ -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
|
|
@@ -63,6 +65,7 @@ module OpenC3
|
|
|
63
65
|
attr_accessor :ignored_parameters
|
|
64
66
|
attr_accessor :ignored_items
|
|
65
67
|
attr_accessor :limits_groups
|
|
68
|
+
attr_accessor :stored_limits_mode
|
|
66
69
|
attr_accessor :cmd_tlm_files
|
|
67
70
|
attr_accessor :id
|
|
68
71
|
attr_accessor :cmd_buffer_depth
|
|
@@ -75,6 +78,7 @@ module OpenC3
|
|
|
75
78
|
attr_accessor :tlm_log_retain_time
|
|
76
79
|
attr_accessor :cmd_decom_retain_time
|
|
77
80
|
attr_accessor :tlm_decom_retain_time
|
|
81
|
+
attr_accessor :decom_flush_period
|
|
78
82
|
attr_accessor :cleanup_poll_time
|
|
79
83
|
attr_accessor :needs_dependencies
|
|
80
84
|
attr_accessor :target_microservices
|
|
@@ -122,6 +126,63 @@ module OpenC3
|
|
|
122
126
|
targets.sort.to_h
|
|
123
127
|
end
|
|
124
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
|
+
|
|
125
186
|
# Given target's modified file list
|
|
126
187
|
def self.modified_files(target_name, scope:)
|
|
127
188
|
modified = []
|
|
@@ -144,7 +205,19 @@ module OpenC3
|
|
|
144
205
|
modified.sort
|
|
145
206
|
end
|
|
146
207
|
|
|
147
|
-
|
|
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
|
|
148
221
|
if ENV['OPENC3_LOCAL_MODE']
|
|
149
222
|
OpenC3::LocalMode.delete_modified(target_name, scope: scope)
|
|
150
223
|
end
|
|
@@ -223,7 +296,7 @@ module OpenC3
|
|
|
223
296
|
|
|
224
297
|
# Assume it exists and just try to get it to avoid an extra call to Store.exist?
|
|
225
298
|
json = store_for_target(target_name, scope: scope).hget("#{scope}__openc3#{type.to_s.downcase}__#{target_name}", packet_name)
|
|
226
|
-
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?
|
|
227
300
|
|
|
228
301
|
packet = JSON.parse(json, allow_nan: true, create_additions: true)
|
|
229
302
|
|
|
@@ -244,7 +317,7 @@ module OpenC3
|
|
|
244
317
|
# @return [Array<Hash>] All packet hashes under the target_name
|
|
245
318
|
def self.packets(target_name, type: :TLM, scope:)
|
|
246
319
|
raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
|
|
247
|
-
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)
|
|
248
321
|
|
|
249
322
|
result = []
|
|
250
323
|
packets = store_for_target(target_name, scope: scope).hgetall("#{scope}__openc3#{type.to_s.downcase}__#{target_name}")
|
|
@@ -280,7 +353,7 @@ module OpenC3
|
|
|
280
353
|
def self.packet_item(target_name, packet_name, item_name, type: :TLM, scope:)
|
|
281
354
|
packet = packet(target_name, packet_name, type: type, scope: scope)
|
|
282
355
|
item = packet['items'].find { |item| item['name'] == item_name.to_s }
|
|
283
|
-
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
|
|
284
357
|
item
|
|
285
358
|
end
|
|
286
359
|
|
|
@@ -384,6 +457,7 @@ module OpenC3
|
|
|
384
457
|
ignored_parameters: [],
|
|
385
458
|
ignored_items: [],
|
|
386
459
|
limits_groups: [],
|
|
460
|
+
stored_limits_mode: 'PROCESS',
|
|
387
461
|
cmd_tlm_files: [],
|
|
388
462
|
id: nil,
|
|
389
463
|
updated_at: nil,
|
|
@@ -398,6 +472,7 @@ module OpenC3
|
|
|
398
472
|
tlm_log_retain_time: nil,
|
|
399
473
|
cmd_decom_retain_time: nil,
|
|
400
474
|
tlm_decom_retain_time: nil,
|
|
475
|
+
decom_flush_period: 5.0,
|
|
401
476
|
cleanup_poll_time: 3600,
|
|
402
477
|
needs_dependencies: false,
|
|
403
478
|
target_microservices: {},
|
|
@@ -412,6 +487,8 @@ module OpenC3
|
|
|
412
487
|
@ignored_parameters = ignored_parameters
|
|
413
488
|
@ignored_items = ignored_items
|
|
414
489
|
@limits_groups = limits_groups
|
|
490
|
+
@stored_limits_mode = stored_limits_mode.to_s.upcase
|
|
491
|
+
@stored_limits_mode = 'PROCESS' unless %w(PROCESS LOG DISABLE).include?(@stored_limits_mode)
|
|
415
492
|
@cmd_tlm_files = cmd_tlm_files
|
|
416
493
|
@id = id
|
|
417
494
|
@cmd_buffer_depth = cmd_buffer_depth
|
|
@@ -424,6 +501,7 @@ module OpenC3
|
|
|
424
501
|
@tlm_log_retain_time = tlm_log_retain_time
|
|
425
502
|
@cmd_decom_retain_time = cmd_decom_retain_time
|
|
426
503
|
@tlm_decom_retain_time = tlm_decom_retain_time
|
|
504
|
+
@decom_flush_period = decom_flush_period
|
|
427
505
|
@cleanup_poll_time = cleanup_poll_time
|
|
428
506
|
@needs_dependencies = needs_dependencies
|
|
429
507
|
@target_microservices = target_microservices
|
|
@@ -442,6 +520,7 @@ module OpenC3
|
|
|
442
520
|
'ignored_parameters' => @ignored_parameters,
|
|
443
521
|
'ignored_items' => @ignored_items,
|
|
444
522
|
'limits_groups' => @limits_groups,
|
|
523
|
+
'stored_limits_mode' => @stored_limits_mode,
|
|
445
524
|
'cmd_tlm_files' => @cmd_tlm_files,
|
|
446
525
|
'id' => @id,
|
|
447
526
|
'updated_at' => @updated_at,
|
|
@@ -456,6 +535,7 @@ module OpenC3
|
|
|
456
535
|
'tlm_log_retain_time' => @tlm_log_retain_time,
|
|
457
536
|
'cmd_decom_retain_time' => @cmd_decom_retain_time,
|
|
458
537
|
'tlm_decom_retain_time' => @tlm_decom_retain_time,
|
|
538
|
+
'decom_flush_period' => @decom_flush_period,
|
|
459
539
|
'cleanup_poll_time' => @cleanup_poll_time,
|
|
460
540
|
'needs_dependencies' => @needs_dependencies,
|
|
461
541
|
'target_microservices' => @target_microservices.as_json(),
|
|
@@ -510,6 +590,9 @@ module OpenC3
|
|
|
510
590
|
if @tlm_decom_retain_time and !@tlm_decom_retain_time.match?(/^\d+[hdwMy]$/)
|
|
511
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)")
|
|
512
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])
|
|
513
596
|
when 'REDUCED_MINUTE_LOG_RETAIN_TIME', 'REDUCED_HOUR_LOG_RETAIN_TIME', 'REDUCED_DAY_LOG_RETAIN_TIME', 'REDUCED_LOG_RETAIN_TIME'
|
|
514
597
|
# DEPRECATED
|
|
515
598
|
when 'REDUCER_DISABLE', 'REDUCER_DISABLED', 'REDUCER_MAX_CPU_UTILIZATION', 'REDUCED_MAX_CPU_UTILIZATION'
|
|
@@ -557,17 +640,37 @@ module OpenC3
|
|
|
557
640
|
parser.verify_num_parameters(1, 1, "#{keyword} <Shard Number Starting from 0>")
|
|
558
641
|
@db_shard = Integer(parameters[0])
|
|
559
642
|
|
|
643
|
+
when 'STORED_LIMITS_MODE'
|
|
644
|
+
parser.verify_num_parameters(1, 1, "#{keyword} <PROCESS, LOG, or DISABLE>")
|
|
645
|
+
mode = parameters[0].to_s.upcase
|
|
646
|
+
unless %w(PROCESS LOG DISABLE).include?(mode)
|
|
647
|
+
raise ConfigParser::Error.new(parser, "STORED_LIMITS_MODE must be one of PROCESS, LOG, or DISABLE")
|
|
648
|
+
end
|
|
649
|
+
@stored_limits_mode = mode
|
|
650
|
+
|
|
560
651
|
else
|
|
561
652
|
raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Target: #{keyword} #{parameters.join(" ")}")
|
|
562
653
|
end
|
|
563
654
|
return nil
|
|
564
655
|
end
|
|
565
656
|
|
|
566
|
-
|
|
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)
|
|
567
668
|
variables["target_name"] = @name
|
|
568
669
|
start_path = "/targets/#{@folder_name}/"
|
|
569
670
|
temp_dir = Dir.mktmpdir
|
|
570
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?
|
|
571
674
|
begin
|
|
572
675
|
target_path = gem_path + start_path + "**/*"
|
|
573
676
|
Dir.glob(target_path) do |filename|
|
|
@@ -603,15 +706,30 @@ module OpenC3
|
|
|
603
706
|
File.open(local_path, 'wb') { |file| file.write(data) }
|
|
604
707
|
found = true
|
|
605
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
|
|
606
719
|
end
|
|
607
720
|
raise "No target files found at #{target_path}" unless found
|
|
608
721
|
|
|
609
722
|
target_folder = File.join(temp_dir, @name)
|
|
610
723
|
# Build a System for just this target
|
|
611
724
|
system = System.new([@name], temp_dir)
|
|
725
|
+
check_column_header_lengths(system)
|
|
612
726
|
if variables["xtce_output"]
|
|
613
727
|
puts "Converting target #{@name} to .xtce files in #{variables["xtce_output"]}/#{@name}"
|
|
614
|
-
|
|
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
|
|
615
733
|
system.packet_config.to_xtce(variables["xtce_output"], variables['time_association_name'])
|
|
616
734
|
end
|
|
617
735
|
unless validate_only
|
|
@@ -625,6 +743,74 @@ module OpenC3
|
|
|
625
743
|
end
|
|
626
744
|
end
|
|
627
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
|
+
|
|
628
814
|
def undeploy
|
|
629
815
|
prefix = "#{@scope}/targets/#{@name}/"
|
|
630
816
|
objects = @bucket.list_objects(bucket: ENV['OPENC3_CONFIG_BUCKET'], prefix: prefix)
|
|
@@ -1019,6 +1205,7 @@ module OpenC3
|
|
|
1019
1205
|
options = []
|
|
1020
1206
|
options << ["CMD_DECOM_RETAIN_TIME", @cmd_decom_retain_time] if @cmd_decom_retain_time
|
|
1021
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
|
|
1022
1209
|
microservice = MicroserviceModel.new(
|
|
1023
1210
|
name: microservice_name,
|
|
1024
1211
|
folder_name: @folder_name,
|