Class: Cosmos::System

Inherits:
Object show all
Defined in:
lib/cosmos/system/system.rb,
ext/cosmos/ext/telemetry/telemetry.c

Overview

System is the primary entry point into the COSMOS framework. It captures system wide configuration items such as the available ports and paths to various files used by the system. The #commands, #telemetry, and #limits class variables are the primary access points for applications. The #targets variable provides access to all the targets defined by the system. Its primary responsibily is to load the system configuration file and create all the Target instances. It also saves and restores configurations using a MD5 checksum over the entire configuration to detect changes.

Constant Summary collapse

KNOWN_PORTS =

Known COSMOS ports

['CTS_API', 'TLMVIEWER_API', 'CTS_PREIDENTIFIED']
KNOWN_PATHS =

Known COSMOS paths

['LOGS', 'TMP', 'SAVED_CONFIG', 'TABLES', 'HANDBOOKS', 'PROCEDURES']
@@instance =
nil
@@instance_mutex =
Mutex.new

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename = nil) ⇒ System

Create a new System object. Note, this should not be called directly but you should instead use System.instance and treat this class as a singleton.

Parameters:

  • filename (String) (defaults to: nil)

    Full path to the system configuration file to read. Be default this is <Cosmos::USERPATH>/config/system/system.txt



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/cosmos/system/system.rb', line 76

def initialize(filename = nil)
  raise "Cosmos::System created twice" unless @@instance.nil?
  @targets = {}
  @targets['UNKNOWN'] = Target.new('UNKNOWN')
  @config = nil
  @commands = nil
  @telemetry = nil
  @limits = nil
  @cmd_tlm_version = nil
  @default_packet_log_writer = PacketLogWriter
  @default_packet_log_reader = PacketLogReader
  @sound = false
  @use_dns = false
  @acl = nil
  @staleness_seconds = 30
  @limits_set = :DEFAULT

  @ports = {}
  @ports['CTS_API'] = 7777
  @ports['TLMVIEWER_API'] = 7778
  @ports['CTS_PREIDENTIFIED'] = 7779

  @paths = {}
  @paths['LOGS'] = File.join(USERPATH, 'outputs', 'logs')
  @paths['TMP'] = File.join(USERPATH, 'outputs', 'tmp')
  @paths['SAVED_CONFIG'] = File.join(USERPATH, 'outputs', 'saved_config')
  @paths['TABLES'] = File.join(USERPATH, 'outputs', 'tables')
  @paths['HANDBOOKS'] = File.join(USERPATH, 'outputs', 'handbooks')
  @paths['PROCEDURES'] = [File.join(USERPATH, 'procedures')]

  unless filename
    system_arg = false
    ARGV.each do |arg|
      if system_arg
        filename = File.join(USERPATH, 'config', 'system', arg)
        break
      end
      system_arg = true if arg == '--system'
    end
    filename = File.join(USERPATH, 'config', 'system', 'system.txt') unless filename
  end
  process_file(filename)
  ENV['COSMOS_LOGS_DIR'] = @paths['LOGS']

  @initial_filename = filename
  @initial_config = nil
  @@instance = self
end

Class Method Details

.clear_countersObject

Clear the command and telemetry counters for all the targets as well as the counters associated with the Commands and Telemetry instances



135
136
137
138
139
140
141
142
# File 'lib/cosmos/system/system.rb', line 135

def self.clear_counters
  self.instance.targets.each do |target_name, target|
    target.cmd_cnt = 0
    target.tlm_cnt = 0
  end
  self.instance.telemetry.clear_counters
  self.instance.commands.clear_counters
end

.commandsCommands

Returns Access to the command definiton.

Returns:

  • (Commands)

    Access to the command definiton



151
152
153
# File 'lib/cosmos/system/system.rb', line 151

def self.commands
  return self.instance.commands
end

.configuration_nameString

Returns Configuration name.

Returns:

  • (String)

    Configuration name



126
127
128
129
130
131
# File 'lib/cosmos/system/system.rb', line 126

def self.configuration_name
  # In order not to make the @config instance variable accessable to the
  # outside (using a regular accessor method) we use the ability of the
  # object to grab its own instance variable
  self.instance.instance_variable_get(:@config).name
end

.instance(filename = nil) ⇒ System

Returns The System singleton.

Returns:

  • (System)

    The System singleton



201
202
203
204
205
206
207
# File 'lib/cosmos/system/system.rb', line 201

def self.instance(filename = nil)
  return @@instance if @@instance
  @@instance_mutex.synchronize do
    @@instance ||= self.new(filename)
    return @@instance
  end
end

.limitsLimits

Returns Access to the limits definition.

Returns:

  • (Limits)

    Access to the limits definition



173
174
175
# File 'lib/cosmos/system/system.rb', line 173

def self.limits
  return self.instance.limits
end

.limits_set=(limits_set) ⇒ Object

Change the system limits set

Parameters:

  • The (Symbol)

    name of the limits set. :DEFAULT is always an option but limits sets are user defined



196
197
198
# File 'lib/cosmos/system/system.rb', line 196

def self.limits_set=(limits_set)
  self.instance.limits_set = limits_set
end

.load_configuration(name = nil) ⇒ String, Exception/nil

Load the specified configuration by iterating through the SAVED_CONFIG directory looking for a matching MD5 sum. Updates the internal state so subsequent commands and telemetry methods return the new configuration.

Parameters:

  • name (String) (defaults to: nil)

    MD5 string which identifies the configuration. Pass nil to load the default configuration.

Returns:



469
470
471
# File 'lib/cosmos/system/system.rb', line 469

def self.load_configuration(name = nil)
  return self.instance.load_configuration(name)
end

.telemetryTelemetry

Returns Access to the telemetry definition.

Returns:

  • (Telemetry)

    Access to the telemetry definition



162
163
164
# File 'lib/cosmos/system/system.rb', line 162

def self.telemetry
  return self.instance.telemetry
end

Instance Method Details

#aclACL

Returns Access control list showing which machines can have access.

Returns:

  • (ACL)

    Access control list showing which machines can have access



54
# File 'lib/cosmos/system/system.rb', line 54

instance_attr_reader :acl

#cmd_tlm_versionString

Returns Arbitrary string containing the version.

Returns:

  • (String)

    Arbitrary string containing the version



38
# File 'lib/cosmos/system/system.rb', line 38

instance_attr_reader :cmd_tlm_version

#commandsCommands

Returns Access to the command definiton.

Returns:

  • (Commands)

    Access to the command definiton



145
146
147
148
# File 'lib/cosmos/system/system.rb', line 145

def commands
  load_packets() unless @config
  return @commands
end

#default_packet_log_readerPacketLogReader

Returns Class used to read log files.

Returns:



42
# File 'lib/cosmos/system/system.rb', line 42

instance_attr_reader :default_packet_log_reader

#default_packet_log_writerPacketLogWriter

Returns Class used to create log files.

Returns:



40
# File 'lib/cosmos/system/system.rb', line 40

instance_attr_reader :default_packet_log_writer

#initial_configPacketDefinition

Returns Stores the initial packet list used when this System was initialized.

Returns:

  • (PacketDefinition)

    Stores the initial packet list used when this System was initialized



52
# File 'lib/cosmos/system/system.rb', line 52

instance_attr_reader :initial_config

#initial_filenameString

Returns Stores the initial configuration file used when this System was initialized.

Returns:

  • (String)

    Stores the initial configuration file used when this System was initialized



49
# File 'lib/cosmos/system/system.rb', line 49

instance_attr_reader :initial_filename

#limitsLimits

Returns Access to the limits definition.

Returns:

  • (Limits)

    Access to the limits definition



167
168
169
170
# File 'lib/cosmos/system/system.rb', line 167

def limits
  load_packets() unless @config
  return @limits
end

#limits_setSymbol

Returns The current limits set.

Returns:

  • (Symbol)

    The current limits set



60
# File 'lib/cosmos/system/system.rb', line 60

instance_attr_reader :limits_set

#limits_set=(limits_set) ⇒ Object

Change the system limits set

Parameters:

  • The (Symbol)

    name of the limits set. :DEFAULT is always an option but limits sets are user defined



181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/cosmos/system/system.rb', line 181

def limits_set=(limits_set)
  load_packets() unless @config
  new_limits_set = limits_set.to_s.upcase.intern
  if @limits_set != new_limits_set
    if @config.limits_sets.include?(new_limits_set)
      @limits_set = new_limits_set
      Logger.info("Limits Set Changed to: #{@limits_set}")
      CmdTlmServer.instance.post_limits_event(:LIMITS_SET, System.limits_set) if defined? CmdTlmServer and CmdTlmServer.instance
    else
      raise "Unknown limits set requested: #{new_limits_set}"
    end
  end
end

#load_configuration(name = nil) ⇒ String, Exception/nil

Load the specified configuration by iterating through the SAVED_CONFIG directory looking for a matching MD5 sum. Updates the internal state so subsequent commands and telemetry methods return the new configuration.

Parameters:

  • name (String) (defaults to: nil)

    MD5 string which identifies the configuration. Pass nil to load the default configuration.

Returns:



430
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
464
465
466
# File 'lib/cosmos/system/system.rb', line 430

def load_configuration(name = nil)
  if name and @config
    # Make sure they're requesting something other than the current
    # configuration.
    if name != @config.name
      # If they want the initial configuration we can just swap out the
      # current configuration without doing any file processing
      if name == @initial_config.name
        update_config(@initial_config)
      else
        # Look for the requested configuration in the saved configurations
        configuration_directory = find_configuration(name)
        if configuration_directory
          # We found the configuration requested. Reprocess the system.txt
          # and reload the packets
          begin
            process_file(File.join(configuration_directory, 'system.txt'), configuration_directory)
            load_packets(name)
          rescue Exception => error
            # Failed to load - Restore initial
            update_config(@initial_config)
            return @config.name, error
          end
        else
          # We couldn't find the configuration request. Reload the
          # initial configuration
          update_config(@initial_config)
        end
      end
    end
  else
    # Ensure packets have been lazy loaded
    System.commands
    update_config(@initial_config)
  end
  return @config.name, nil
end

#pathsHash<String,String>

Returns Hash of all the known paths and their values.

Returns:



36
# File 'lib/cosmos/system/system.rb', line 36

instance_attr_reader :paths

#portsHash<String,Fixnum>

Returns Hash of all the known ports and their values.

Returns:

  • (Hash<String,Fixnum>)

    Hash of all the known ports and their values



34
# File 'lib/cosmos/system/system.rb', line 34

instance_attr_reader :ports

#process_file(filename, configuration_directory = nil) ⇒ Object

Process the system.txt configuration file

Parameters:

  • filename (String)

    The configuration file

  • configuration_directory (String) (defaults to: nil)

    The configuration directory to search for the target command and telemetry files. Pass nil to look in the default location of <USERPATH>/config/targets.



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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
# File 'lib/cosmos/system/system.rb', line 215

def process_file(filename, configuration_directory = nil)
  @targets = {}
  # Set config to nil so things will lazy load later
  @config = nil
  acl_list = []
  all_allowed = false
  first_procedures_path = true

  Cosmos.set_working_dir do
    parser = ConfigParser.new

    # First pass - Everything except targets
    parser.parse_file(filename) do |keyword, parameters|
      case keyword
      when 'AUTO_DECLARE_TARGETS', 'DECLARE_TARGET', 'DECLARE_GEM_TARGET'
        # Will be handled by second pass

      when 'PORT'
        usage = "#{keyword} <PORT NAME> <PORT VALUE>"
        parser.verify_num_parameters(2, 2, usage)
        port_name = parameters[0].to_s.upcase
        @ports[port_name] = Integer(parameters[1])
        Logger.warn("Unknown port name given: #{port_name}") unless KNOWN_PORTS.include?(port_name)

      when 'PATH'
        usage = "#{keyword} <PATH NAME> <PATH>"
        parser.verify_num_parameters(2, 2, usage)
        path_name = parameters[0].to_s.upcase
        path = File.expand_path(parameters[1])
        if path_name == 'PROCEDURES'
          if first_procedures_path
            @paths[path_name] = []
            first_procedures_path = false
          end
          @paths[path_name] << path
        else
          @paths[path_name] = path
        end
        unless Dir.exist?(path)
          begin
            FileUtils.mkdir_p(path)
            raise "Path creation failed: #{path}" unless File.exist?(path)
            Logger.info "Created PATH #{path_name} #{path}"
          rescue Exception => err
            Logger.error "Problem creating PATH #{path_name} #{path}\n#{err.formatted}"
          end
        end
        Logger.warn("Unknown path name given: #{path_name}") unless KNOWN_PATHS.include?(path_name)

      when 'DEFAULT_PACKET_LOG_WRITER'
        usage = "#{keyword} <FILENAME>"
        parser.verify_num_parameters(1, 1, usage)
        @default_packet_log_writer = Cosmos.require_class(parameters[0])

      when 'DEFAULT_PACKET_LOG_READER'
        usage = "#{keyword} <FILENAME>"
        parser.verify_num_parameters(1, 1, usage)
        @default_packet_log_reader = Cosmos.require_class(parameters[0])

      when 'ENABLE_SOUND'
        usage = "#{keyword}"
        parser.verify_num_parameters(0, 0, usage)
        @sound = true

      when 'DISABLE_DNS'
        usage = "#{keyword}"
        parser.verify_num_parameters(0, 0, usage)
        @use_dns = false

      when 'ENABLE_DNS'
        usage = "#{keyword}"
        parser.verify_num_parameters(0, 0, usage)
        @use_dns = true

      when 'ALLOW_ACCESS'
        parser.verify_num_parameters(1, 1, "#{keyword} <IP Address or Hostname>")
        begin
          addr = parameters[0].upcase
          if addr == 'ALL'
            all_allowed = true
            acl_list = []
          end

          unless all_allowed
            first_char = addr[0..0]
            if !((first_char =~ /[1234567890]/) or (first_char == '*') or (addr.upcase == 'ALL'))
              # Try to lookup IP Address
              info = Socket.gethostbyname(addr)
              addr = "#{info[3].getbyte(0)}.#{info[3].getbyte(1)}.#{info[3].getbyte(2)}.#{info[3].getbyte(3)}"
              if (acl_list.empty?)
                acl_list << 'allow'
                acl_list << '127.0.0.1'
              end
              acl_list << 'allow'
              acl_list << addr
            else
              raise "badly formatted address #{addr}" unless /\b(?:\d{1,3}\.){3}\d{1,3}\b/.match(addr)
              if (acl_list.empty?)
                acl_list << 'allow'
                acl_list << '127.0.0.1'
              end
              acl_list << 'allow'
              acl_list << addr
            end
          end
        rescue => err
          raise parser.error("Problem with ALLOW_ACCESS due to #{err.message.strip}")
        end

      when 'STALENESS_SECONDS'
        parser.verify_num_parameters(1, 1, "#{keyword} <Value in Seconds>")
        @staleness_seconds = Integer(parameters[0])

      when 'CMD_TLM_VERSION'
        usage = "#{keyword} <VERSION>"
        parser.verify_num_parameters(1, 1, usage)
        @cmd_tlm_version = parameters[0]

      else
        # blank lines will have a nil keyword and should not raise an exception
        raise parser.error("Unknown keyword '#{keyword}'") if keyword
      end # case keyword
    end # parser.parse_file

    @acl = ACL.new(acl_list, ACL::ALLOW_DENY) unless acl_list.empty?

    # Second pass - Process targets
    process_targets(parser, filename, configuration_directory)

  end # Cosmos.set_working_dir
end

#process_targets(parser, filename, configuration_directory) ⇒ Object

Parse the system.txt configuration file looking for keywords associated with targets and create all the Target instances in the system.

Parameters:

  • parser (ConfigParser)

    Parser created by process_file

  • filename (String)

    The configuration file

  • configuration_directory (String)

    The configuration directory to search for the target command and telemetry files. Pass nil to look in the default location of <USERPATH>/config/targets.



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
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/cosmos/system/system.rb', line 353

def process_targets(parser, filename, configuration_directory)
  parser.parse_file(filename) do |keyword, parameters|
    case keyword
    when 'AUTO_DECLARE_TARGETS'
      usage = "#{keyword}"
      parser.verify_num_parameters(0, 0, usage)
      path = File.join(USERPATH, 'config', 'targets')
      unless File.exist? path
        raise parser.error("#{path} must exist", usage)
      end
      system_found = false
      dirs = []
      Dir.foreach(File.join(USERPATH, 'config', 'targets')) { |dir_filename| dirs << dir_filename }
      dirs.sort!
      dirs.each do |dir_filename|
        if dir_filename[0] != '.'
          if dir_filename == dir_filename.upcase
            if dir_filename == 'SYSTEM'
              system_found = true
              next
            end
            target = Target.new(dir_filename)
            @targets[target.name] = target
          else
            raise parser.error("Target folder must be uppercase: '#{dir_filename}'")
          end
        end
      end

      auto_detect_gem_based_targets()

      if system_found
        target = Target.new('SYSTEM')
        @targets[target.name] = target
      end

    when 'DECLARE_TARGET'
      usage = "#{keyword} <TARGET NAME> <SUBSTITUTE TARGET NAME (Optional)> <TARGET FILENAME (Optional - defaults to target.txt)>"
      parser.verify_num_parameters(1, 3, usage)
      target_name = parameters[0].to_s.upcase
      substitute_name = nil
      substitute_name = ConfigParser.handle_nil(parameters[1])
      substitute_name.to_s.upcase if substitute_name
      if configuration_directory
        folder_name = File.join(configuration_directory, target_name)
      else
        folder_name = File.join(USERPATH, 'config', 'targets', target_name)
      end
      unless Dir.exist?(folder_name)
        raise parser.error("Target folder must exist '#{folder_name}'.")
      end
      target = Target.new(target_name, substitute_name, configuration_directory, ConfigParser.handle_nil(parameters[2]))
      @targets[target.name] = target

    when 'DECLARE_GEM_TARGET'
      usage = "#{keyword} <GEM NAME> <SUBSTITUTE TARGET NAME (Optional)> <TARGET FILENAME (Optional - defaults to target.txt)>"
      parser.verify_num_parameters(1, 3, usage)
      # Remove 'cosmos' from the gem name 'cosmos-power-supply'
      target_name = parameters[0].split('-')[1..-1].join('-').to_s.upcase
      substitute_name = nil
      substitute_name = ConfigParser.handle_nil(parameters[1])
      substitute_name.to_s.upcase if substitute_name
      gem_dir = Gem::Specification.find_by_name(parameters[0]).gem_dir
      target = Target.new(target_name, substitute_name, configuration_directory, ConfigParser.handle_nil(parameters[2]), gem_dir)
      @targets[target.name] = target

    end # case keyword
  end # parser.parse_file
end

#soundBoolean

Returns Whether to use sound for alerts.

Returns:

  • (Boolean)

    Whether to use sound for alerts



44
# File 'lib/cosmos/system/system.rb', line 44

instance_attr_reader :sound

#staleness_secondsInteger

Returns The number of seconds before a telemetry packet is considered stale.

Returns:

  • (Integer)

    The number of seconds before a telemetry packet is considered stale



58
# File 'lib/cosmos/system/system.rb', line 58

instance_attr_reader :staleness_seconds

#targetsHash<String,Target>

Returns Hash of all the known targets.

Returns:



56
# File 'lib/cosmos/system/system.rb', line 56

instance_attr_reader :targets

#telemetryTelemetry

Returns Access to the telemetry definition.

Returns:

  • (Telemetry)

    Access to the telemetry definition



156
157
158
159
# File 'lib/cosmos/system/system.rb', line 156

def telemetry
  load_packets() unless @config
  return @telemetry
end

#use_dnsBoolean

Returns Whether to use DNS to lookup IP addresses or not.

Returns:

  • (Boolean)

    Whether to use DNS to lookup IP addresses or not



46
# File 'lib/cosmos/system/system.rb', line 46

instance_attr_reader :use_dns