openc3 5.11.3 → 5.13.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of openc3 might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/bin/openc3cli +29 -15
- data/data/config/_id_items.yaml +6 -4
- data/data/config/_id_params.yaml +9 -6
- data/data/config/_items.yaml +6 -4
- data/data/config/_params.yaml +3 -2
- data/data/config/graph_settings.yaml +1 -1
- data/data/config/interface_modifiers.yaml +1 -1
- data/data/config/item_modifiers.yaml +1 -2
- data/data/config/microservice.yaml +10 -1
- data/data/config/parameter_modifiers.yaml +13 -14
- data/data/config/plugins.yaml +13 -3
- data/data/config/screen.yaml +1 -2
- data/data/config/target.yaml +9 -0
- data/data/config/target_config.yaml +14 -6
- data/data/config/tool.yaml +12 -3
- data/lib/openc3/api/api.rb +1 -1
- data/lib/openc3/api/cmd_api.rb +123 -59
- data/lib/openc3/api/config_api.rb +12 -12
- data/lib/openc3/api/limits_api.rb +4 -3
- data/lib/openc3/api/settings_api.rb +5 -2
- data/lib/openc3/api/tlm_api.rb +70 -34
- data/lib/openc3/conversions/unix_time_conversion.rb +8 -6
- data/lib/openc3/interfaces/mqtt_interface.rb +11 -9
- data/lib/openc3/interfaces/mqtt_stream_interface.rb +78 -0
- data/lib/openc3/interfaces/tcpip_server_interface.rb +0 -7
- data/lib/openc3/io/json_drb.rb +3 -2
- data/lib/openc3/io/json_rpc.rb +6 -6
- data/lib/openc3/logs/buffered_packet_log_writer.rb +4 -2
- data/lib/openc3/logs/packet_log_reader.rb +2 -2
- data/lib/openc3/logs/packet_log_writer.rb +22 -7
- data/lib/openc3/logs/text_log_writer.rb +3 -2
- data/lib/openc3/microservices/cleanup_microservice.rb +8 -1
- data/lib/openc3/microservices/decom_microservice.rb +1 -1
- data/lib/openc3/microservices/interface_microservice.rb +2 -2
- data/lib/openc3/microservices/microservice.rb +5 -2
- data/lib/openc3/microservices/reaction_microservice.rb +1 -0
- data/lib/openc3/microservices/timeline_microservice.rb +7 -5
- data/lib/openc3/microservices/trigger_group_microservice.rb +2 -1
- data/lib/openc3/migrations/20231022000000_tlm_viewer_config.rb +22 -0
- data/lib/openc3/models/activity_model.rb +21 -3
- data/lib/openc3/models/cvt_model.rb +2 -1
- data/lib/openc3/models/gem_model.rb +4 -1
- data/lib/openc3/models/interface_model.rb +11 -5
- data/lib/openc3/models/metadata_model.rb +11 -0
- data/lib/openc3/models/microservice_model.rb +16 -3
- data/lib/openc3/models/model.rb +18 -0
- data/lib/openc3/models/note_model.rb +11 -0
- data/lib/openc3/models/plugin_model.rb +56 -4
- data/lib/openc3/models/python_package_model.rb +104 -0
- data/lib/openc3/models/scope_model.rb +2 -0
- data/lib/openc3/models/sorted_model.rb +17 -8
- data/lib/openc3/models/target_model.rb +53 -18
- data/lib/openc3/models/tool_config_model.rb +9 -3
- data/lib/openc3/models/tool_model.rb +22 -7
- data/lib/openc3/models/widget_model.rb +19 -3
- data/lib/openc3/operators/microservice_operator.rb +2 -0
- data/lib/openc3/packets/json_packet.rb +46 -15
- data/lib/openc3/packets/limits.rb +6 -18
- data/lib/openc3/packets/packet.rb +1 -0
- data/lib/openc3/packets/packet_config.rb +2 -1
- data/lib/openc3/packets/parsers/format_string_parser.rb +4 -4
- data/lib/openc3/packets/parsers/limits_parser.rb +4 -4
- data/lib/openc3/packets/parsers/limits_response_parser.rb +5 -5
- data/lib/openc3/packets/parsers/processor_parser.rb +4 -4
- data/lib/openc3/packets/parsers/state_parser.rb +3 -3
- data/lib/openc3/packets/parsers/xtce_parser.rb +5 -1
- data/lib/openc3/script/api_shared.rb +81 -63
- data/lib/openc3/script/calendar.rb +109 -0
- data/lib/openc3/script/commands.rb +18 -19
- data/lib/openc3/script/limits.rb +1 -1
- data/lib/openc3/script/{gems.rb → packages.rb} +20 -16
- data/lib/openc3/script/script.rb +49 -38
- data/lib/openc3/script/storage.rb +4 -4
- data/lib/openc3/script/web_socket_api.rb +2 -2
- data/lib/openc3/streams/mqtt_stream.rb +109 -0
- data/lib/openc3/system/system.rb +2 -0
- data/lib/openc3/system/target.rb +10 -1
- data/lib/openc3/top_level.rb +2 -2
- data/lib/openc3/utilities/aws_bucket.rb +3 -2
- data/lib/openc3/utilities/bucket_file_cache.rb +1 -1
- data/lib/openc3/utilities/cli_generator.rb +33 -20
- data/lib/openc3/utilities/local_mode.rb +5 -3
- data/lib/openc3/utilities/logger.rb +18 -17
- data/lib/openc3/utilities/process_manager.rb +1 -1
- data/lib/openc3/utilities/ruby_lex_utils.rb +0 -8
- data/lib/openc3/version.rb +6 -6
- data/templates/conversion/conversion.py +28 -0
- data/templates/conversion/conversion.rb +1 -18
- data/templates/limits_response/response.py +37 -0
- data/templates/limits_response/response.rb +0 -17
- data/templates/microservice/microservices/TEMPLATE/microservice.py +54 -0
- data/templates/microservice/microservices/TEMPLATE/microservice.rb +0 -7
- data/templates/plugin/.gitignore +1 -0
- data/templates/target/targets/TARGET/lib/target.py +9 -0
- data/templates/target/targets/TARGET/procedures/procedure.py +3 -0
- data/templates/tool_angular/package.json +22 -21
- data/templates/tool_angular/yarn.lock +2319 -3156
- data/templates/tool_react/package.json +16 -16
- data/templates/tool_react/yarn.lock +763 -645
- data/templates/tool_svelte/package.json +15 -14
- data/templates/tool_svelte/src/services/openc3-api.js +33 -82
- data/templates/tool_svelte/yarn.lock +748 -538
- data/templates/tool_vue/package.json +15 -14
- data/templates/tool_vue/yarn.lock +150 -64
- data/templates/widget/package.json +14 -13
- data/templates/widget/yarn.lock +133 -58
- metadata +60 -7
@@ -14,7 +14,7 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2023, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
@@ -79,7 +79,7 @@ module OpenC3
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def initialize(name, is_plugin: false)
|
82
|
-
|
82
|
+
@shutdown_complete = false
|
83
83
|
raise "Microservice must be named" unless name
|
84
84
|
|
85
85
|
@name = name
|
@@ -120,6 +120,7 @@ module OpenC3
|
|
120
120
|
# Get configuration for any targets
|
121
121
|
@target_names = @config["target_names"]
|
122
122
|
@target_names ||= []
|
123
|
+
# NOTE: setup_targets doesn't do anything if @target_names is empty
|
123
124
|
System.setup_targets(@target_names, @temp_dir, scope: @scope) unless is_plugin
|
124
125
|
|
125
126
|
# Use at_exit to shutdown cleanly no matter how we die
|
@@ -199,6 +200,7 @@ module OpenC3
|
|
199
200
|
end
|
200
201
|
|
201
202
|
def shutdown
|
203
|
+
return if @shutdown_complete
|
202
204
|
@logger.info("Shutting down microservice: #{@name}")
|
203
205
|
@cancel_thread = true
|
204
206
|
@microservice_status_sleeper.cancel if @microservice_status_sleeper
|
@@ -206,6 +208,7 @@ module OpenC3
|
|
206
208
|
FileUtils.remove_entry(@temp_dir) if File.exist?(@temp_dir)
|
207
209
|
@metric.shutdown
|
208
210
|
@logger.info("Shutting down microservice complete: #{@name}")
|
211
|
+
@shutdown_complete = true
|
209
212
|
end
|
210
213
|
end
|
211
214
|
end
|
@@ -14,7 +14,7 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2023, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
@@ -43,6 +43,7 @@ module OpenC3
|
|
43
43
|
|
44
44
|
def get_token(username)
|
45
45
|
if ENV['OPENC3_API_CLIENT'].nil?
|
46
|
+
ENV['OPENC3_API_PASSWORD'] ||= ENV['OPENC3_SERVICE_PASSWORD']
|
46
47
|
return OpenC3Authentication.new().token
|
47
48
|
else
|
48
49
|
# Check for offline access token
|
@@ -125,7 +126,8 @@ module OpenC3
|
|
125
126
|
|
126
127
|
def clear_expired(activity)
|
127
128
|
begin
|
128
|
-
ActivityModel.range_destroy(name: @timeline_name, scope: @scope, min: activity.start, max: activity.stop)
|
129
|
+
num = ActivityModel.range_destroy(name: @timeline_name, scope: @scope, min: activity.start, max: activity.stop)
|
130
|
+
@logger.info "#{@timeline_name} clear_expired removed #{num} items from #{activity.start} to #{activity.stop}"
|
129
131
|
activity.add_event(status: 'completed')
|
130
132
|
rescue StandardError => e
|
131
133
|
@logger.error "#{@timeline_name} clear_expired failed > #{activity.as_json(:allow_nan => true)} #{e.message}"
|
@@ -231,15 +233,15 @@ module OpenC3
|
|
231
233
|
@logger.info "#{@timeline_name} timeine manager exiting"
|
232
234
|
end
|
233
235
|
|
234
|
-
# Add task to remove events older than 7
|
236
|
+
# Add task to remove events older than 7 days
|
235
237
|
def add_expire_activity
|
236
238
|
now = Time.now.to_i
|
237
239
|
@expire = now + 3_000
|
238
240
|
activity = ActivityModel.new(
|
239
241
|
name: @timeline_name,
|
240
242
|
scope: @scope,
|
241
|
-
start:
|
242
|
-
stop: (now -
|
243
|
+
start: 0,
|
244
|
+
stop: (now - 86_400 * 7),
|
243
245
|
kind: 'EXPIRE',
|
244
246
|
data: {}
|
245
247
|
)
|
@@ -333,7 +333,8 @@ module OpenC3
|
|
333
333
|
)
|
334
334
|
return nil if packet.nil?
|
335
335
|
_, limit = packet.read_with_limits_state(operand[ITEM_TYPE], operand[ITEM_VALUE_TYPE].intern)
|
336
|
-
|
336
|
+
# Convert limit symbol to string since we'll be comparing with strings
|
337
|
+
return limit.to_s
|
337
338
|
end
|
338
339
|
|
339
340
|
# extract the value outlined in the operand to get the packet item value
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'openc3/utilities/migration'
|
2
|
+
require 'openc3/models/tool_config_model'
|
3
|
+
|
4
|
+
module OpenC3
|
5
|
+
class TlmViewerConfig < Migration
|
6
|
+
def self.run
|
7
|
+
ScopeModel.names.each do |scope|
|
8
|
+
# Get all existing ToolConfigModels and change keys from tlm_viewer to telemetry_viewer
|
9
|
+
names = ToolConfigModel.list_configs('tlm_viewer')
|
10
|
+
names.each do |name|
|
11
|
+
config = ToolConfigModel.load_config('tlm_viewer', name)
|
12
|
+
ToolConfigModel.save_config('telemetry_viewer', name, config)
|
13
|
+
ToolConfigModel.delete_config('tlm_viewer', name)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
unless ENV['OPENC3_NO_MIGRATE']
|
21
|
+
OpenC3::TlmViewerConfig.run
|
22
|
+
end
|
@@ -14,7 +14,7 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2023, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
@@ -94,13 +94,31 @@ module OpenC3
|
|
94
94
|
# Remove one member from a sorted set.
|
95
95
|
# @return [Integer] count of the members removed
|
96
96
|
def self.destroy(name:, scope:, score:)
|
97
|
-
Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score)
|
97
|
+
result = Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score)
|
98
|
+
notification = {
|
99
|
+
# start / stop to match SortedModel
|
100
|
+
'data' => JSON.generate({'start' => score}),
|
101
|
+
'kind' => 'deleted',
|
102
|
+
'type' => 'activity',
|
103
|
+
'timeline' => name
|
104
|
+
}
|
105
|
+
TimelineTopic.write_activity(notification, scope: scope)
|
106
|
+
return result
|
98
107
|
end
|
99
108
|
|
100
109
|
# Remove members from min to max of the sorted set.
|
101
110
|
# @return [Integer] count of the members removed
|
102
111
|
def self.range_destroy(name:, scope:, min:, max:)
|
103
|
-
Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", min, max)
|
112
|
+
result = Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", min, max)
|
113
|
+
notification = {
|
114
|
+
# start / stop to match SortedModel
|
115
|
+
'data' => JSON.generate({'start' => min, 'stop' => max}),
|
116
|
+
'kind' => 'deleted',
|
117
|
+
'type' => 'activity',
|
118
|
+
'timeline' => name
|
119
|
+
}
|
120
|
+
TimelineTopic.write_activity(notification, scope: scope)
|
121
|
+
return result
|
104
122
|
end
|
105
123
|
|
106
124
|
# @return [ActivityModel] Model generated from the passed JSON
|
@@ -237,10 +237,11 @@ module OpenC3
|
|
237
237
|
end
|
238
238
|
|
239
239
|
tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}"
|
240
|
-
@@override_cache[tgt_pkt_key] = [Time.now, hash]
|
241
240
|
if hash.empty?
|
241
|
+
@@override_cache.delete(tgt_pkt_key)
|
242
242
|
Store.hdel("#{scope}__override__#{target_name}", packet_name)
|
243
243
|
else
|
244
|
+
@@override_cache[tgt_pkt_key] = [Time.now, hash]
|
244
245
|
Store.hset("#{scope}__override__#{target_name}", packet_name, JSON.generate(hash.as_json(:allow_nan => true)))
|
245
246
|
end
|
246
247
|
end
|
@@ -27,6 +27,7 @@ require 'rubygems'
|
|
27
27
|
require 'rubygems/uninstaller'
|
28
28
|
require 'tempfile'
|
29
29
|
require 'openc3/utilities/process_manager'
|
30
|
+
require 'openc3/api/api'
|
30
31
|
require 'pathname'
|
31
32
|
|
32
33
|
module OpenC3
|
@@ -35,6 +36,8 @@ module OpenC3
|
|
35
36
|
# and destroy to allow interaction with gem files from the PluginModel and
|
36
37
|
# the GemsController.
|
37
38
|
class GemModel
|
39
|
+
include Api
|
40
|
+
|
38
41
|
def self.names
|
39
42
|
result = Pathname.new("#{ENV['GEM_HOME']}/gems").children.select { |c| c.directory? }.collect { |p| File.basename(p) + '.gem' }
|
40
43
|
return result.sort
|
@@ -53,7 +56,7 @@ module OpenC3
|
|
53
56
|
FileUtils.cp(gem_file_path, "#{ENV['GEM_HOME']}/cache/#{File.basename(gem_file_path)}")
|
54
57
|
if gem_install
|
55
58
|
Logger.info "Installing gem: #{gem_filename}"
|
56
|
-
result = OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "geminstall", gem_filename, scope], "
|
59
|
+
result = OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "geminstall", gem_filename, scope], "package_install", gem_filename, Time.now + 3600.0, scope: scope)
|
57
60
|
return result.name
|
58
61
|
end
|
59
62
|
else
|
@@ -145,7 +145,13 @@ module OpenC3
|
|
145
145
|
unless @cmd
|
146
146
|
type = self.class._get_type
|
147
147
|
microservice_name = "#{@scope}__#{type}__#{@name}"
|
148
|
-
|
148
|
+
if config_params[0] and File.extname(config_params[0]) == '.py'
|
149
|
+
work_dir.sub!('openc3/lib', 'openc3/python')
|
150
|
+
@cmd = ["python", "#{type.downcase}_microservice.py", microservice_name]
|
151
|
+
else
|
152
|
+
# If there are no config_params we assume ruby
|
153
|
+
@cmd = ["ruby", "#{type.downcase}_microservice.rb", microservice_name]
|
154
|
+
end
|
149
155
|
end
|
150
156
|
@work_dir = work_dir
|
151
157
|
@ports = ports
|
@@ -417,7 +423,7 @@ module OpenC3
|
|
417
423
|
# Respawn the microservice
|
418
424
|
type = self.class._get_type
|
419
425
|
microservice_name = "#{@scope}__#{type}__#{@name}"
|
420
|
-
microservice = MicroserviceModel.get_model(name: microservice_name, scope: scope)
|
426
|
+
microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
|
421
427
|
microservice.target_names.delete(target_name) unless @target_names.include?(target_name)
|
422
428
|
microservice.update
|
423
429
|
end
|
@@ -432,11 +438,11 @@ module OpenC3
|
|
432
438
|
|
433
439
|
if unmap_old
|
434
440
|
# Remove from old interface
|
435
|
-
all_interfaces = InterfaceModel.all(scope: scope)
|
441
|
+
all_interfaces = InterfaceModel.all(scope: @scope)
|
436
442
|
old_interface = nil
|
437
443
|
all_interfaces.each do |old_interface_name, old_interface_details|
|
438
444
|
if old_interface_details['target_names'].include?(target_name)
|
439
|
-
old_interface = InterfaceModel.from_json(old_interface_details, scope: scope)
|
445
|
+
old_interface = InterfaceModel.from_json(old_interface_details, scope: @scope)
|
440
446
|
old_interface.unmap_target(target_name, cmd_only: cmd_only, tlm_only: tlm_only) if old_interface
|
441
447
|
end
|
442
448
|
end
|
@@ -451,7 +457,7 @@ module OpenC3
|
|
451
457
|
# Respawn the microservice
|
452
458
|
type = self.class._get_type
|
453
459
|
microservice_name = "#{@scope}__#{type}__#{@name}"
|
454
|
-
microservice = MicroserviceModel.get_model(name: microservice_name, scope: scope)
|
460
|
+
microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
|
455
461
|
microservice.target_names << target_name unless microservice.target_names.include?(target_name)
|
456
462
|
microservice.update
|
457
463
|
end
|
@@ -33,6 +33,17 @@ module OpenC3
|
|
33
33
|
"#{scope}#{PRIMARY_KEY}"
|
34
34
|
end
|
35
35
|
|
36
|
+
def self.notify(scope:, kind:, start:, stop: nil)
|
37
|
+
json = {'type' => METADATA_TYPE, 'start' => start}
|
38
|
+
json['stop'] = stop if stop
|
39
|
+
notification = {
|
40
|
+
'data' => JSON.generate(json),
|
41
|
+
'kind' => kind,
|
42
|
+
'type' => 'calendar',
|
43
|
+
}
|
44
|
+
CalendarTopic.write_entry(notification, scope: scope)
|
45
|
+
end
|
46
|
+
|
36
47
|
attr_reader :color, :metadata, :constraints, :type
|
37
48
|
|
38
49
|
# @param [Integer] start - Time metadata is active in seconds from Epoch
|
@@ -42,6 +42,7 @@ module OpenC3
|
|
42
42
|
attr_accessor :parent
|
43
43
|
attr_accessor :secrets
|
44
44
|
attr_accessor :prefix
|
45
|
+
attr_accessor :disable_erb
|
45
46
|
|
46
47
|
# NOTE: The following three class methods are used by the ModelController
|
47
48
|
# and are reimplemented to enable various Model class methods to work
|
@@ -101,6 +102,7 @@ module OpenC3
|
|
101
102
|
needs_dependencies: false,
|
102
103
|
secrets: [],
|
103
104
|
prefix: nil,
|
105
|
+
disable_erb: nil,
|
104
106
|
scope:
|
105
107
|
)
|
106
108
|
parts = name.split("__")
|
@@ -125,6 +127,7 @@ module OpenC3
|
|
125
127
|
@needs_dependencies = needs_dependencies
|
126
128
|
@secrets = secrets
|
127
129
|
@prefix = prefix
|
130
|
+
@disable_erb = disable_erb
|
128
131
|
@bucket = Bucket.getClient()
|
129
132
|
end
|
130
133
|
|
@@ -145,7 +148,8 @@ module OpenC3
|
|
145
148
|
'plugin' => @plugin,
|
146
149
|
'needs_dependencies' => @needs_dependencies,
|
147
150
|
'secrets' => @secrets.as_json(*a),
|
148
|
-
'prefix' => @prefix
|
151
|
+
'prefix' => @prefix,
|
152
|
+
'disable_erb' => @disable_erb
|
149
153
|
}
|
150
154
|
end
|
151
155
|
|
@@ -201,6 +205,12 @@ module OpenC3
|
|
201
205
|
when 'ROUTE_PREFIX'
|
202
206
|
parser.verify_num_parameters(1, 1, "#{keyword} <Route Prefix>")
|
203
207
|
@prefix = parameters[0]
|
208
|
+
when 'DISABLE_ERB'
|
209
|
+
# 0 to unlimited parameters
|
210
|
+
@disable_erb ||= []
|
211
|
+
if parameters
|
212
|
+
@disable_erb.concat(parameters)
|
213
|
+
end
|
204
214
|
else
|
205
215
|
raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Microservice: #{keyword} #{parameters.join(" ")}")
|
206
216
|
end
|
@@ -220,8 +230,11 @@ module OpenC3
|
|
220
230
|
|
221
231
|
# Load microservice files
|
222
232
|
data = File.read(filename, mode: "rb")
|
223
|
-
|
224
|
-
|
233
|
+
erb_disabled = check_disable_erb(filename)
|
234
|
+
unless erb_disabled
|
235
|
+
OpenC3.set_working_dir(File.dirname(filename)) do
|
236
|
+
data = ERB.new(data.comment_erb(), trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable? and File.basename(filename)[0] != '_'
|
237
|
+
end
|
225
238
|
end
|
226
239
|
unless validate_only
|
227
240
|
@bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: key, body: data)
|
data/lib/openc3/models/model.rb
CHANGED
@@ -182,6 +182,24 @@ module OpenC3
|
|
182
182
|
'plugin' => @plugin,
|
183
183
|
'scope' => @scope }
|
184
184
|
end
|
185
|
+
|
186
|
+
def check_disable_erb(filename)
|
187
|
+
erb_disabled = false
|
188
|
+
if @disable_erb
|
189
|
+
if @disable_erb.length == 0
|
190
|
+
# Disable all ERB
|
191
|
+
erb_disabled = true
|
192
|
+
else
|
193
|
+
@disable_erb.each do |pattern|
|
194
|
+
if filename =~ Regexp.new(pattern)
|
195
|
+
erb_disabled = true
|
196
|
+
break
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
return erb_disabled
|
202
|
+
end
|
185
203
|
end
|
186
204
|
|
187
205
|
class EphemeralModel < Model
|
@@ -33,6 +33,17 @@ module OpenC3
|
|
33
33
|
"#{scope}#{PRIMARY_KEY}"
|
34
34
|
end
|
35
35
|
|
36
|
+
def self.notify(scope:, kind:, start:, stop: nil)
|
37
|
+
json = {'type' => NOTE_TYPE, 'start' => start}
|
38
|
+
json['stop'] = stop if stop
|
39
|
+
notification = {
|
40
|
+
'data' => JSON.generate(json),
|
41
|
+
'kind' => kind,
|
42
|
+
'type' => 'calendar',
|
43
|
+
}
|
44
|
+
CalendarTopic.write_entry(notification, scope: scope)
|
45
|
+
end
|
46
|
+
|
36
47
|
attr_reader :stop, :color, :description, :type
|
37
48
|
|
38
49
|
# @param [String] scope - OpenC3 scope to track event to
|
@@ -34,6 +34,7 @@ require 'openc3/models/router_model'
|
|
34
34
|
require 'openc3/models/tool_model'
|
35
35
|
require 'openc3/models/widget_model'
|
36
36
|
require 'openc3/models/microservice_model'
|
37
|
+
require 'openc3/api/api'
|
37
38
|
require 'tmpdir'
|
38
39
|
require 'tempfile'
|
39
40
|
require 'fileutils'
|
@@ -43,6 +44,8 @@ module OpenC3
|
|
43
44
|
# microservices and tools. The PluginModel installs all these pieces as well
|
44
45
|
# as destroys them all when the plugin is removed.
|
45
46
|
class PluginModel < Model
|
47
|
+
include Api
|
48
|
+
|
46
49
|
PRIMARY_KEY = 'openc3_plugins'
|
47
50
|
# Reserved VARIABLE names. See local_mode.rb: update_local_plugin()
|
48
51
|
RESERVED_VARIABLE_NAMES = ['target_name', 'microservice_name', 'scope']
|
@@ -170,6 +173,21 @@ module OpenC3
|
|
170
173
|
end
|
171
174
|
needs_dependencies = pkg.spec.runtime_dependencies.length > 0
|
172
175
|
needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
|
176
|
+
|
177
|
+
# Handle python requirements.txt
|
178
|
+
if File.exist?(File.join(gem_path, 'requirements.txt'))
|
179
|
+
begin
|
180
|
+
pypi_url = get_setting('pypi_url', scope: scope)
|
181
|
+
rescue
|
182
|
+
# If Redis isn't running try the ENV, then simply pypi.org/simple
|
183
|
+
pypi_url = ENV['PYPI_URL']
|
184
|
+
pypi_url ||= 'https://pypi.org/simple'
|
185
|
+
end
|
186
|
+
Logger.info "Installing python packages from requirements.txt"
|
187
|
+
puts `pip install --user -i #{pypi_url} -r #{File.join(gem_path, 'requirements.txt')}`
|
188
|
+
needs_dependencies = true
|
189
|
+
end
|
190
|
+
|
173
191
|
# If needs_dependencies hasn't already been set we need to scan the plugin.txt
|
174
192
|
# to see if they've explicitly set the NEEDS_DEPENDENCIES keyword
|
175
193
|
unless needs_dependencies
|
@@ -278,10 +296,15 @@ module OpenC3
|
|
278
296
|
|
279
297
|
# Undeploy all models associated with this plugin
|
280
298
|
def undeploy
|
299
|
+
errors = []
|
281
300
|
microservice_count = 0
|
282
301
|
microservices = MicroserviceModel.find_all_by_plugin(plugin: @name, scope: @scope)
|
283
302
|
microservices.each do |name, model_instance|
|
284
|
-
|
303
|
+
begin
|
304
|
+
model_instance.destroy
|
305
|
+
rescue Exception => error
|
306
|
+
errors << error
|
307
|
+
end
|
285
308
|
microservice_count += 1
|
286
309
|
end
|
287
310
|
# Wait for the operator to wake up and remove the microservice processes
|
@@ -290,15 +313,44 @@ module OpenC3
|
|
290
313
|
# Save TargetModel for last as it has the most to cleanup
|
291
314
|
[InterfaceModel, RouterModel, ToolModel, WidgetModel, TargetModel].each do |model|
|
292
315
|
model.find_all_by_plugin(plugin: @name, scope: @scope).each do |name, model_instance|
|
293
|
-
|
316
|
+
begin
|
317
|
+
model_instance.destroy
|
318
|
+
rescue Exception => error
|
319
|
+
errors << error
|
320
|
+
end
|
294
321
|
end
|
295
322
|
end
|
296
323
|
# Cleanup Redis stuff that might have been left by microservices
|
297
324
|
microservices.each do |name, model_instance|
|
298
|
-
|
325
|
+
begin
|
326
|
+
model_instance.cleanup
|
327
|
+
rescue Exception => error
|
328
|
+
errors << error
|
329
|
+
end
|
330
|
+
end
|
331
|
+
# Raise all the errors at once
|
332
|
+
if errors.length > 0
|
333
|
+
message = ''
|
334
|
+
errors.each do |error|
|
335
|
+
message += "\n#{error.formatted}\n"
|
336
|
+
end
|
337
|
+
raise message
|
299
338
|
end
|
300
339
|
rescue Exception => error
|
301
|
-
Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to #{error}")
|
340
|
+
Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to: #{error}")
|
341
|
+
ensure
|
342
|
+
# Double check everything is gone
|
343
|
+
found = []
|
344
|
+
[MicroserviceModel, InterfaceModel, RouterModel, ToolModel, WidgetModel, TargetModel].each do |model|
|
345
|
+
model.find_all_by_plugin(plugin: @name, scope: @scope).each do |name, model_instance|
|
346
|
+
found << model_instance
|
347
|
+
end
|
348
|
+
end
|
349
|
+
if found.length > 0
|
350
|
+
# If undeploy failed we need to not move forward with anything else
|
351
|
+
Logger.error("Error undeploying plugin model #{@name} in scope #{@scope} due to: Plugin submodels still exist after undeploy = #{found.length}")
|
352
|
+
raise "Plugin #{@name} submodels still exist after undeploy = #{found.length}"
|
353
|
+
end
|
302
354
|
end
|
303
355
|
|
304
356
|
# Reinstall
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
# Copyright 2023 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 'fileutils'
|
20
|
+
require 'openc3/utilities/process_manager'
|
21
|
+
require 'pathname'
|
22
|
+
|
23
|
+
module OpenC3
|
24
|
+
# This class acts like a Model but doesn't inherit from Model because it doesn't
|
25
|
+
# actual interact with the Store (Redis). Instead we implement names, get, put
|
26
|
+
# and destroy to allow interaction with python package files from the PluginModel and
|
27
|
+
# the PackagesController.
|
28
|
+
class PythonPackageModel
|
29
|
+
def self.names
|
30
|
+
paths = Dir.glob("#{ENV['PYTHONUSERBASE']}/lib/*")
|
31
|
+
results = []
|
32
|
+
paths.each do |path|
|
33
|
+
results.concat(Pathname.new(File.join(path, 'site-packages')).children.select { |c| c.directory? and File.extname(c) == '.dist-info' }.collect { |p| File.basename(p, '.dist-info') })
|
34
|
+
end
|
35
|
+
return results.sort
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.get(name)
|
39
|
+
path = "#{ENV['PYTHONUSERBASE']}/cache"
|
40
|
+
FileUtils.mkdir_p(path) unless Dir.exist?(path)
|
41
|
+
result = Pathname.new(path).children.select { |c| c.file? and File.basename(c, File.extname(c)) == name }
|
42
|
+
if result.length > 0
|
43
|
+
return result[0] if File.exist?(result[0])
|
44
|
+
end
|
45
|
+
raise "Package #{name} not found"
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.put(package_file_path, package_install: true, scope:)
|
49
|
+
if File.file?(package_file_path)
|
50
|
+
package_filename = File.basename(package_file_path)
|
51
|
+
FileUtils.mkdir_p("#{ENV['PYTHONUSERBASE']}/cache") unless Dir.exist?("#{ENV['PYTHONUSERBASE']}/cache")
|
52
|
+
cache_path = "#{ENV['PYTHONUSERBASE']}/cache/#{File.basename(package_file_path)}"
|
53
|
+
FileUtils.cp(package_file_path, cache_path)
|
54
|
+
if package_install
|
55
|
+
return self.install(cache_path, scope: scope)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
message = "Package file #{package_file_path} does not exist!"
|
59
|
+
Logger.error message
|
60
|
+
raise message
|
61
|
+
end
|
62
|
+
return nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.install(name_or_path, scope:)
|
66
|
+
if File.exist?(name_or_path)
|
67
|
+
package_file_path = name_or_path
|
68
|
+
else
|
69
|
+
package_file_path = get(name_or_path)
|
70
|
+
end
|
71
|
+
package_filename = File.basename(package_file_path)
|
72
|
+
begin
|
73
|
+
pypi_url = get_setting('pypi_url', scope: scope)
|
74
|
+
rescue
|
75
|
+
# If Redis isn't running try the ENV, then simply pypi.org/simple
|
76
|
+
pypi_url = ENV['PYPI_URL']
|
77
|
+
pypi_url ||= 'https://pypi.org/simple'
|
78
|
+
end
|
79
|
+
Logger.info "Installing python package: #{name_or_path}"
|
80
|
+
result = OpenC3::ProcessManager.instance.spawn(["pip", "install", "--user", "-i", pypi_url, package_file_path], "package_install", package_filename, Time.now + 3600.0, scope: scope)
|
81
|
+
return result.name
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.destroy(name, scope:)
|
85
|
+
package_name, version = self.extract_name_and_version(name)
|
86
|
+
Logger.info "Uninstalling package: #{name}"
|
87
|
+
result = OpenC3::ProcessManager.instance.spawn(["pip", "uninstall", package_name, "-y"], "package_uninstall", name, Time.now + 3600.0, scope: scope)
|
88
|
+
return result.name
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.extract_name_and_version(name)
|
92
|
+
split_name = name.split('-')
|
93
|
+
if split_name.length > 1
|
94
|
+
package_name = split_name[0..-2].join('-')
|
95
|
+
version = File.basename(split_name[-1], '.dist-info')
|
96
|
+
else
|
97
|
+
package_name = name
|
98
|
+
version = "Unknown"
|
99
|
+
end
|
100
|
+
|
101
|
+
return package_name, version
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -305,6 +305,8 @@ module OpenC3
|
|
305
305
|
SettingModel.set({ name: 'source_url', data: 'https://github.com/OpenC3/cosmos' }, scope: @scope) unless setting
|
306
306
|
setting = SettingModel.get(name: 'rubygems_url')
|
307
307
|
SettingModel.set({ name: 'rubygems_url', data: 'https://rubygems.org' }, scope: @scope) unless setting
|
308
|
+
setting = SettingModel.get(name: 'pypi_url')
|
309
|
+
SettingModel.set({ name: 'pypi_url', data: 'https://pypi.org/simple' }, scope: @scope) unless setting
|
308
310
|
end
|
309
311
|
end
|
310
312
|
end
|
@@ -14,10 +14,10 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2023, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
|
-
# This file may also be used under the terms of a commercial license
|
20
|
+
# This file may also be used under the terms of a commercial license
|
21
21
|
# if purchased from OpenC3, Inc.
|
22
22
|
|
23
23
|
# https://www.rubydoc.info/gems/redis/Redis/Commands/SortedSets
|
@@ -39,7 +39,12 @@ module OpenC3
|
|
39
39
|
|
40
40
|
# MUST be overriden by any subclasses
|
41
41
|
def self.pk(scope)
|
42
|
-
"#{scope}#{PRIMARY_KEY}"
|
42
|
+
return "#{scope}#{PRIMARY_KEY}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# MUST be overriden by any subclasses
|
46
|
+
def self.notify(scope:, kind:, start:, stop: nil)
|
47
|
+
# Do nothing by default
|
43
48
|
end
|
44
49
|
|
45
50
|
# @return [String|nil] String of the saved json or nil if start not found
|
@@ -52,7 +57,7 @@ module OpenC3
|
|
52
57
|
# @return [Array<Hash>] Array up to the limit of the models (as Hash objects) stored under the primary key
|
53
58
|
def self.all(scope:, limit: 100)
|
54
59
|
result = Store.zrevrangebyscore(self.pk(scope), '+inf', '-inf', limit: [0, limit])
|
55
|
-
result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
|
60
|
+
return result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
|
56
61
|
end
|
57
62
|
|
58
63
|
# @return [String|nil] json or nil if metadata empty
|
@@ -71,24 +76,28 @@ module OpenC3
|
|
71
76
|
raise SortedInputError.new "start: #{start} must be before stop: #{stop}"
|
72
77
|
end
|
73
78
|
result = Store.zrangebyscore(self.pk(scope), start, stop, limit: [0, limit])
|
74
|
-
result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
|
79
|
+
return result.map { |item| JSON.parse(item, :allow_nan => true, :create_additions => true) }
|
75
80
|
end
|
76
81
|
|
77
82
|
# @return [Integer] count of the members stored under the primary key
|
78
83
|
def self.count(scope:)
|
79
|
-
Store.zcard(self.pk(scope))
|
84
|
+
return Store.zcard(self.pk(scope))
|
80
85
|
end
|
81
86
|
|
82
87
|
# Remove member from a sorted set
|
83
88
|
# @return [Integer] count of the members removed, 0 if not found
|
84
89
|
def self.destroy(scope:, start:)
|
85
|
-
Store.zremrangebyscore(self.pk(scope), start, start)
|
90
|
+
result = Store.zremrangebyscore(self.pk(scope), start, start)
|
91
|
+
self.notify(kind: 'deleted', start: start, scope: scope)
|
92
|
+
return result
|
86
93
|
end
|
87
94
|
|
88
95
|
# Remove members from min to max of the sorted set.
|
89
96
|
# @return [Integer] count of the members removed
|
90
97
|
def self.range_destroy(scope:, start:, stop:)
|
91
|
-
Store.zremrangebyscore(self.pk(scope), start, stop)
|
98
|
+
result = Store.zremrangebyscore(self.pk(scope), start, stop)
|
99
|
+
self.notify(kind: 'deleted', start: start, stop: stop, scope: scope)
|
100
|
+
return result
|
92
101
|
end
|
93
102
|
|
94
103
|
attr_reader :start
|