openc3 5.11.3 → 5.12.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.

Potentially problematic release.


This version of openc3 might be problematic. Click here for more details.

Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/bin/openc3cli +26 -12
  4. data/data/config/_id_items.yaml +6 -4
  5. data/data/config/_id_params.yaml +9 -6
  6. data/data/config/_items.yaml +6 -4
  7. data/data/config/_params.yaml +3 -2
  8. data/data/config/interface_modifiers.yaml +1 -1
  9. data/data/config/microservice.yaml +10 -1
  10. data/data/config/plugins.yaml +13 -3
  11. data/data/config/target.yaml +9 -0
  12. data/data/config/target_config.yaml +12 -0
  13. data/data/config/tool.yaml +12 -3
  14. data/lib/openc3/api/api.rb +1 -1
  15. data/lib/openc3/api/cmd_api.rb +24 -24
  16. data/lib/openc3/api/config_api.rb +12 -12
  17. data/lib/openc3/api/limits_api.rb +4 -3
  18. data/lib/openc3/api/settings_api.rb +5 -2
  19. data/lib/openc3/api/tlm_api.rb +7 -10
  20. data/lib/openc3/conversions/unix_time_conversion.rb +8 -6
  21. data/lib/openc3/interfaces/tcpip_server_interface.rb +0 -7
  22. data/lib/openc3/io/json_drb.rb +3 -2
  23. data/lib/openc3/io/json_rpc.rb +6 -6
  24. data/lib/openc3/logs/buffered_packet_log_writer.rb +4 -2
  25. data/lib/openc3/logs/packet_log_writer.rb +22 -7
  26. data/lib/openc3/microservices/cleanup_microservice.rb +8 -1
  27. data/lib/openc3/microservices/decom_microservice.rb +1 -1
  28. data/lib/openc3/microservices/interface_microservice.rb +2 -2
  29. data/lib/openc3/microservices/microservice.rb +5 -2
  30. data/lib/openc3/microservices/reaction_microservice.rb +1 -0
  31. data/lib/openc3/microservices/timeline_microservice.rb +7 -5
  32. data/lib/openc3/migrations/20231022000000_tlm_viewer_config.rb +22 -0
  33. data/lib/openc3/models/activity_model.rb +21 -3
  34. data/lib/openc3/models/cvt_model.rb +2 -1
  35. data/lib/openc3/models/gem_model.rb +4 -1
  36. data/lib/openc3/models/interface_model.rb +11 -5
  37. data/lib/openc3/models/metadata_model.rb +11 -0
  38. data/lib/openc3/models/microservice_model.rb +16 -3
  39. data/lib/openc3/models/model.rb +18 -0
  40. data/lib/openc3/models/note_model.rb +11 -0
  41. data/lib/openc3/models/plugin_model.rb +18 -0
  42. data/lib/openc3/models/python_package_model.rb +104 -0
  43. data/lib/openc3/models/scope_model.rb +2 -0
  44. data/lib/openc3/models/sorted_model.rb +17 -8
  45. data/lib/openc3/models/target_model.rb +53 -18
  46. data/lib/openc3/models/tool_config_model.rb +9 -3
  47. data/lib/openc3/models/tool_model.rb +22 -7
  48. data/lib/openc3/models/widget_model.rb +19 -3
  49. data/lib/openc3/operators/microservice_operator.rb +2 -0
  50. data/lib/openc3/packets/limits.rb +6 -18
  51. data/lib/openc3/packets/packet.rb +1 -0
  52. data/lib/openc3/packets/parsers/format_string_parser.rb +4 -4
  53. data/lib/openc3/packets/parsers/limits_parser.rb +4 -4
  54. data/lib/openc3/packets/parsers/limits_response_parser.rb +5 -5
  55. data/lib/openc3/packets/parsers/processor_parser.rb +4 -4
  56. data/lib/openc3/packets/parsers/state_parser.rb +3 -3
  57. data/lib/openc3/script/api_shared.rb +50 -32
  58. data/lib/openc3/script/calendar.rb +109 -0
  59. data/lib/openc3/script/commands.rb +1 -8
  60. data/lib/openc3/script/{gems.rb → packages.rb} +20 -16
  61. data/lib/openc3/script/script.rb +49 -38
  62. data/lib/openc3/system/system.rb +2 -0
  63. data/lib/openc3/system/target.rb +10 -1
  64. data/lib/openc3/top_level.rb +2 -2
  65. data/lib/openc3/utilities/aws_bucket.rb +3 -2
  66. data/lib/openc3/utilities/bucket_file_cache.rb +1 -1
  67. data/lib/openc3/utilities/local_mode.rb +3 -1
  68. data/lib/openc3/utilities/logger.rb +1 -1
  69. data/lib/openc3/utilities/ruby_lex_utils.rb +0 -8
  70. data/lib/openc3/version.rb +6 -6
  71. data/templates/tool_angular/package.json +14 -14
  72. data/templates/tool_angular/yarn.lock +282 -129
  73. data/templates/tool_react/package.json +11 -11
  74. data/templates/tool_react/yarn.lock +353 -300
  75. data/templates/tool_svelte/package.json +12 -12
  76. data/templates/tool_svelte/src/services/openc3-api.js +16 -60
  77. data/templates/tool_svelte/yarn.lock +338 -212
  78. data/templates/tool_vue/package.json +9 -9
  79. data/templates/tool_vue/yarn.lock +55 -41
  80. data/templates/widget/package.json +10 -10
  81. data/templates/widget/yarn.lock +59 -45
  82. metadata +36 -5
@@ -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
@@ -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 2022, OpenC3, Inc.
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
@@ -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 2022, OpenC3, Inc.
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
@@ -78,6 +78,7 @@ module OpenC3
78
78
  attr_accessor :needs_dependencies
79
79
  attr_accessor :target_microservices
80
80
  attr_accessor :children
81
+ attr_accessor :disable_erb
81
82
 
82
83
  # NOTE: The following three class methods are used by the ModelController
83
84
  # and are reimplemented to enable various Model class methods to work
@@ -349,6 +350,7 @@ module OpenC3
349
350
  target_microservices: {'REDUCER' => [[]]},
350
351
  reducer_disable: false,
351
352
  reducer_max_cpu_utilization: 30.0,
353
+ disable_erb: nil,
352
354
  scope:
353
355
  )
354
356
  super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at,
@@ -396,6 +398,7 @@ module OpenC3
396
398
  @target_microservices = target_microservices
397
399
  @reducer_disable = reducer_disable
398
400
  @reducer_max_cpu_utilization = reducer_max_cpu_utilization
401
+ @disable_erb = disable_erb
399
402
  @bucket = Bucket.getClient()
400
403
  @children = []
401
404
  end
@@ -435,7 +438,8 @@ module OpenC3
435
438
  'needs_dependencies' => @needs_dependencies,
436
439
  'target_microservices' => @target_microservices.as_json(:allow_nan => true),
437
440
  'reducer_disable' => @reducer_disable,
438
- 'reducer_max_cpu_utilization' => @reducer_max_cpu_utilization
441
+ 'reducer_max_cpu_utilization' => @reducer_max_cpu_utilization,
442
+ 'disable_erb' => @disable_erb
439
443
  }
440
444
  end
441
445
 
@@ -544,6 +548,12 @@ module OpenC3
544
548
  else
545
549
  raise ConfigParser::Error.new(parser, "PACKET cannot be used without a TARGET_MICROSERVICE")
546
550
  end
551
+ when 'DISABLE_ERB'
552
+ # 0 to unlimited parameters
553
+ @disable_erb ||= []
554
+ if parameters
555
+ @disable_erb.concat(parameters)
556
+ end
547
557
  else
548
558
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Target: #{keyword} #{parameters.join(" ")}")
549
559
  end
@@ -567,10 +577,13 @@ module OpenC3
567
577
  # Load target files
568
578
  @filename = filename # For render
569
579
  data = File.read(filename, mode: "rb")
580
+ erb_disabled = check_disable_erb(filename)
570
581
  begin
571
- OpenC3.set_working_dir(File.dirname(filename)) do
572
- if ERB_EXTENSIONS.include?(File.extname(filename).downcase) and File.basename(filename)[0] != '_'
573
- data = ERB.new(data.force_encoding("UTF-8").comment_erb(), trim_mode: "-").result(binding.set_variables(variables))
582
+ unless erb_disabled
583
+ OpenC3.set_working_dir(File.dirname(filename)) do
584
+ if ERB_EXTENSIONS.include?(File.extname(filename).downcase) and File.basename(filename)[0] != '_'
585
+ data = ERB.new(data.force_encoding("UTF-8").comment_erb(), trim_mode: "-").result(binding.set_variables(variables))
586
+ end
574
587
  end
575
588
  end
576
589
  rescue => error
@@ -679,9 +692,15 @@ module OpenC3
679
692
  path = File.join(File.dirname(@filename), template_name)
680
693
  end
681
694
 
695
+ data = File.read(path, mode: "rb")
696
+ erb_disabled = check_disable_erb(path)
682
697
  begin
683
- OpenC3.set_working_dir(File.dirname(path)) do
684
- return ERB.new(File.read(path).force_encoding("UTF-8").comment_erb(), trim_mode: "-").result(b)
698
+ if erb_disabled
699
+ return data
700
+ else
701
+ OpenC3.set_working_dir(File.dirname(path)) do
702
+ return ERB.new(data.force_encoding("UTF-8").comment_erb(), trim_mode: "-").result(b)
703
+ end
685
704
  end
686
705
  rescue => error
687
706
  raise "ERB error parsing: #{path}: #{error.formatted}"
@@ -692,14 +711,14 @@ module OpenC3
692
711
  target_files = []
693
712
  Find.find(target_folder) { |file| target_files << file }
694
713
  target_files.sort!
695
- hash = OpenC3.hash_files(target_files, nil, 'SHA256').hexdigest
696
- File.open(File.join(target_folder, 'target_id.txt'), 'wb') { |file| file.write(hash) }
714
+ @id = OpenC3.hash_files(target_files, nil, 'SHA256').hexdigest
715
+ File.open(File.join(target_folder, 'target_id.txt'), 'wb') { |file| file.write(@id) }
697
716
  key = "#{@scope}/targets/#{@name}/target_id.txt"
698
- @bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: key, body: hash)
717
+ @bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: key, body: @id)
699
718
 
700
719
  # Create target archive zip file
701
720
  prefix = File.dirname(target_folder) + '/'
702
- output_file = File.join(temp_dir, @name + '_' + hash + '.zip')
721
+ output_file = File.join(temp_dir, @name + '_' + @id + '.zip')
703
722
  Zip.continue_on_exists_proc = true
704
723
  Zip::File.open(output_file, Zip::File::CREATE) do |zipfile|
705
724
  target_files.each do |target_file|
@@ -718,7 +737,7 @@ module OpenC3
718
737
  @bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, body: file)
719
738
  end
720
739
  File.open(output_file, 'rb') do |file|
721
- bucket_key = key = "#{@scope}/target_archives/#{@name}/#{@name}_#{hash}.zip"
740
+ bucket_key = key = "#{@scope}/target_archives/#{@name}/#{@name}_#{@id}.zip"
722
741
  @bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, body: file)
723
742
  end
724
743
  end
@@ -733,7 +752,6 @@ module OpenC3
733
752
  @cmd_tlm_files = target.cmd_tlm_files
734
753
  @cmd_unique_id_mode = target.cmd_unique_id_mode
735
754
  @tlm_unique_id_mode = target.tlm_unique_id_mode
736
- @id = target.id
737
755
  @limits_groups = system.limits.groups.keys
738
756
  update()
739
757
 
@@ -897,13 +915,21 @@ module OpenC3
897
915
  Logger.info "Configured microservice #{microservice_name}"
898
916
  end
899
917
 
900
- def deploy_decom_microservice(gem_path, variables, topics, instance = nil, parent = nil)
918
+ def deploy_decom_microservice(target, gem_path, variables, topics, instance = nil, parent = nil)
901
919
  microservice_name = "#{@scope}__DECOM#{instance}__#{@name}"
920
+ # Assume Ruby initially
921
+ filename = 'decom_microservice.rb'
922
+ work_dir = '/openc3/lib/openc3/microservices'
923
+ if target.language == 'python'
924
+ filename = 'decom_microservice.py'
925
+ work_dir.sub!('openc3/lib', 'openc3/python')
926
+ parent = nil
927
+ end
902
928
  microservice = MicroserviceModel.new(
903
929
  name: microservice_name,
904
930
  folder_name: @folder_name,
905
- cmd: ["ruby", "decom_microservice.rb", microservice_name],
906
- work_dir: '/openc3/lib/openc3/microservices',
931
+ cmd: [target.language, filename, microservice_name],
932
+ work_dir: work_dir,
907
933
  topics: topics,
908
934
  target_names: [@name],
909
935
  plugin: @plugin,
@@ -963,7 +989,6 @@ module OpenC3
963
989
  name: microservice_name,
964
990
  cmd: ["ruby", "multi_microservice.rb", *@children],
965
991
  work_dir: '/openc3/lib/openc3/microservices',
966
- target_names: [@name],
967
992
  plugin: @plugin,
968
993
  scope: @scope
969
994
  )
@@ -976,9 +1001,14 @@ module OpenC3
976
1001
  def deploy_target_microservices(type, base_topic_list, topic_prefix)
977
1002
  target_microservices = @target_microservices[type]
978
1003
  if target_microservices
1004
+ # These are stand alone microservice(s) ... not part of MULTI
979
1005
  if base_topic_list
1006
+ # Only create the microservice if there are topics
1007
+ # This prevents creation of DECOM with no TLM Packets (for example)
980
1008
  deploy_count = 0
981
1009
  all_topics = base_topic_list.dup
1010
+
1011
+ # Figure out if there are individual packets assigned to this microservice
982
1012
  target_microservices.sort! {|a, b| a.length <=> b.length}
983
1013
  target_microservices.each_with_index do |packet_names, index|
984
1014
  topics = []
@@ -996,15 +1026,19 @@ module OpenC3
996
1026
  end
997
1027
  end
998
1028
  end
1029
+ # If there are any topics (packets) left over that haven't been
1030
+ # explictly handled above, spawn another microservice
999
1031
  if all_topics.length > 0
1000
1032
  instance = nil
1001
1033
  instance = deploy_count unless deploy_count == 0
1002
1034
  yield all_topics, instance, nil
1003
1035
  end
1004
1036
  else
1037
+ # Do not spawn the microservice
1005
1038
  yield nil, nil, nil
1006
1039
  end
1007
1040
  else
1041
+ # Not a stand alone microservice ... part of MULTI
1008
1042
  yield base_topic_list, nil, @parent if not base_topic_list or base_topic_list.length > 0
1009
1043
  end
1010
1044
  end
@@ -1068,11 +1102,12 @@ module OpenC3
1068
1102
 
1069
1103
  # Decommutation Microservice
1070
1104
  deploy_target_microservices('DECOM', packet_topic_list, "#{@scope}__TELEMETRY__{#{@name}}") do |topics, instance, parent|
1071
- deploy_decom_microservice(gem_path, variables, topics, instance, parent)
1105
+ deploy_decom_microservice(system.targets[@name], gem_path, variables, topics, instance, parent)
1072
1106
  end
1073
1107
 
1074
1108
  # Reducer Microservice
1075
1109
  unless @reducer_disable
1110
+ # TODO: Does Reducer even need a topic list?
1076
1111
  deploy_target_microservices('REDUCER', decom_topic_list, "#{@scope}__DECOM__{#{@name}}") do |topics, instance, parent|
1077
1112
  deploy_reducer_microservice(gem_path, variables, topics, instance, parent)
1078
1113
  end
@@ -24,6 +24,12 @@ require 'openc3/utilities/local_mode'
24
24
 
25
25
  module OpenC3
26
26
  class ToolConfigModel
27
+ def self.config_tool_names(scope: $openc3_scope)
28
+ cursor, keys = Store.scan(0, match: "#{scope}__config__*", type: 'hash', count: 100)
29
+ # Just return the tool name that is used in the other APIs
30
+ return keys.map! { |key| key.split('__')[2] }.sort
31
+ end
32
+
27
33
  def self.list_configs(tool, scope: $openc3_scope)
28
34
  Store.hkeys("#{scope}__config__#{tool}")
29
35
  end
@@ -32,14 +38,14 @@ module OpenC3
32
38
  Store.hget("#{scope}__config__#{tool}", name)
33
39
  end
34
40
 
35
- def self.save_config(tool, name, data, scope: $openc3_scope, local_mode: true)
41
+ def self.save_config(tool, name, data, local_mode: true, scope: $openc3_scope)
36
42
  Store.hset("#{scope}__config__#{tool}", name, data)
37
43
  LocalMode.save_tool_config(scope, tool, name, data) if local_mode
38
44
  end
39
45
 
40
- def self.delete_config(tool, name, scope: $openc3_scope)
46
+ def self.delete_config(tool, name, local_mode: true, scope: $openc3_scope)
41
47
  Store.hdel("#{scope}__config__#{tool}", name)
42
- LocalMode.delete_tool_config(scope, tool, name)
48
+ LocalMode.delete_tool_config(scope, tool, name) if local_mode
43
49
  end
44
50
  end
45
51
  end
@@ -39,6 +39,7 @@ module OpenC3
39
39
  attr_accessor :shown
40
40
  attr_accessor :position
41
41
  attr_accessor :needs_dependencies
42
+ attr_accessor :disable_erb
42
43
 
43
44
  # NOTE: The following three class methods are used by the ModelController
44
45
  # and are reimplemented to enable various Model class methods to work
@@ -135,6 +136,7 @@ module OpenC3
135
136
  updated_at: nil,
136
137
  plugin: nil,
137
138
  needs_dependencies: false,
139
+ disable_erb: nil,
138
140
  scope:
139
141
  )
140
142
  super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: scope)
@@ -152,6 +154,7 @@ module OpenC3
152
154
  @url = "/tools/#{folder_name}" unless @url
153
155
  end
154
156
  @needs_dependencies = needs_dependencies
157
+ @disable_erb = disable_erb
155
158
  end
156
159
 
157
160
  def create(update: false, force: false)
@@ -159,9 +162,11 @@ module OpenC3
159
162
 
160
163
  # Make sure a tool with this folder_name doesn't already exist
161
164
  unless update
162
- tools.each do |_tool_name, tool|
163
- if tool['folder_name'] == @folder_name
164
- raise "Tool with folder_name #{@folder_name} already exists at create"
165
+ if @folder_name
166
+ tools.each do |_tool_name, tool|
167
+ if tool['folder_name'] == @folder_name
168
+ raise "Tool with folder_name #{@folder_name} already exists at create"
169
+ end
165
170
  end
166
171
  end
167
172
  end
@@ -193,6 +198,7 @@ module OpenC3
193
198
  'updated_at' => @updated_at,
194
199
  'plugin' => @plugin,
195
200
  'needs_dependencies' => @needs_dependencies,
201
+ 'disable_erb' => @disable_erb
196
202
  }
197
203
  end
198
204
 
@@ -203,14 +209,14 @@ module OpenC3
203
209
  @url = parameters[0]
204
210
  when 'INLINE_URL'
205
211
  parser.verify_num_parameters(1, 1, "INLINE_URL <URL>")
206
- @inline_url = parameters[0]
212
+ @inline_url = ConfigParser.handle_nil(parameters[0])
207
213
  when 'ICON'
208
214
  parser.verify_num_parameters(1, 1, "ICON <ICON Name>")
209
215
  @icon = parameters[0]
210
216
  when 'WINDOW'
211
- parser.verify_num_parameters(1, 1, "WINDOW <INLINE | IFRAME | NEW>")
217
+ parser.verify_num_parameters(1, 1, "WINDOW <INLINE | IFRAME | SAME | NEW>")
212
218
  @window = parameters[0].to_s.upcase
213
- raise ConfigParser::Error.new(parser, "Invalid WINDOW setting: #{@window}") unless ['INLINE', 'IFRAME', 'NEW'].include?(@window)
219
+ raise ConfigParser::Error.new(parser, "Invalid WINDOW setting: #{@window}") unless ['INLINE', 'IFRAME', 'SAME', 'NEW'].include?(@window)
214
220
  when 'CATEGORY'
215
221
  parser.verify_num_parameters(1, 1, "CATEGORY <Category Name>")
216
222
  @category = parameters[0].to_s
@@ -220,6 +226,12 @@ module OpenC3
220
226
  when 'POSITION'
221
227
  parser.verify_num_parameters(1, 1, "POSITION <value>")
222
228
  @position = parameters[0].to_i
229
+ when 'DISABLE_ERB'
230
+ # 0 to unlimited parameters
231
+ @disable_erb ||= []
232
+ if parameters
233
+ @disable_erb.concat(parameters)
234
+ end
223
235
  else
224
236
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Tool: #{keyword} #{parameters.join(" ")}")
225
237
  end
@@ -240,7 +252,10 @@ module OpenC3
240
252
 
241
253
  # Load tool files
242
254
  data = File.read(filename, mode: "rb")
243
- data = ERB.new(data.comment_erb(), trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
255
+ erb_disabled = check_disable_erb(filename)
256
+ unless erb_disabled
257
+ data = ERB.new(data.comment_erb(), trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
258
+ end
244
259
  unless validate_only
245
260
  client = Bucket.getClient()
246
261
  cache_control = BucketUtilities.get_cache_control(filename)
@@ -35,6 +35,7 @@ module OpenC3
35
35
  attr_accessor :filename
36
36
  attr_accessor :bucket_key
37
37
  attr_accessor :needs_dependencies
38
+ attr_accessor :disable_erb
38
39
 
39
40
  # NOTE: The following three class methods are used by the ModelController
40
41
  # and are reimplemented to enable various Model class methods to work
@@ -87,6 +88,7 @@ module OpenC3
87
88
  plugin: nil,
88
89
  label: nil,
89
90
  needs_dependencies: false,
91
+ disable_erb: nil,
90
92
  scope:
91
93
  )
92
94
  super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: scope)
@@ -95,6 +97,7 @@ module OpenC3
95
97
  @bucket_key = 'widgets/' + @full_name + '/' + @filename
96
98
  @label = label
97
99
  @needs_dependencies = needs_dependencies
100
+ @disable_erb = disable_erb
98
101
  end
99
102
 
100
103
  def as_json(*a)
@@ -104,11 +107,21 @@ module OpenC3
104
107
  'plugin' => @plugin,
105
108
  'label' => @label,
106
109
  'needs_dependencies' => @needs_dependencies,
110
+ 'disable_erb' => @disable_erb
107
111
  }
108
112
  end
109
113
 
110
114
  def handle_config(parser, keyword, parameters)
111
- raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Widget: #{keyword} #{parameters.join(" ")}")
115
+ case keyword
116
+ when 'DISABLE_ERB'
117
+ # 0 to unlimited parameters
118
+ @disable_erb ||= []
119
+ if parameters
120
+ @disable_erb.concat(parameters)
121
+ end
122
+ else
123
+ raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Widget: #{keyword} #{parameters.join(" ")}")
124
+ end
112
125
  end
113
126
 
114
127
  def deploy(gem_path, variables, validate_only: false)
@@ -123,8 +136,11 @@ module OpenC3
123
136
 
124
137
  # Load widget file
125
138
  data = File.read(filename, mode: "rb")
126
- OpenC3.set_working_dir(File.dirname(filename)) do
127
- data = ERB.new(data.comment_erb(), trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
139
+ erb_disabled = check_disable_erb(filename)
140
+ unless erb_disabled
141
+ OpenC3.set_working_dir(File.dirname(filename)) do
142
+ data = ERB.new(data.comment_erb(), trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
143
+ end
128
144
  end
129
145
  unless validate_only
130
146
  cache_control = BucketUtilities.get_cache_control(@filename)
@@ -50,8 +50,10 @@ module OpenC3
50
50
  env = microservice_config["env"].dup
51
51
  if microservice_config["needs_dependencies"]
52
52
  env['GEM_HOME'] = '/gems'
53
+ env['PYTHONUSERBASE'] = '/gems/python_packages'
53
54
  else
54
55
  env['GEM_HOME'] = nil
56
+ env['PYTHONUSERBASE'] = nil
55
57
  end
56
58
  env['OPENC3_MICROSERVICE_NAME'] = microservice_name
57
59
  container = microservice_config["container"]