openc3 5.0.6 → 5.0.9

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.

@@ -40,10 +40,47 @@ module OpenC3
40
40
  super(PRIMARY_KEY)
41
41
  end
42
42
 
43
+ def self.from_json(json, scope: nil)
44
+ json = JSON.parse(json, :allow_nan => true, :create_additions => true) if String === json
45
+ raise "json data is nil" if json.nil?
46
+
47
+ json.transform_keys!(&:to_sym)
48
+ self.new(**json, scope: scope)
49
+ end
50
+
51
+ def self.get_model(name:, scope: nil)
52
+ json = get(name: name)
53
+ if json
54
+ return from_json(json)
55
+ else
56
+ return nil
57
+ end
58
+ end
59
+
43
60
  def initialize(name:, updated_at: nil, scope: nil)
44
61
  super(PRIMARY_KEY, name: name, scope: name, updated_at: updated_at)
45
62
  end
46
63
 
64
+ def create(update: false, force: false)
65
+ # Ensure there are no "." in the scope name - prevents gems accidently becoming scope names
66
+ raise "Invalid scope name: #{@name}" if @name !~ /^[a-zA-Z0-9_-]+$/
67
+ @name = @name.upcase
68
+ super(update: update, force: force)
69
+ end
70
+
71
+ def destroy
72
+ if @name != 'DEFAULT'
73
+ # Remove all the plugins for this scope
74
+ plugins = PluginModel.get_all_models(scope: @name)
75
+ plugins.each do |plugin_name, plugin|
76
+ plugin.destroy
77
+ end
78
+ super()
79
+ else
80
+ raise "DEFAULT scope cannot be destroyed"
81
+ end
82
+ end
83
+
47
84
  def as_json(*a)
48
85
  { 'name' => @name,
49
86
  'updated_at' => @updated_at }
@@ -51,9 +88,12 @@ module OpenC3
51
88
 
52
89
  def deploy(gem_path, variables)
53
90
  seed_database()
54
-
55
91
  ConfigTopic.initialize_stream(@scope)
56
92
 
93
+ # Create UNKNOWN target for display of unknown data
94
+ model = TargetModel.new(name: "UNKNOWN", scope: @scope)
95
+ model.create
96
+
57
97
  # OpenC3 Log Microservice
58
98
  microservice_name = "#{@scope}__OPENC3__LOG"
59
99
  microservice = MicroserviceModel.new(
@@ -24,6 +24,7 @@ require 'openc3/models/microservice_model'
24
24
  require 'openc3/topics/limits_event_topic'
25
25
  require 'openc3/topics/config_topic'
26
26
  require 'openc3/system'
27
+ require 'openc3/utilities/local_mode'
27
28
  require 'openc3/utilities/s3'
28
29
  require 'openc3/utilities/zip'
29
30
  require 'fileutils'
@@ -82,13 +83,149 @@ module OpenC3
82
83
  super("#{scope}__#{PRIMARY_KEY}")
83
84
  end
84
85
 
86
+ # All targets with indication of modified targets
87
+ def self.all_modified(scope:)
88
+ targets = self.all(scope: scope)
89
+ targets.each { |target_name, target| target['modified'] = false }
90
+
91
+ if ENV['OPENC3_LOCAL_MODE']
92
+ modified_targets = OpenC3::LocalMode.modified_targets(scope: scope)
93
+ modified_targets.each do |target_name|
94
+ targets[target_name]['modified'] = true if targets[target_name]
95
+ end
96
+ else
97
+ rubys3_client = Aws::S3::Client.new
98
+ token = nil
99
+ while true
100
+ resp = rubys3_client.list_objects_v2({
101
+ bucket: 'config',
102
+ max_keys: 1000,
103
+ # The trailing slash is important!
104
+ prefix: "#{scope}/targets_modified/",
105
+ delimiter: '/',
106
+ continuation_token: token
107
+ })
108
+ resp.common_prefixes.each do |item|
109
+ # Results look like DEFAULT/targets_modified/INST/
110
+ # so split on '/' and pull out the last value
111
+ target_name = item.prefix.split('/')[-1]
112
+ # A target could have been deleted without removing the modified files
113
+ # Thus we have to check for the existance of the target_name key
114
+ if targets.has_key?(target_name)
115
+ targets[target_name]['modified'] = true
116
+ end
117
+ end
118
+ break unless resp.is_truncated
119
+ token = resp.next_continuation_token
120
+ end
121
+ end
122
+ # Sort (which turns hash to array) and return hash
123
+ # This enables a consistent listing of the targets
124
+ targets.sort.to_h
125
+ end
126
+
127
+ # Given target's modified file list
128
+ def self.modified_files(target_name, scope:)
129
+ modified = []
130
+
131
+ if ENV['OPENC3_LOCAL_MODE']
132
+ modified = OpenC3::LocalMode.modified_files(target_name, scope: scope)
133
+ else
134
+ rubys3_client = Aws::S3::Client.new
135
+ token = nil
136
+ while true
137
+ resp = rubys3_client.list_objects_v2({
138
+ bucket: 'config',
139
+ max_keys: 1000,
140
+ # The trailing slash is important!
141
+ prefix: "#{scope}/targets_modified/#{target_name}/",
142
+ continuation_token: token
143
+ })
144
+ resp.contents.each do |item|
145
+ # Results look like DEFAULT/targets_modified/INST/procedures/new.rb
146
+ # so split on '/' and ignore the first two values
147
+ modified << item.key.split('/')[2..-1].join('/')
148
+ end
149
+ break unless resp.is_truncated
150
+ token = resp.next_continuation_token
151
+ end
152
+ end
153
+ # Sort to enable a consistent listing of the modified files
154
+ modified.sort
155
+ end
156
+
157
+ def self.delete_modified(target_name, scope:)
158
+ if ENV['OPENC3_LOCAL_MODE']
159
+ OpenC3::LocalMode.delete_modified(target_name, scope: scope)
160
+ end
161
+
162
+ # Delete the remote files as well
163
+ rubys3_client = Aws::S3::Client.new
164
+ token = nil
165
+ while true
166
+ resp = rubys3_client.list_objects_v2({
167
+ bucket: 'config',
168
+ max_keys: 1000,
169
+ # The trailing slash is important!
170
+ prefix: "#{scope}/targets_modified/#{target_name}/",
171
+ continuation_token: token
172
+ })
173
+ resp.contents.each do |item|
174
+ rubys3_client.delete_object(bucket: 'config', key: item.key)
175
+ end
176
+ break unless resp.is_truncated
177
+ token = resp.next_continuation_token
178
+ end
179
+ end
180
+
181
+ def self.download(target_name, scope:)
182
+ tmp_dir = Dir.mktmpdir
183
+ zip_filename = File.join(tmp_dir, "#{target_name}.zip")
184
+ Zip.continue_on_exists_proc = true
185
+ zip = Zip::File.open(zip_filename, Zip::File::CREATE)
186
+
187
+ if ENV['OPENC3_LOCAL_MODE']
188
+ OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
189
+ else
190
+ rubys3_client = Aws::S3::Client.new
191
+ token = nil
192
+ # The trailing slash is important!
193
+ prefix = "#{scope}/targets_modified/#{target_name}/"
194
+ while true
195
+ resp = rubys3_client.list_objects_v2({
196
+ bucket: 'config',
197
+ max_keys: 1000,
198
+ prefix: prefix,
199
+ continuation_token: token
200
+ })
201
+ resp.contents.each do |item|
202
+ # item.key looks like DEFAULT/targets_modified/INST/screens/blah.txt
203
+ base_path = item.key.sub(prefix, '') # remove prefix
204
+ local_path = File.join(tmp_dir, base_path)
205
+ # Ensure dir structure exists, get_object fails if not
206
+ FileUtils.mkdir_p(File.dirname(local_path))
207
+ rubys3_client.get_object(bucket: 'config', key: item.key, response_target: local_path)
208
+ zip.add(base_path, local_path)
209
+ end
210
+ break unless resp.is_truncated
211
+ token = resp.next_continuation_token
212
+ end
213
+ end
214
+ zip.close
215
+
216
+ result = OpenStruct.new
217
+ result.filename = File.basename(zip_filename)
218
+ result.contents = File.read(zip_filename, mode: 'rb')
219
+ return result
220
+ end
221
+
85
222
  # @return [Array] Array of all the packet names
86
223
  def self.packet_names(target_name, type: :TLM, scope:)
87
224
  raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
88
225
  # If the key doesn't exist or if there are no packets we return empty array
89
226
  Store.hkeys("#{scope}__openc3#{type.to_s.downcase}__#{target_name}").sort
90
227
  end
91
-
228
+
92
229
  # @return [Hash] Packet hash or raises an exception
93
230
  def self.packet(target_name, packet_name, type: :TLM, scope:)
94
231
  raise "Unknown type #{type} for #{target_name} #{packet_name}" unless VALID_TYPES.include?(type)
@@ -420,9 +557,6 @@ module OpenC3
420
557
  end
421
558
 
422
559
  def undeploy
423
- # Note: The plugin_model undeploy method removes all the microservices first
424
- # so we don't need to destroy them here
425
-
426
560
  rubys3_client = Aws::S3::Client.new
427
561
  prefix = "#{@scope}/targets/#{@name}/"
428
562
  rubys3_client.list_objects(bucket: 'config', prefix: prefix).contents.each do |object|
@@ -203,6 +203,9 @@ module OpenC3
203
203
  when 'SHOWN'
204
204
  parser.verify_num_parameters(1, 1, "SHOWN <true/false>")
205
205
  @shown = ConfigParser.handle_true_false(parameters[0])
206
+ when 'POSITION'
207
+ parser.verify_num_parameters(1, 1, "POSITION <value>")
208
+ @position = parameters[0].to_i
206
209
  else
207
210
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Tool: #{keyword} #{parameters.join(" ")}")
208
211
  end
@@ -90,7 +90,7 @@ module OpenC3
90
90
  @new_microservices.each do |microservice_name, microservice_config|
91
91
  cmd_array, work_dir, env, scope, container = convert_microservice_to_process_definition(microservice_name, microservice_config)
92
92
  if cmd_array
93
- process = OperatorProcess.new(cmd_array, work_dir: work_dir, env: env, scope: scope, container: container)
93
+ process = OperatorProcess.new(cmd_array, work_dir: work_dir, env: env, scope: scope, container: container, config: microservice_config)
94
94
  @new_processes[microservice_name] = process
95
95
  @processes[microservice_name] = process
96
96
  end
@@ -108,7 +108,7 @@ module OpenC3
108
108
  @changed_processes[microservice_name] = process
109
109
  else # TODO: How is this even possible?
110
110
  Logger.error("Changed microservice #{microservice_name} does not exist. Creating new...", scope: scope)
111
- process = OperatorProcess.new(cmd_array, work_dir: work_dir, env: env, scope: scope, container: container)
111
+ process = OperatorProcess.new(cmd_array, work_dir: work_dir, env: env, scope: scope, container: container, config: microservice_config)
112
112
  @new_processes[microservice_name] = process
113
113
  @processes[microservice_name] = process
114
114
  end
@@ -35,7 +35,8 @@ module OpenC3
35
35
  # Perform any setup steps necessary
36
36
  end
37
37
 
38
- def initialize(process_definition, work_dir: '/openc3/lib/openc3/microservices', temp_dir: nil, env: {}, scope:, container: nil) # container is not used, it's just here for Enterprise
38
+ # container is not used, it's just here for Enterprise
39
+ def initialize(process_definition, work_dir: '/openc3/lib/openc3/microservices', temp_dir: nil, env: {}, scope:, container: nil, config: nil)
39
40
  @process = nil
40
41
  @process_definition = process_definition
41
42
  @work_dir = work_dir
@@ -43,12 +44,26 @@ module OpenC3
43
44
  @new_temp_dir = temp_dir
44
45
  @env = env
45
46
  @scope = scope
47
+ # @config only used in start to help print a better Logger message
48
+ @config = config
46
49
  end
47
50
 
48
51
  def start
49
52
  @temp_dir = @new_temp_dir
50
53
  @new_temp_dir = nil
51
- Logger.info("Starting: #{@process_definition.join(' ')}", scope: @scope)
54
+
55
+ # In ProcessManager processes, the process_definition is the actual thing run
56
+ # e.g. OpenC3::ProcessManager.instance.spawn(["ruby", "/openc3/bin/openc3cli", "load", ...])
57
+ # However, if the MicroserviceOperator is spawning the proceses it sets
58
+ # process_definition = ["ruby", "plugin_microservice.rb"]
59
+ # which then calls exec(*@config["cmd"]) to actually run
60
+ # So check if the @config['cmd'] is defined to give the user more info in the log
61
+ cmd = @process_definition.join(' ')
62
+ if @config && @config['cmd']
63
+ cmd = @config['cmd'].join(' ')
64
+ end
65
+ Logger.info("Starting: #{cmd}", scope: @scope)
66
+
52
67
  @process = ChildProcess.build(*@process_definition)
53
68
  # This lets the ChildProcess use the parent IO ... but it breaks unit tests
54
69
  # @process.io.inherit!
@@ -178,14 +178,40 @@ module OpenC3
178
178
  when 'ParameterTypeSet', 'EnumerationList', 'ParameterSet', 'ContainerSet',
179
179
  'EntryList', 'DefaultCalibrator', 'DefaultAlarm', 'RestrictionCriteria',
180
180
  'ComparisonList', 'MetaCommandSet', 'ArgumentTypeSet', 'ArgumentList',
181
- 'ArgumentAssignmentList', 'LocationInContainerInBits'
181
+ 'ArgumentAssignmentList', 'LocationInContainerInBits', 'ReferenceTime'
182
182
  # Do Nothing
183
183
 
184
+ when 'ErrorDetectCorrect'
185
+ # TODO: Setup an algorithm to calculate the CRC
186
+ exponents = []
187
+ xtce_recurse_element(element) do |crc_element|
188
+ if crc_element.name == 'CRC'
189
+ xtce_recurse_element(crc_element) do |poly_element|
190
+ if poly_element['Polynomial']
191
+ xtce_recurse_element(poly_element) do |term_element|
192
+ if term_element['Term']
193
+ exponents << term_element['exponent'].to_i
194
+ end
195
+ true
196
+ end
197
+ end
198
+ true
199
+ end
200
+ end
201
+ true
202
+ end
203
+ @current_type.xtce_encoding = 'IntegerDataEncoding'
204
+ @current_type.signed = 'false'
205
+ return false # Already recursed
206
+
184
207
  when 'EnumeratedParameterType', 'EnumeratedArgumentType',
185
208
  'IntegerParameterType', 'IntegerArgumentType',
186
209
  'FloatParameterType', 'FloatArgumentType',
187
210
  'StringParameterType', 'StringArgumentType',
188
- 'BinaryParameterType', 'BinaryArgumentType'
211
+ 'BinaryParameterType', 'BinaryArgumentType',
212
+ 'BooleanParameterType', 'BooleanArgumentType',
213
+ 'AbsoluteTimeParameterType', 'AbsoluteTimeArgumentType',
214
+ 'RelativeTimeParameterType', 'RelativeTimeArgumentType'
189
215
  @current_type = create_new_type(element)
190
216
  @current_type.endianness = :BIG_ENDIAN
191
217
 
@@ -204,6 +230,20 @@ module OpenC3
204
230
  when 'BinaryParameterType', 'BinaryArgumentType'
205
231
  @current_type.xtce_encoding = 'BinaryDataEncoding'
206
232
  @current_type.sizeInBits = 8 # This is undocumented but appears to be the design
233
+ when 'BooleanParameterType', 'BooleanArgumentType'
234
+ @current_type.xtce_encoding = 'StringDataEncoding'
235
+ @current_type.sizeInBits = 8 # This is undocumented but appears to be the design
236
+ @current_type.states ||= {}
237
+ if element.attributes['zeroStringValue'] && element.attributes['oneStringValue']
238
+ @current_type.states[element.attributes['zeroStringValue'].value] = 0
239
+ @current_type.states[element.attributes['oneStringValue'].value] = 1
240
+ else
241
+ @current_type.states['FALSE'] = 0
242
+ @current_type.states['TRUE'] = 1
243
+ end
244
+ # No defaults for the time types
245
+ # when 'AbsoluteTimeParameterType', 'AbsoluteTimeArgumentType',
246
+ # when'RelativeTimeParameterType', 'RelativeTimeArgumentType'
207
247
  end
208
248
 
209
249
  when 'ArrayParameterType', 'ArrayArgumentType'
@@ -251,7 +291,7 @@ module OpenC3
251
291
 
252
292
  return false # Already recursed
253
293
 
254
- when "SizeInBits"
294
+ when 'SizeInBits'
255
295
  xtce_recurse_element(element) do |block_element|
256
296
  if block_element.name == 'FixedValue'
257
297
  @current_type.sizeInBits = Integer(block_element.text)
@@ -326,6 +366,26 @@ module OpenC3
326
366
  @current_type.states ||= {}
327
367
  @current_type.states[element['label']] = Integer(element['value'])
328
368
 
369
+ when 'Encoding'
370
+ if element.attributes['units']
371
+ @current_type.units_full = element.attributes['units'].value
372
+ # Could do a better job mapping units to abbreviations
373
+ @current_type.units = element.attributes['units'].value[0]
374
+ end
375
+ # TODO: Not sure if this is correct
376
+ # if @current_type.attributes['scale'] || @current_type.attributes['offset']
377
+ # @current_type.conversion ||= PolynomialConversion.new()
378
+ # if @current_type.attributes['offset']
379
+ # @current_type.conversion.coeffs[0] = @current_type.attributes['offset']
380
+ # end
381
+ # if @current_type.attributes['scale']
382
+ # @current_type.conversion.coeffs[1] = @current_type.attributes['scale']
383
+ # end
384
+ # end
385
+
386
+ when 'Epoch'
387
+ # TODO: How to handle this ... it affects the conversion applied
388
+
329
389
  when 'IntegerDataEncoding', 'FloatDataEncoding', 'StringDataEncoding', 'BinaryDataEncoding'
330
390
  @current_type.xtce_encoding = element.name
331
391
  element.attributes.each do |att_name, att|
@@ -340,6 +400,11 @@ module OpenC3
340
400
  @current_type.endianness = :LITTLE_ENDIAN
341
401
  end
342
402
 
403
+ # Ensure if the encoding is a string we convert state values to strings
404
+ if @current_type.xtce_encoding == 'StringDataEncoding' && @current_type.states
405
+ @current_type.states.transform_values!(&:to_s)
406
+ end
407
+
343
408
  when 'Parameter'
344
409
  @current_parameter = OpenStruct.new
345
410
  element.attributes.each do |att_name, att|
@@ -34,7 +34,7 @@ module OpenC3
34
34
  OpenC3::Logger.info "Deleting #{delete_path}"
35
35
  response = $api_server.request('delete', endpoint, query: {bucket: 'config'}, scope: scope)
36
36
  if response.nil? || response.code != 200
37
- raise "Failed to delete #{delete_path}. Note: #{scope}/targets is read-only."
37
+ raise "Failed to delete #{delete_path}"
38
38
  end
39
39
  rescue => error
40
40
  raise "Failed deleting #{path} due to #{error.message}"
@@ -48,7 +48,13 @@ module OpenC3
48
48
  # @param io_or_string [Io or String] IO object
49
49
  def put_target_file(path, io_or_string, scope: $openc3_scope)
50
50
  raise "Disallowed path modifier '..' found in #{path}" if path.include?('..')
51
+
51
52
  upload_path = "#{scope}/targets_modified/#{path}"
53
+
54
+ if ENV['OPENC3_LOCAL_MODE'] and $openc3_in_cluster
55
+ OpenC3::LocalMode.put_target_file(upload_path, io_or_string, scope: scope)
56
+ end
57
+
52
58
  endpoint = "/openc3-api/storage/upload/#{upload_path}"
53
59
  OpenC3::Logger.info "Writing #{upload_path}"
54
60
  result = _get_presigned_request(endpoint, scope: scope)
@@ -86,6 +92,17 @@ module OpenC3
86
92
  # Loop to allow redo when switching from modified to original
87
93
  loop do
88
94
  begin
95
+ if part == "targets_modified" and ENV['OPENC3_LOCAL_MODE']
96
+ local_file = OpenC3::LocalMode.open_local_file(path, scope: scope)
97
+ if local_file
98
+ file = Tempfile.new('target', binmode: true)
99
+ file.write(local_file.read)
100
+ local_file.close
101
+ file.rewind
102
+ return file if local_file
103
+ end
104
+ end
105
+
89
106
  return _get_storage_file("#{part}/#{path}", scope: scope)
90
107
  rescue => error
91
108
  if part == "targets_modified"