Class: OpenC3::InterfaceModel

Inherits:
Model show all
Defined in:
lib/openc3/models/interface_model.rb

Direct Known Subclasses

RouterModel

Constant Summary collapse

INTERFACES_PRIMARY_KEY =
'openc3_interfaces'
ROUTERS_PRIMARY_KEY =
'openc3_routers'

Instance Attribute Summary collapse

Attributes inherited from Model

#name, #plugin, #scope, #updated_at

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model

#check_disable_erb, #create, #destroy, #destroyed?, filter, find_all_by_plugin, from_json, get_all_models, get_model, set, store, store_queued, #update

Constructor Details

#initialize(name:, config_params: [], target_names: [], cmd_target_names: [], tlm_target_names: [], connect_on_startup: true, auto_reconnect: true, reconnect_delay: 5.0, disable_disconnect: false, options: [], secret_options: [], protocols: [], log_stream: nil, updated_at: nil, plugin: nil, needs_dependencies: false, secrets: [], cmd: nil, work_dir: '/openc3/lib/openc3/microservices', ports: [], env: {}, container: nil, prefix: nil, scope:) ⇒ InterfaceModel

Returns a new instance of InterfaceModel.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/openc3/models/interface_model.rb', line 100

def initialize(
  name:,
  config_params: [],
  target_names: [],
  cmd_target_names: [],
  tlm_target_names: [],
  connect_on_startup: true,
  auto_reconnect: true,
  reconnect_delay: 5.0,
  disable_disconnect: false,
  options: [],
  secret_options: [],
  protocols: [],
  log_stream: nil,
  updated_at: nil,
  plugin: nil,
  needs_dependencies: false,
  secrets: [],
  cmd: nil,
  work_dir: '/openc3/lib/openc3/microservices',
  ports: [],
  env: {},
  container: nil,
  prefix: nil,
  scope:
)
  if self.class._get_type == 'INTERFACE'
    super("#{scope}__#{INTERFACES_PRIMARY_KEY}", name: name, updated_at: updated_at, plugin: plugin, scope: scope)
  else
    super("#{scope}__#{ROUTERS_PRIMARY_KEY}", name: name, updated_at: updated_at, plugin: plugin, scope: scope)
  end
  @config_params = config_params
  @target_names = target_names
  @cmd_target_names = cmd_target_names
  @tlm_target_names = tlm_target_names
  @connect_on_startup = connect_on_startup
  @auto_reconnect = auto_reconnect
  @reconnect_delay = reconnect_delay
  @disable_disconnect = disable_disconnect
  @options = options
  @secret_options = secret_options
  @protocols = protocols
  @log_stream = log_stream
  @needs_dependencies = needs_dependencies
  @cmd = cmd
  unless @cmd
    type = self.class._get_type
    microservice_name = "#{@scope}__#{type}__#{@name}"
    if config_params[0] and File.extname(config_params[0]) == '.py'
      work_dir.sub!('openc3/lib', 'openc3/python')
      @cmd = ["python", "#{type.downcase}_microservice.py", microservice_name]
    else
      # If there are no config_params we assume ruby
      @cmd = ["ruby", "#{type.downcase}_microservice.rb", microservice_name]
    end
  end
  @work_dir = work_dir
  @ports = ports
  @env = env
  @container = container
  @prefix = prefix
  @secrets = secrets
end

Instance Attribute Details

#auto_reconnectObject

Returns the value of attribute auto_reconnect.



37
38
39
# File 'lib/openc3/models/interface_model.rb', line 37

def auto_reconnect
  @auto_reconnect
end

#cmdObject

Returns the value of attribute cmd.



47
48
49
# File 'lib/openc3/models/interface_model.rb', line 47

def cmd
  @cmd
end

#cmd_target_namesObject

Returns the value of attribute cmd_target_names.



34
35
36
# File 'lib/openc3/models/interface_model.rb', line 34

def cmd_target_names
  @cmd_target_names
end

#config_paramsObject

Returns the value of attribute config_params.



32
33
34
# File 'lib/openc3/models/interface_model.rb', line 32

def config_params
  @config_params
end

#connect_on_startupObject

Returns the value of attribute connect_on_startup.



36
37
38
# File 'lib/openc3/models/interface_model.rb', line 36

def connect_on_startup
  @connect_on_startup
end

#containerObject

Returns the value of attribute container.



48
49
50
# File 'lib/openc3/models/interface_model.rb', line 48

def container
  @container
end

#disable_disconnectObject

Returns the value of attribute disable_disconnect.



39
40
41
# File 'lib/openc3/models/interface_model.rb', line 39

def disable_disconnect
  @disable_disconnect
end

#envObject

Returns the value of attribute env.



49
50
51
# File 'lib/openc3/models/interface_model.rb', line 49

def env
  @env
end

#interfacesObject

Returns the value of attribute interfaces.



43
44
45
# File 'lib/openc3/models/interface_model.rb', line 43

def interfaces
  @interfaces
end

#log_streamObject

Returns the value of attribute log_stream.



44
45
46
# File 'lib/openc3/models/interface_model.rb', line 44

def log_stream
  @log_stream
end

#needs_dependenciesObject

Returns the value of attribute needs_dependencies.



45
46
47
# File 'lib/openc3/models/interface_model.rb', line 45

def needs_dependencies
  @needs_dependencies
end

#optionsObject

Returns the value of attribute options.



40
41
42
# File 'lib/openc3/models/interface_model.rb', line 40

def options
  @options
end

#portsObject

Returns the value of attribute ports.



51
52
53
# File 'lib/openc3/models/interface_model.rb', line 51

def ports
  @ports
end

#prefixObject

Returns the value of attribute prefix.



52
53
54
# File 'lib/openc3/models/interface_model.rb', line 52

def prefix
  @prefix
end

#protocolsObject

Returns the value of attribute protocols.



42
43
44
# File 'lib/openc3/models/interface_model.rb', line 42

def protocols
  @protocols
end

#reconnect_delayObject

Returns the value of attribute reconnect_delay.



38
39
40
# File 'lib/openc3/models/interface_model.rb', line 38

def reconnect_delay
  @reconnect_delay
end

#secret_optionsObject

Returns the value of attribute secret_options.



41
42
43
# File 'lib/openc3/models/interface_model.rb', line 41

def secret_options
  @secret_options
end

#secretsObject

Returns the value of attribute secrets.



46
47
48
# File 'lib/openc3/models/interface_model.rb', line 46

def secrets
  @secrets
end

#target_namesObject

Redundant superset of cmd_target_names and tlm_target_names for backwards compat



33
34
35
# File 'lib/openc3/models/interface_model.rb', line 33

def target_names
  @target_names
end

#tlm_target_namesObject

Returns the value of attribute tlm_target_names.



35
36
37
# File 'lib/openc3/models/interface_model.rb', line 35

def tlm_target_names
  @tlm_target_names
end

#work_dirObject

Returns the value of attribute work_dir.



50
51
52
# File 'lib/openc3/models/interface_model.rb', line 50

def work_dir
  @work_dir
end

Class Method Details

._get_keyObject

Helper method to return the correct primary key based on class name



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/openc3/models/interface_model.rb', line 88

def self._get_key
  type = _get_type
  case type
  when 'INTERFACE'
    INTERFACES_PRIMARY_KEY
  when 'ROUTER'
    ROUTERS_PRIMARY_KEY
  else
    raise "Unknown type #{type} from class #{self.name}"
  end
end

._get_typeObject

Helper method to return the correct type based on class name



83
84
85
# File 'lib/openc3/models/interface_model.rb', line 83

def self._get_type
  self.name.to_s.split("Model")[0].upcase.split("::")[-1]
end

.all(scope:) ⇒ Object



64
65
66
# File 'lib/openc3/models/interface_model.rb', line 64

def self.all(scope:)
  super("#{scope}__#{_get_key}")
end

.get(name:, scope:) ⇒ Object

NOTE: The following three class methods are used by the ModelController and are reimplemented to enable various Model class methods to work



56
57
58
# File 'lib/openc3/models/interface_model.rb', line 56

def self.get(name:, scope:)
  super("#{scope}__#{_get_key}", name: name)
end

.handle_config(parser, keyword, parameters, plugin: nil, needs_dependencies: false, scope:) ⇒ Object

Called by the PluginModel to allow this class to validate it’s top-level keyword: “INTERFACE” Interface/Router specific keywords are handled by the instance method “handle_config” NOTE: See RouterModel for the router method implementation



72
73
74
75
76
77
78
79
80
# File 'lib/openc3/models/interface_model.rb', line 72

def self.handle_config(parser, keyword, parameters, plugin: nil, needs_dependencies: false, scope:)
  case keyword
  when 'INTERFACE'
    parser.verify_num_parameters(2, nil, "INTERFACE <Name> <Filename> <Specific Parameters>")
    return self.new(name: parameters[0].upcase, config_params: parameters[1..-1], plugin: plugin, needs_dependencies: needs_dependencies, scope: scope)
  else
    raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Interface: #{keyword} #{parameters.join(" ")}")
  end
end

.names(scope:) ⇒ Object



60
61
62
# File 'lib/openc3/models/interface_model.rb', line 60

def self.names(scope:)
  super("#{scope}__#{_get_key}")
end

Instance Method Details

#as_json(*a) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/openc3/models/interface_model.rb', line 201

def as_json(*a)
  {
    'name' => @name,
    'config_params' => @config_params,
    'target_names' => @target_names,
    'cmd_target_names' => @cmd_target_names,
    'tlm_target_names' => @tlm_target_names,
    'connect_on_startup' => @connect_on_startup,
    'auto_reconnect' => @auto_reconnect,
    'reconnect_delay' => @reconnect_delay,
    'disable_disconnect' => @disable_disconnect,
    'options' => @options,
    'secret_options' => @secret_options,
    'protocols' => @protocols,
    'log_stream' => @log_stream,
    'plugin' => @plugin,
    'needs_dependencies' => @needs_dependencies,
    'secrets' => @secrets.as_json(*a),
    'cmd' => @cmd,
    'work_dir' => @work_dir,
    'ports' => @ports,
    'env' => @env,
    'container' => @container,
    'prefix' => @prefix,
    'updated_at' => @updated_at
  }
end

#buildObject

Called by InterfaceMicroservice to instantiate the Interface defined by the model configuration. Must be called after get_model which calls from_json to instantiate the class and populate the attributes.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/openc3/models/interface_model.rb', line 167

def build
  klass = OpenC3.require_class(@config_params[0])
  if @config_params.length > 1
    interface_or_router = klass.new(*@config_params[1..-1])
  else
    interface_or_router = klass.new
  end
  interface_or_router.secrets.setup(@secrets)
  interface_or_router.target_names = @target_names.dup
  interface_or_router.cmd_target_names = @cmd_target_names.dup
  interface_or_router.tlm_target_names = @tlm_target_names.dup
  interface_or_router.connect_on_startup = @connect_on_startup
  interface_or_router.auto_reconnect = @auto_reconnect
  interface_or_router.reconnect_delay = @reconnect_delay
  interface_or_router.disable_disconnect = @disable_disconnect
  @options.each do |option|
    interface_or_router.set_option(option[0], option[1..-1])
  end
  @secret_options.each do |option|
    secret_name = option[1]
    secret_value = interface_or_router.secrets.get(secret_name, scope: @scope)
    interface_or_router.set_option(option[0], [secret_value])
  end
  @protocols.each do |protocol|
    klass = OpenC3.require_class(protocol[1])
    interface_or_router.add_protocol(klass, protocol[2..-1], protocol[0].upcase.intern)
  end
  if @log_stream
    interface_or_router.stream_log_pair = StreamLogPair.new(interface_or_router.name, @log_stream)
    interface_or_router.start_raw_logging
  end
  interface_or_router
end

#deploy(gem_path, variables, validate_only: false) ⇒ Object

Creates a MicroserviceModel to deploy the Interface/Router



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/openc3/models/interface_model.rb', line 353

def deploy(gem_path, variables, validate_only: false)
  type = self.class._get_type
  microservice_name = "#{@scope}__#{type}__#{@name}"
  microservice = MicroserviceModel.new(
    name: microservice_name,
    work_dir: @work_dir,
    cmd: @cmd,
    env: @env,
    ports: @ports,
    container: @container,
    target_names: @target_names,
    plugin: @plugin,
    needs_dependencies: @needs_dependencies,
    secrets: @secrets,
    prefix: @prefix,
    scope: @scope
  )
  unless validate_only
    @target_names.each { |target_name| ensure_target_exists(target_name) }
    microservice.create
    microservice.deploy(gem_path, variables)
    ConfigTopic.write({ kind: 'created', type: type.downcase, name: @name, plugin: @plugin }, scope: @scope)
    Logger.info "Configured #{type.downcase} microservice #{microservice_name}"
  end
  microservice
end

#ensure_target_exists(target_name) ⇒ Object



229
230
231
232
233
# File 'lib/openc3/models/interface_model.rb', line 229

def ensure_target_exists(target_name)
  target = TargetModel.get(name: target_name, scope: @scope)
  raise "Target #{target_name} does not exist" unless target
  target
end

#handle_config(parser, keyword, parameters) ⇒ Object

Handles Interface/Router specific configuration keywords



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/openc3/models/interface_model.rb', line 236

def handle_config(parser, keyword, parameters)
  case keyword
  when 'MAP_TARGET'
    parser.verify_num_parameters(1, 1, "#{keyword} <Target Name>")
    target_name = parameters[0].upcase
    @target_names << target_name unless @target_names.include?(target_name)
    @cmd_target_names << target_name unless @cmd_target_names.include?(target_name)
    @tlm_target_names << target_name unless @tlm_target_names.include?(target_name)

  when 'MAP_CMD_TARGET'
    parser.verify_num_parameters(1, 1, "#{keyword} <Target Name>")
    target_name = parameters[0].upcase
    @target_names << target_name unless @target_names.include?(target_name)
    @cmd_target_names << target_name unless @cmd_target_names.include?(target_name)

  when 'MAP_TLM_TARGET'
    parser.verify_num_parameters(1, 1, "#{keyword} <Target Name>")
    target_name = parameters[0].upcase
    @target_names << target_name unless @target_names.include?(target_name)
    @tlm_target_names << target_name unless @tlm_target_names.include?(target_name)

  when 'DONT_CONNECT'
    parser.verify_num_parameters(0, 0, "#{keyword}")
    @connect_on_startup = false

  when 'DONT_RECONNECT'
    parser.verify_num_parameters(0, 0, "#{keyword}")
    @auto_reconnect = false

  when 'RECONNECT_DELAY'
    parser.verify_num_parameters(1, 1, "#{keyword} <Delay in Seconds>")
    @reconnect_delay = Float(parameters[0])

  when 'DISABLE_DISCONNECT'
    parser.verify_num_parameters(0, 0, "#{keyword}")
    @disable_disconnect = true

  when 'OPTION'
    parser.verify_num_parameters(2, nil, "#{keyword} <Option Name> <Option Value 1> <Option Value 2 (optional)> <etc>")
    @options << parameters.dup

  when 'PROTOCOL'
    usage = "#{keyword} <READ WRITE READ_WRITE> <protocol filename or classname> <Protocol specific parameters>"
    parser.verify_num_parameters(2, nil, usage)
    unless %w(READ WRITE READ_WRITE).include? parameters[0].upcase
      raise parser.error("Invalid protocol type: #{parameters[0]}", usage)
    end

    @protocols << parameters.dup

  when 'DONT_LOG'
    Logger.warn "DONT_LOG is deprecated and does nothing."

  when 'LOG_STREAM', 'LOG_RAW'
    parser.verify_num_parameters(0, nil, "#{keyword} <Log Stream Class File (optional)> <Log Stream Parameters (optional)>")
    @log_stream = parameters.dup # Even if it is empty we copy it to set it as not nil

  when 'SECRET'
    parser.verify_num_parameters(3, 5, "#{keyword} <Secret Type: ENV or FILE> <Secret Name> <Environment Variable Name or File Path> <Option Name (Optional)> <Secret Store Name (Optional)>")
    @secrets << parameters[0..2]
    if ConfigParser.handle_nil(parameters[3])
      # Option Name, Secret Name
      @secret_options << [parameters[3], parameters[1]]
    end
    if ConfigParser.handle_nil(parameters[4])
      @secrets[-1] << parameters[4]
    end

  when 'ENV'
    parser.verify_num_parameters(2, 2, "#{keyword} <Key> <Value>")
    @env[parameters[0]] = parameters[1]

  when 'PORT'
    usage = "PORT <Number> <Protocol (Optional)"
    parser.verify_num_parameters(1, 2, usage)
    begin
      @ports << [Integer(parameters[0])]
    rescue # In case Integer fails
      raise ConfigParser::Error.new(parser, "Port must be an integer: #{parameters[0]}", usage)
    end
    protocol = ConfigParser.handle_nil(parameters[1])
    if protocol
      # Per https://kubernetes.io/docs/concepts/services-networking/service/#protocol-support
      if %w(TCP UDP SCTP).include?(protocol.upcase)
        @ports[-1] << protocol.upcase
      else
        raise ConfigParser::Error.new(parser, "Unknown port protocol: #{parameters[1]}", usage)
      end
    else
      @ports[-1] << 'TCP'
    end

  when 'WORK_DIR'
    parser.verify_num_parameters(1, 1, "#{keyword} <Dir>")
    @work_dir = parameters[0]

  when 'CMD'
    parser.verify_num_parameters(1, nil, "#{keyword} <Args>")
    @cmd = parameters.dup

  when 'CONTAINER'
    parser.verify_num_parameters(1, 1, "#{keyword} <Container Image Name>")
    @container = parameters[0]

  when 'ROUTE_PREFIX'
    parser.verify_num_parameters(1, 1, "#{keyword} <Route Prefix>")
    @prefix = parameters[0]

  else
    raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Interface/Router: #{keyword} #{parameters.join(" ")}")

  end

  return nil
end

#map_target(target_name, cmd_only: false, tlm_only: false, unmap_old: true) ⇒ Object



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/openc3/models/interface_model.rb', line 431

def map_target(target_name, cmd_only: false, tlm_only: false, unmap_old: true)
  if cmd_only and tlm_only
    cmd_only = false
    tlm_only = false
  end
  target_name = target_name.to_s.upcase
  ensure_target_exists(target_name)

  if unmap_old
    # Remove from old interface
    all_interfaces = InterfaceModel.all(scope: @scope)
    old_interface = nil
    all_interfaces.each do |old_interface_name, old_interface_details|
      if old_interface_details['target_names'].include?(target_name)
        old_interface = InterfaceModel.from_json(old_interface_details, scope: @scope)
        old_interface.unmap_target(target_name, cmd_only: cmd_only, tlm_only: tlm_only) if old_interface
      end
    end
  end

  # Add to this interface
  @target_names << target_name unless @target_names.include?(target_name)
  @cmd_target_names << target_name unless @cmd_target_names.include?(target_name) or tlm_only
  @tlm_target_names << target_name unless @tlm_target_names.include?(target_name) or cmd_only
  update()

  # Respawn the microservice
  type = self.class._get_type
  microservice_name = "#{@scope}__#{type}__#{@name}"
  microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
  microservice.target_names << target_name unless microservice.target_names.include?(target_name)
  microservice.update
end

#undeployObject

Looks up the deployed MicroserviceModel and destroy the microservice model should should trigger the operator to kill the microservice that in turn will destroy the InterfaceStatusModel when a stop is called.



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/openc3/models/interface_model.rb', line 383

def undeploy
  type = self.class._get_type
  name = "#{@scope}__#{type}__#{@name}"
  model = MicroserviceModel.get_model(name: name, scope: @scope)
  if model
    model.destroy
    ConfigTopic.write({ kind: 'deleted', type: type.downcase, name: @name, plugin: @plugin }, scope: @scope)
  end

  if type == 'INTERFACE'
    status_model = InterfaceStatusModel.get_model(name: @name, scope: @scope)
  else
    status_model = RouterStatusModel.get_model(name: @name, scope: @scope)
  end
  status_model.destroy if status_model
rescue Exception => error
  Logger.error("Error undeploying interface/router model #{@name} in scope #{@scope} due to #{error}")
end

#unmap_target(target_name, cmd_only: false, tlm_only: false) ⇒ Object



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/openc3/models/interface_model.rb', line 402

def unmap_target(target_name, cmd_only: false, tlm_only: false)
  if cmd_only and tlm_only
    cmd_only = false
    tlm_only = false
  end
  target_name = target_name.to_s.upcase

  # Remove from this interface
  if cmd_only
    @cmd_target_names.delete(target_name)
    @target_names.delete(target_name) unless @tlm_target_names.include?(target_name)
  elsif tlm_only
    @tlm_target_names.delete(target_name)
    @target_names.delete(target_name) unless @cmd_target_names.include?(target_name)
  else
    @cmd_target_names.delete(target_name)
    @tlm_target_names.delete(target_name)
    @target_names.delete(target_name)
  end
  update()

  # Respawn the microservice
  type = self.class._get_type
  microservice_name = "#{@scope}__#{type}__#{@name}"
  microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope)
  microservice.target_names.delete(target_name) unless @target_names.include?(target_name)
  microservice.update
end