openc3 6.9.0 → 6.9.2

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.
@@ -56,6 +56,13 @@ module OpenC3
56
56
  attr_accessor :plugin_txt_lines
57
57
  attr_accessor :needs_dependencies
58
58
  attr_accessor :store_id
59
+ attr_accessor :title
60
+ attr_accessor :description
61
+ attr_accessor :licenses
62
+ attr_accessor :homepage
63
+ attr_accessor :repository
64
+ attr_accessor :keywords
65
+ attr_accessor :img_path
59
66
 
60
67
  # NOTE: The following three class methods are used by the ModelController
61
68
  # and are reimplemented to enable various Model class methods to work
@@ -179,11 +186,31 @@ module OpenC3
179
186
  raise "Invalid screen filename: #{filename}. Screen filenames must be lowercase."
180
187
  end
181
188
  end
189
+
190
+ # Process app store metadata
191
+ plugin_model.title = pkg.spec.metadata['openc3_store_title'] || pkg.spec.summary.strip
192
+ plugin_model.description = pkg.spec.metadata['openc3_store_description'] || pkg.spec.description.strip
193
+ plugin_model.licenses = pkg.spec.licenses
194
+ plugin_model.homepage = pkg.spec.homepage
195
+ plugin_model.repository = pkg.spec.metadata['source_code_uri'] # this key because it's in the official gemspec examples
196
+ plugin_model.keywords = pkg.spec.metadata['openc3_store_keywords']&.split(/, ?/)
197
+ img_path = pkg.spec.metadata['openc3_store_image']
198
+ unless img_path
199
+ default_img_path = 'public/store_img.png'
200
+ full_default_path = File.join(gem_path, default_img_path)
201
+ img_path = default_img_path if File.exist? full_default_path
202
+ end
203
+ plugin_model.img_path = File.join('gems', gem_name.split(".gem")[0], img_path) if img_path # convert this filesystem path to volumes mount path
204
+ plugin_model.update() unless validate_only
205
+
182
206
  needs_dependencies = pkg.spec.runtime_dependencies.length > 0
183
207
  needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
184
208
 
185
- # Handle python requirements.txt
186
- if File.exist?(File.join(gem_path, 'requirements.txt'))
209
+ # Handle python dependencies (pyproject.toml or requirements.txt)
210
+ pyproject_path = File.join(gem_path, 'pyproject.toml')
211
+ requirements_path = File.join(gem_path, 'requirements.txt')
212
+
213
+ if File.exist?(pyproject_path) || File.exist?(requirements_path)
187
214
  begin
188
215
  pypi_url = get_setting('pypi_url', scope: scope)
189
216
  if pypi_url
@@ -202,11 +229,20 @@ module OpenC3
202
229
  end
203
230
  end
204
231
  unless validate_only
205
- Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
206
- if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
207
- pip_args = "--no-warn-script-location -i #{pypi_url} -r #{File.join(gem_path, 'requirements.txt')}"
232
+ if File.exist?(pyproject_path)
233
+ Logger.info "Installing python packages from pyproject.toml with pypi_url=#{pypi_url}"
234
+ if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
235
+ pip_args = "--no-warn-script-location -i #{pypi_url} #{gem_path}"
236
+ else
237
+ pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} #{gem_path}"
238
+ end
208
239
  else
209
- pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} -r #{File.join(gem_path, 'requirements.txt')}"
240
+ Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
241
+ if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
242
+ pip_args = "--no-warn-script-location -i #{pypi_url} -r #{requirements_path}"
243
+ else
244
+ pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} -r #{requirements_path}"
245
+ end
210
246
  end
211
247
  puts `/openc3/bin/pipinstall #{pip_args}`
212
248
  end
@@ -302,6 +338,13 @@ module OpenC3
302
338
  plugin_txt_lines: [],
303
339
  needs_dependencies: false,
304
340
  store_id: nil,
341
+ title: nil,
342
+ description: nil,
343
+ keywords: nil,
344
+ licenses: nil,
345
+ homepage: nil,
346
+ repository: nil,
347
+ img_path: nil,
305
348
  updated_at: nil,
306
349
  scope:
307
350
  )
@@ -310,6 +353,13 @@ module OpenC3
310
353
  @plugin_txt_lines = plugin_txt_lines
311
354
  @needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
312
355
  @store_id = store_id
356
+ @title = title
357
+ @description = description
358
+ @keywords = keywords
359
+ @licenses = licenses
360
+ @homepage = homepage
361
+ @repository = repository
362
+ @img_path = img_path
313
363
  end
314
364
 
315
365
  def create(update: false, force: false, queued: false)
@@ -329,6 +379,13 @@ module OpenC3
329
379
  'plugin_txt_lines' => @plugin_txt_lines,
330
380
  'needs_dependencies' => @needs_dependencies,
331
381
  'store_id' => @store_id,
382
+ 'title' => @title,
383
+ 'description' => @description,
384
+ 'keywords' => @keywords,
385
+ 'licenses' => @licenses,
386
+ 'homepage' => @homepage,
387
+ 'repository' => @repository,
388
+ 'img_path' => @img_path,
332
389
  'updated_at' => @updated_at
333
390
  }
334
391
  end
@@ -51,11 +51,11 @@ module OpenC3
51
51
  if model.state != 'DISABLE'
52
52
  result = Store.zrevrange("#{scope}:#{name}", 0, 0, with_scores: true)
53
53
  if result.empty?
54
- index = 1.0
54
+ id = 1.0
55
55
  else
56
- index = result[0][1].to_f + 1
56
+ id = result[0][1].to_f + 1
57
57
  end
58
- Store.zadd("#{scope}:#{name}", index, { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }.to_json)
58
+ Store.zadd("#{scope}:#{name}", id, { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }.to_json)
59
59
  model.notify(kind: 'command')
60
60
  else
61
61
  raise QueueError, "Queue '#{name}' is disabled. Command '#{command}' not queued."
@@ -103,55 +103,55 @@ module OpenC3
103
103
  QueueTopic.write_notification(notification, scope: @scope)
104
104
  end
105
105
 
106
- def insert_command(index, command_data)
106
+ def insert_command(id, command_data)
107
107
  if @state == 'DISABLE'
108
108
  raise QueueError, "Queue '#{@name}' is disabled. Command '#{command_data['value']}' not queued."
109
109
  end
110
110
 
111
- unless index
111
+ unless id
112
112
  result = Store.zrevrange("#{@scope}:#{@name}", 0, 0, with_scores: true)
113
113
  if result.empty?
114
- index = 1.0
114
+ id = 1.0
115
115
  else
116
- index = result[0][1].to_f + 1
116
+ id = result[0][1].to_f + 1
117
117
  end
118
118
  end
119
- Store.zadd("#{@scope}:#{@name}", index, command_data.to_json)
119
+ Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
120
120
  notify(kind: 'command')
121
121
  end
122
122
 
123
- def update_command(index:, command:, username:)
123
+ def update_command(id:, command:, username:)
124
124
  if @state == 'DISABLE'
125
- raise QueueError, "Queue '#{@name}' is disabled. Command at index #{index} not updated."
125
+ raise QueueError, "Queue '#{@name}' is disabled. Command at id #{id} not updated."
126
126
  end
127
127
 
128
- # Check if command exists at the given index
129
- existing = Store.zrangebyscore("#{@scope}:#{@name}", index, index)
128
+ # Check if command exists at the given id
129
+ existing = Store.zrangebyscore("#{@scope}:#{@name}", id, id)
130
130
  if existing.empty?
131
- raise QueueError, "No command found at index #{index} in queue '#{@name}'"
131
+ raise QueueError, "No command found at id #{id} in queue '#{@name}'"
132
132
  end
133
133
 
134
- # Remove the existing command and add the new one at the same index
135
- Store.zremrangebyscore("#{@scope}:#{@name}", index, index)
134
+ # Remove the existing command and add the new one at the same id
135
+ Store.zremrangebyscore("#{@scope}:#{@name}", id, id)
136
136
  command_data = { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }
137
- Store.zadd("#{@scope}:#{@name}", index, command_data.to_json)
137
+ Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
138
138
  notify(kind: 'command')
139
139
  end
140
140
 
141
- def remove_command(index = nil)
141
+ def remove_command(id = nil)
142
142
  if @state == 'DISABLE'
143
143
  raise QueueError, "Queue '#{@name}' is disabled. Command not removed."
144
144
  end
145
145
 
146
- if index
147
- # Remove specific index
148
- result = Store.zrangebyscore("#{@scope}:#{@name}", index, index)
146
+ if id
147
+ # Remove specific id
148
+ result = Store.zrangebyscore("#{@scope}:#{@name}", id, id)
149
149
  if result.empty?
150
150
  return nil
151
151
  else
152
- Store.zremrangebyscore("#{@scope}:#{@name}", index, index)
152
+ Store.zremrangebyscore("#{@scope}:#{@name}", id, id)
153
153
  command_data = JSON.parse(result[0])
154
- command_data['index'] = index.to_f
154
+ command_data['id'] = id.to_f
155
155
  notify(kind: 'command')
156
156
  return command_data
157
157
  end
@@ -164,7 +164,7 @@ module OpenC3
164
164
  score = result[0][1]
165
165
  Store.zremrangebyscore("#{@scope}:#{@name}", score, score)
166
166
  command_data = JSON.parse(result[0][0])
167
- command_data['index'] = score.to_f
167
+ command_data['id'] = score.to_f
168
168
  notify(kind: 'command')
169
169
  return command_data
170
170
  end
@@ -174,7 +174,7 @@ module OpenC3
174
174
  def list
175
175
  return Store.zrange("#{@scope}:#{@name}", 0, -1, with_scores: true).map do |item|
176
176
  result = JSON.parse(item[0])
177
- result['index'] = item[1].to_f
177
+ result['id'] = item[1].to_f
178
178
  result
179
179
  end
180
180
  end
@@ -143,7 +143,7 @@ module OpenC3
143
143
  unless triggers.is_a?(Array)
144
144
  raise ReactionInputError.new "invalid triggers, must be array of hashes: #{triggers}"
145
145
  end
146
- trigger_hash = Hash.new()
146
+ trigger_hash = {}
147
147
  triggers.each do | trigger |
148
148
  unless trigger.is_a?(Hash)
149
149
  raise ReactionInputError.new "invalid trigger, must be hash: #{trigger}"
@@ -166,10 +166,14 @@ module OpenC3
166
166
  end
167
167
 
168
168
  def self.download(target_name, scope:)
169
+ # Validate target_name to not allow directory traversal
170
+ if target_name.include?('..') || target_name.include?('/') || target_name.include?('\\')
171
+ raise ArgumentError, "Invalid target_name: #{target_name.inspect}"
172
+ end
169
173
  tmp_dir = Dir.mktmpdir
170
174
  zip_filename = File.join(tmp_dir, "#{target_name}.zip")
171
175
  Zip.continue_on_exists_proc = true
172
- zip = Zip::File.open(zip_filename, Zip::File::CREATE)
176
+ zip = Zip::File.open(zip_filename, create: true)
173
177
 
174
178
  if ENV['OPENC3_LOCAL_MODE']
175
179
  OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
@@ -756,7 +760,7 @@ module OpenC3
756
760
  prefix = File.dirname(target_folder) + '/'
757
761
  output_file = File.join(temp_dir, @name + '_' + @id + '.zip')
758
762
  Zip.continue_on_exists_proc = true
759
- Zip::File.open(output_file, Zip::File::CREATE) do |zipfile|
763
+ Zip::File.open(output_file, create: true) do |zipfile|
760
764
  target_files.each do |target_file|
761
765
  zip_file_path = target_file.delete_prefix(prefix)
762
766
  if File.directory?(target_file)
@@ -807,7 +811,7 @@ module OpenC3
807
811
  Logger.error("Invalid text present in #{target_name} #{packet_name} tlm packet")
808
812
  raise e
809
813
  end
810
- json_hash = Hash.new
814
+ json_hash = {}
811
815
  packet.sorted_items.each do |item|
812
816
  json_hash[item.name] = nil
813
817
  TargetModel.add_to_target_allitems_list(target_name, item.name, scope: @scope)
@@ -237,7 +237,7 @@ module OpenC3
237
237
 
238
238
  # ["#{@scope}__DECOM__{#{@target}}__#{@packet}"]
239
239
  def generate_topics
240
- topics = Hash.new
240
+ topics = {}
241
241
  if @left['type'] == ITEM_TYPE
242
242
  topics["#{@scope}__DECOM__{#{left['target']}}__#{left['packet']}"] = 1
243
243
  end
@@ -31,7 +31,7 @@ module OpenC3
31
31
  raise "Failed to #{action}. No response from server."
32
32
  elsif response.status != 200 and response.status != 201
33
33
  result = JSON.parse(response.body, allow_nan: true, create_additions: true)
34
- raise "Failed to #{action} due to #{result['message']}"
34
+ raise "#{action} failed with status #{response.status}. #{result['message']}."
35
35
  end
36
36
  return JSON.parse(response.body, allow_nan: true, create_additions: true)
37
37
  end
@@ -66,12 +66,20 @@ module OpenC3
66
66
  return _make_request(action: 'disable queue', verb: 'post', uri: "/openc3-api/queues/#{name}/disable", scope: scope)
67
67
  end
68
68
 
69
- def queue_exec(name, index: nil, scope: $openc3_scope)
69
+ def queue_exec(name, id=nil, scope: $openc3_scope)
70
70
  data = {}
71
- data['index'] = index if index
71
+ data['id'] = id if id
72
72
  return _make_request(action: 'exec command', verb: 'post', uri: "/openc3-api/queues/#{name}/exec_command", data: data, scope: scope)
73
73
  end
74
74
 
75
+ # No queue_insert because we do that through the cmd APIs with the queue kwarg
76
+
77
+ def queue_remove(name, id=nil, scope: $openc3_scope)
78
+ data = {}
79
+ data['id'] = id if id
80
+ return _make_request(action: 'remove command', verb: 'post', uri: "/openc3-api/queues/#{name}/remove_command", data: data, scope: scope)
81
+ end
82
+
75
83
  def queue_delete(name, scope: $openc3_scope)
76
84
  return _make_request(action: 'delete queue', verb: 'delete', uri: "/openc3-api/queues/#{name}", scope: scope)
77
85
  end
@@ -232,7 +232,7 @@ module OpenC3
232
232
  raise "initialize_offline_access only valid in COSMOS Enterprise. OPENC3_KEYCLOAK_URL environment variable must be set."
233
233
  end
234
234
  auth = OpenC3KeycloakAuthentication.new(keycloak_url)
235
- auth.token(include_bearer: true, openid_scope: 'openid%20offline_access')
235
+ auth.token(include_bearer: true, openid_scope: 'openid offline_access')
236
236
  set_offline_access(auth.refresh_token)
237
237
  end
238
238
 
@@ -95,11 +95,11 @@ module OpenC3
95
95
  bucket_key = "#{scope}/target_archives/#{target_name}/#{target_name}_current.zip"
96
96
  Logger.info("Retrieving #{bucket_key} from targets bucket")
97
97
  bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, path: zip_path)
98
+ targets_path = "#{base_dir}/targets"
99
+ FileUtils.mkdir_p(targets_path)
98
100
  Zip::File.open(zip_path) do |zip_file|
99
101
  zip_file.each do |entry|
100
- path = File.join("#{base_dir}/targets", entry.name)
101
- FileUtils.mkdir_p(File.dirname(path))
102
- zip_file.extract(entry, path) unless File.exist?(path)
102
+ zip_file.extract(entry.name, destination_directory: targets_path)
103
103
  end
104
104
  end
105
105
 
@@ -23,6 +23,7 @@
23
23
  require 'openc3/version'
24
24
  require 'openc3/io/json_drb'
25
25
  require 'faraday'
26
+ require 'uri'
26
27
 
27
28
  module OpenC3
28
29
  # Basic exception for known errors
@@ -112,9 +113,13 @@ module OpenC3
112
113
  client_id = ENV['OPENC3_API_CLIENT'] || 'api'
113
114
  if ENV['OPENC3_API_USER'] and ENV['OPENC3_API_PASSWORD']
114
115
  # Username and password
115
- data = "username=#{ENV['OPENC3_API_USER']}&password=#{ENV['OPENC3_API_PASSWORD']}"
116
- data << "&client_id=#{client_id}"
117
- data << "&grant_type=password&scope=#{openid_scope}"
116
+ data = {
117
+ 'username' => ENV['OPENC3_API_USER'],
118
+ 'password' => ENV['OPENC3_API_PASSWORD'],
119
+ 'client_id' => client_id,
120
+ 'grant_type' => 'password',
121
+ 'scope' => openid_scope
122
+ }
118
123
  headers = {
119
124
  'Content-Type' => 'application/x-www-form-urlencoded',
120
125
  'User-Agent' => "OpenC3KeycloakAuthorization / #{OPENC3_VERSION} (ruby/openc3/lib/utilities/authentication)",
@@ -134,7 +139,11 @@ module OpenC3
134
139
  # Refresh the token and save token to instance
135
140
  def _refresh_token(current_time)
136
141
  client_id = ENV['OPENC3_API_CLIENT'] || 'api'
137
- data = "client_id=#{client_id}&refresh_token=#{@refresh_token}&grant_type=refresh_token"
142
+ data = {
143
+ 'client_id' => client_id,
144
+ 'refresh_token' => @refresh_token,
145
+ 'grant_type' => 'refresh_token'
146
+ }
138
147
  headers = {
139
148
  'Content-Type' => 'application/x-www-form-urlencoded',
140
149
  'User-Agent' => "OpenC3KeycloakAuthorization / #{OPENC3_VERSION} (ruby/openc3/lib/utilities/authentication)",
@@ -150,11 +159,21 @@ module OpenC3
150
159
  def _make_request(headers, data)
151
160
  realm = ENV['OPENC3_KEYCLOAK_REALM'] || 'openc3'
152
161
  uri = URI("#{@url}/realms/#{realm}/protocol/openid-connect/token")
153
- @log[0] = "request uri: #{uri} header: #{headers} body: #{data}"
162
+ # Obfuscate password and refresh token in logs unless debug mode is enabled
163
+ if JsonDRb.debug?
164
+ log_data = data.inspect
165
+ else
166
+ # Create a copy of the hash with sensitive values obfuscated
167
+ log_data = data.dup
168
+ log_data['password'] = '***' if log_data.key?('password')
169
+ log_data['refresh_token'] = '***' if log_data.key?('refresh_token')
170
+ log_data = log_data.inspect
171
+ end
172
+ @log[0] = "request uri: #{uri} header: #{headers} body: #{log_data}"
154
173
  STDOUT.puts @log[0] if JsonDRb.debug?
155
174
  saved_verbose = $VERBOSE; $VERBOSE = nil
156
175
  begin
157
- resp = @http.post(uri, data, headers)
176
+ resp = @http.post(uri, URI.encode_www_form(data), headers)
158
177
  ensure
159
178
  $VERBOSE = saved_verbose
160
179
  end