BallAerospace/COSMOS

View on GitHub
cosmos/lib/cosmos/models/target_model.rb

Summary

Maintainability
D
1 day
Test Coverage
# encoding: ascii-8bit

# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# This program may also be used under the terms of a commercial or
# enterprise edition license of COSMOS if purchased from the
# copyright holder

require 'cosmos/top_level'
require 'cosmos/models/model'
require 'cosmos/models/cvt_model'
require 'cosmos/models/microservice_model'
require 'cosmos/topics/limits_event_topic'
require 'cosmos/topics/config_topic'
require 'cosmos/system'
require 'cosmos/utilities/s3'
require 'cosmos/utilities/zip'
require 'fileutils'
require 'tmpdir'

module Cosmos
  # Manages the target in Redis. It stores the target itself under the
  # <SCOPE>__cosmos_targets key under the target name field. All the command packets
  # in the target are stored under the <SCOPE>__cosmoscmd__<TARGET NAME> key and the
  # telemetry under the <SCOPE>__cosmostlm__<TARGET NAME> key. Any new limits sets
  # are merged into the <SCOPE>__limits_sets key as fields. Any new limits groups are
  # created under <SCOPE>__limits_groups with field name. These Redis key/fields are
  # all removed when the undeploy method is called.
  class TargetModel < Model
    PRIMARY_KEY = 'cosmos_targets'
    VALID_TYPES = %i(CMD TLM)

    attr_accessor :folder_name
    attr_accessor :requires
    attr_accessor :ignored_parameters
    attr_accessor :ignored_items
    attr_accessor :limits_groups
    attr_accessor :cmd_tlm_files
    attr_accessor :cmd_unique_id_mode
    attr_accessor :tlm_unique_id_mode
    attr_accessor :id
    attr_accessor :cmd_log_cycle_time
    attr_accessor :cmd_log_cycle_size
    attr_accessor :cmd_log_retain_time
    attr_accessor :cmd_decom_log_cycle_time
    attr_accessor :cmd_decom_log_cycle_size
    attr_accessor :cmd_decom_log_retain_time
    attr_accessor :tlm_log_cycle_time
    attr_accessor :tlm_log_cycle_size
    attr_accessor :tlm_log_retain_time
    attr_accessor :tlm_decom_log_cycle_time
    attr_accessor :tlm_decom_log_cycle_size
    attr_accessor :tlm_decom_log_retain_time
    attr_accessor :reduced_minute_log_retain_time
    attr_accessor :reduced_hour_log_retain_time
    attr_accessor :reduced_day_log_retain_time
    attr_accessor :cleanup_poll_time
    attr_accessor :needs_dependencies

    # NOTE: The following three class methods are used by the ModelController
    # and are reimplemented to enable various Model class methods to work
    def self.get(name:, scope:)
      super("#{scope}__#{PRIMARY_KEY}", name: name)
    end

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

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

    # @return [Array] Array of all the packet names
    def self.packet_names(target_name, type: :TLM, scope:)
      raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
      # If the key doesn't exist or if there are no packets we return empty array
      Store.hkeys("#{scope}__cosmos#{type.to_s.downcase}__#{target_name}").sort
    end
  
    # @return [Hash] Packet hash or raises an exception
    def self.packet(target_name, packet_name, type: :TLM, scope:)
      raise "Unknown type #{type} for #{target_name} #{packet_name}" unless VALID_TYPES.include?(type)

      # Assume it exists and just try to get it to avoid an extra call to Store.exist?
      json = Store.hget("#{scope}__cosmos#{type.to_s.downcase}__#{target_name}", packet_name)
      raise "Packet '#{target_name} #{packet_name}' does not exist" if json.nil?

      JSON.parse(json)
    end

    # @return [Array>Hash>] All packet hashes under the target_name
    def self.packets(target_name, type: :TLM, scope:)
      raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
      raise "Target '#{target_name}' does not exist" unless get(name: target_name, scope: scope)

      result = []
      packets = Store.hgetall("#{scope}__cosmos#{type.to_s.downcase}__#{target_name}")
      packets.sort.each do |packet_name, packet_json|
        result << JSON.parse(packet_json)
      end
      result
    end

    # @return [Array>Hash>] All packet hashes under the target_name
    def self.all_packet_name_descriptions(target_name, type: :TLM, scope:)
      self.packets(target_name, type: type, scope: scope).map! { |hash| hash.slice("packet_name", "description") }
    end

    def self.set_packet(target_name, packet_name, packet, type: :TLM, scope:)
      raise "Unknown type #{type} for #{target_name} #{packet_name}" unless VALID_TYPES.include?(type)

      begin
        Store.hset("#{scope}__cosmostlm__#{target_name}", packet_name, JSON.generate(packet.as_json))
      rescue JSON::GeneratorError => err
        Logger.error("Invalid text present in #{target_name} #{packet_name} #{type.to_s.downcase} packet")
        raise err
      end
    end

    # @return [Hash] Item hash or raises an exception
    def self.packet_item(target_name, packet_name, item_name, type: :TLM, scope:)
      packet = packet(target_name, packet_name, type: type, scope: scope)
      item = packet['items'].find { |item| item['name'] == item_name.to_s }
      raise "Item '#{packet['target_name']} #{packet['packet_name']} #{item_name}' does not exist" unless item
      item
    end

    # @return [Array<Hash>] Item hash array or raises an exception
    def self.packet_items(target_name, packet_name, items, type: :TLM, scope:)
      packet = packet(target_name, packet_name, type: type, scope: scope)
      found = packet['items'].find_all { |item| items.map(&:to_s).include?(item['name']) }
      if found.length != items.length # we didn't find them all
        found_items = found.collect { |item| item['name'] }
        not_found = []
        (items - found_items).each do |item|
          not_found << "'#{target_name} #{packet_name} #{item}'"
        end
        # 'does not exist' not gramatically correct but we use it in every other exception
        raise "Item(s) #{not_found.join(', ')} does not exist"
      end
      found
    end

    # @return [Hash{String => Array<Array<String, String, String>>}]
    def self.limits_groups(scope:)
      groups = Store.hgetall("#{scope}__limits_groups")
      if groups
        groups.map { |group, items| [group, JSON.parse(items)] }.to_h
      else
        {}
      end
    end

    # Called by the PluginModel to allow this class to validate it's top-level keyword: "TARGET"
    def self.handle_config(parser, keyword, parameters, plugin: nil, needs_dependencies: false, scope:)
      case keyword
      when 'TARGET'
        usage = "#{keyword} <TARGET FOLDER NAME> <TARGET NAME>"
        parser.verify_num_parameters(2, 2, usage)
        parser.verify_parameter_naming(2) # Target name is the 2nd parameter
        return self.new(name: parameters[1].to_s.upcase, folder_name: parameters[0].to_s.upcase, plugin: plugin,  needs_dependencies: needs_dependencies, scope: scope)
      else
        raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Target: #{keyword} #{parameters.join(" ")}")
      end
    end

    def initialize(
      name:,
      folder_name: nil,
      requires: [],
      ignored_parameters: [],
      ignored_items: [],
      limits_groups: [],
      cmd_tlm_files: [],
      cmd_unique_id_mode: false,
      tlm_unique_id_mode: false,
      id: nil,
      updated_at: nil,
      plugin: nil,
      cmd_log_cycle_time: 600,
      cmd_log_cycle_size: 50_000_000,
      cmd_log_retain_time: nil,
      cmd_decom_log_cycle_time: 600,
      cmd_decom_log_cycle_size: 50_000_000,
      cmd_decom_log_retain_time: nil,
      tlm_log_cycle_time: 600,
      tlm_log_cycle_size: 50_000_000,
      tlm_log_retain_time: nil,
      tlm_decom_log_cycle_time: 600,
      tlm_decom_log_cycle_size: 50_000_000,
      tlm_decom_log_retain_time: nil,
      reduced_minute_log_retain_time: nil,
      reduced_hour_log_retain_time: nil,
      reduced_day_log_retain_time: nil,
      cleanup_poll_time: 900,
      needs_dependencies: false,
      scope:
    )
      super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at,
        cmd_log_cycle_time: cmd_log_cycle_time, cmd_log_cycle_size: cmd_log_cycle_size,
        cmd_log_retain_time: cmd_log_retain_time,
        cmd_decom_log_cycle_time: cmd_decom_log_cycle_time, cmd_decom_log_cycle_size: cmd_decom_log_cycle_size,
        cmd_decom_log_retain_time: cmd_decom_log_retain_time,
        tlm_log_cycle_time: tlm_log_cycle_time, tlm_log_cycle_size: tlm_log_cycle_size,
        tlm_log_retain_time: tlm_log_retain_time,
        tlm_decom_log_cycle_time: tlm_decom_log_cycle_time, tlm_decom_log_cycle_size: tlm_decom_log_cycle_size,
        tlm_decom_log_retain_time: tlm_decom_log_retain_time,
        reduced_minute_log_retain_time: reduced_minute_log_retain_time,
        reduced_hour_log_retain_time: reduced_hour_log_retain_time, reduced_day_log_retain_time: reduced_day_log_retain_time,
        cleanup_poll_time: cleanup_poll_time, needs_dependencies: needs_dependencies,
        scope: scope)
      @folder_name = folder_name
      @requires = requires
      @ignored_parameters = ignored_parameters
      @ignored_items = ignored_items
      @limits_groups = limits_groups
      @cmd_tlm_files = cmd_tlm_files
      @cmd_unique_id_mode = cmd_unique_id_mode
      @tlm_unique_id_mode = tlm_unique_id_mode
      @id = id
      @cmd_log_cycle_time = cmd_log_cycle_time
      @cmd_log_cycle_size = cmd_log_cycle_size
      @cmd_log_retain_time = cmd_log_retain_time
      @cmd_decom_log_cycle_time = cmd_decom_log_cycle_time
      @cmd_decom_log_cycle_size = cmd_decom_log_cycle_size
      @cmd_decom_log_retain_time = cmd_decom_log_retain_time
      @tlm_log_cycle_time = tlm_log_cycle_time
      @tlm_log_cycle_size = tlm_log_cycle_size
      @tlm_log_retain_time = tlm_log_retain_time
      @tlm_decom_log_cycle_time = tlm_decom_log_cycle_time
      @tlm_decom_log_cycle_size = tlm_decom_log_cycle_size
      @tlm_decom_log_retain_time = tlm_decom_log_retain_time
      @reduced_minute_log_retain_time = reduced_minute_log_retain_time
      @reduced_hour_log_retain_time = reduced_hour_log_retain_time
      @reduced_day_log_retain_time = reduced_day_log_retain_time
      @cleanup_poll_time = cleanup_poll_time
      @needs_dependencies = needs_dependencies
    end

    def as_json
      {
        'name' => @name,
        'folder_name' => @folder_name,
        'requires' => @requires,
        'ignored_parameters' => @ignored_parameters,
        'ignored_items' => @ignored_items,
        'limits_groups' => @limits_groups,
        'cmd_tlm_files' => @cmd_tlm_files,
        'cmd_unique_id_mode' => cmd_unique_id_mode,
        'tlm_unique_id_mode' => @tlm_unique_id_mode,
        'id' => @id,
        'updated_at' => @updated_at,
        'plugin' => @plugin,
        'cmd_log_cycle_time' => @cmd_log_cycle_time,
        'cmd_log_cycle_size' => @cmd_log_cycle_size,
        'cmd_log_retain_time' => @cmd_log_retain_time,
        'cmd_decom_log_cycle_time' => @cmd_decom_log_cycle_time,
        'cmd_decom_log_cycle_size' => @cmd_decom_log_cycle_size,
        'cmd_decom_log_retain_time' => @cmd_decom_log_retain_time,
        'tlm_log_cycle_time' => @tlm_log_cycle_time,
        'tlm_log_cycle_size' => @tlm_log_cycle_size,
        'tlm_log_retain_time' => @tlm_log_retain_time,
        'tlm_decom_log_cycle_time' => @tlm_decom_log_cycle_time,
        'tlm_decom_log_cycle_size' => @tlm_decom_log_cycle_size,
        'tlm_decom_log_retain_time' => @tlm_decom_log_retain_time,
        'reduced_minute_log_retain_time' => @reduced_minute_log_retain_time,
        'reduced_hour_log_retain_time' => @reduced_hour_log_retain_time,
        'reduced_day_log_retain_time' => @reduced_day_log_retain_time,
        'cleanup_poll_time' => @cleanup_poll_time,
        'needs_dependencies' => @needs_dependencies,
      }
    end

    def as_config
      "TARGET #{@folder_name} #{@name}\n"
    end

    # Handles Target specific configuration keywords
    def handle_config(parser, keyword, parameters)
      case keyword
      when 'CMD_LOG_CYCLE_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum time between files in seconds>")
        @cmd_log_cycle_time = parameters[0].to_i
      when 'CMD_LOG_CYCLE_SIZE'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum file size in bytes>")
        @cmd_log_cycle_size = parameters[0].to_i
      when 'CMD_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for cmd log files in seconds - nil = Forever>")
        @cmd_log_retain_time = ConfigParser.handle_nil(parameters[0])
        @cmd_log_retain_time = @cmd_log_retain_time.to_i if @cmd_log_retain_time
      when 'CMD_DECOM_LOG_CYCLE_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum time between files in seconds>")
        @cmd_decom_log_cycle_time = parameters[0].to_i
      when 'CMD_DECOM_LOG_CYCLE_SIZE'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum file size in bytes>")
        @cmd_decom_log_cycle_size = parameters[0].to_i
      when 'CMD_DECOM_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for cmd decom log files in seconds - nil = Forever>")
        @cmd_decom_log_retain_time = ConfigParser.handle_nil(parameters[0])
        @cmd_decom_log_retain_time = @cmd_decom_log_retain_time.to_i if @cmd_decom_log_retain_time
      when 'TLM_LOG_CYCLE_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum time between files in seconds>")
        @tlm_log_cycle_time = parameters[0].to_i
      when 'TLM_LOG_CYCLE_SIZE'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum file size in bytes>")
        @tlm_log_cycle_size = parameters[0].to_i
      when 'TLM_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for tlm log files in seconds - nil = Forever>")
        @tlm_log_retain_time = ConfigParser.handle_nil(parameters[0])
        @tlm_log_retain_time = @tlm_log_retain_time.to_i if @tlm_log_retain_time
      when 'TLM_DECOM_LOG_CYCLE_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum time between files in seconds>")
        @tlm_decom_log_cycle_time = parameters[0].to_i
      when 'TLM_DECOM_LOG_CYCLE_SIZE'
        parser.verify_num_parameters(1, 1, "#{keyword} <Maximum file size in bytes>")
        @tlm_decom_log_cycle_size = parameters[0].to_i
      when 'TLM_DECOM_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for tlm decom log files in seconds - nil = Forever>")
        @tlm_decom_log_retain_time = ConfigParser.handle_nil(parameters[0])
        @tlm_decom_log_retain_time = @tlm_decom_log_retain_time.to_i if @tlm_decom_log_retain_time
      when 'REDUCED_MINUTE_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for reduced minute log files in seconds - nil = Forever>")
        @reduced_minute_log_retain_time = ConfigParser.handle_nil(parameters[0])
        @reduced_minute_log_retain_time = @reduced_minute_log_retain_time.to_i if @reduced_minute_log_retain_time
      when 'REDUCED_HOUR_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for reduced hour log files in seconds - nil = Forever>")
        @reduced_hour_log_retain_time = ConfigParser.handle_nil(parameters[0])
        @reduced_hour_log_retain_time = @reduced_hour_log_retain_time.to_i if @reduced_hour_log_retain_time
      when 'REDUCED_DAY_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for reduced day log files in seconds - nil = Forever>")
        @reduced_day_log_retain_time = ConfigParser.handle_nil(parameters[0])
        @reduced_day_log_retain_time = @reduced_day_log_retain_time.to_i if @reduced_day_log_retain_time
      when 'LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for all log files in seconds - nil = Forever>")
        log_retain_time = ConfigParser.handle_nil(parameters[0])
        if log_retain_time
          @cmd_log_retain_time = log_retain_time.to_i
          @cmd_decom_log_retain_time = log_retain_time.to_i
          @tlm_log_retain_time = log_retain_time.to_i
          @tlm_decom_log_retain_time = log_retain_time.to_i
        end
      when 'REDUCED_LOG_RETAIN_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Retention time for all reduced log files in seconds - nil = Forever>")
        reduced_log_retain_time = ConfigParser.handle_nil(parameters[0])
        if reduced_log_retain_time
          @reduced_minute_log_retain_time = reduced_log_retain_time.to_i
          @reduced_hour_log_retain_time = reduced_log_retain_time.to_i
          @reduced_day_log_retain_time = reduced_log_retain_time.to_i
        end
      when 'CLEANUP_POLL_TIME'
        parser.verify_num_parameters(1, 1, "#{keyword} <Cleanup polling period in seconds>")
        @cleanup_poll_time = parameters[0].to_i
      else
        raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Target: #{keyword} #{parameters.join(" ")}")
      end
      return nil
    end

    def deploy(gem_path, variables, validate_only: false)
      rubys3_client = Aws::S3::Client.new
      variables["target_name"] = @name
      start_path = "/targets/#{@folder_name}/"
      temp_dir = Dir.mktmpdir
      found = false
      begin
        target_path = gem_path + start_path + "**/*"
        Dir.glob(target_path) do |filename|
          next if filename == '.' or filename == '..' or File.directory?(filename)

          path = filename.split(gem_path)[-1]
          target_folder_path = path.split(start_path)[-1]
          key = "#{@scope}/targets/#{@name}/#{target_folder_path}"

          # Load target files
          @filename = filename # For render
          data = File.read(filename, mode: "rb")
          begin
            Cosmos.set_working_dir(File.dirname(filename)) do
              data = ERB.new(data, trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable? and File.basename(filename)[0] != '_'
            end
          rescue => error
            raise "ERB error parsing: #{filename}: #{error.formatted}"
          end
          local_path = File.join(temp_dir, @name, target_folder_path)
          FileUtils.mkdir_p(File.dirname(local_path))
          File.open(local_path, 'wb') { |file| file.write(data) }
          found = true
          rubys3_client.put_object(bucket: 'config', key: key, body: data) unless validate_only
        end
        raise "No target files found at #{target_path}" unless found

        target_folder = File.join(temp_dir, @name)
        # Build a System for just this target
        system = System.new([@name], temp_dir)
        if variables["xtce_output"]
          puts "Converting target #{@name} to .xtce files in #{variables["xtce_output"]}/#{@name}"
          system.packet_config.to_xtce(variables["xtce_output"])
        end
        unless validate_only
          build_target_archive(rubys3_client, temp_dir, target_folder)
          system = update_store(system)
          deploy_microservices(gem_path, variables, system)
          ConfigTopic.write({ kind: 'created', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
        end
      ensure
        FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
      end
    end

    def undeploy
      rubys3_client = Aws::S3::Client.new
      prefix = "#{@scope}/targets/#{@name}/"
      rubys3_client.list_objects(bucket: 'config', prefix: prefix).contents.each do |object|
        rubys3_client.delete_object(bucket: 'config', key: object.key)
      end

      self.class.get_model(name: @name, scope: @scope).limits_groups.each do |group|
        Store.hdel("#{@scope}__limits_groups", group)
      end
      self.class.packets(@name, type: :CMD, scope: @scope).each do |packet|
        Topic.del("#{@scope}__COMMAND__{#{@name}}__#{packet['packet_name']}")
        Topic.del("#{@scope}__DECOMCMD__{#{@name}}__#{packet['packet_name']}")
      end
      self.class.packets(@name, scope: @scope).each do |packet|
        Topic.del("#{@scope}__TELEMETRY__{#{@name}}__#{packet['packet_name']}")
        Topic.del("#{@scope}__DECOM__{#{@name}}__#{packet['packet_name']}")
        CvtModel.del(target_name: @name, packet_name: packet['packet_name'], scope: @scope)
        LimitsEventTopic.delete(@name, packet['packet_name'], scope: @scope)
      end
      Store.del("#{@scope}__cosmostlm__#{@name}")
      Store.del("#{@scope}__cosmoscmd__#{@name}")

      # Note: these match the names of the services in deploy_microservices
      %w(DECOM COMMANDLOG DECOMCMDLOG PACKETLOG DECOMLOG REDUCER CLEANUP).each do |type|
        model = MicroserviceModel.get_model(name: "#{@scope}__#{type}__#{@name}", scope: @scope)
        model.destroy if model
      end
      ConfigTopic.write({ kind: 'deleted', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
    end

    ##################################################
    # The following methods are implementation details
    ##################################################

    # Called by the ERB template to render a partial
    def render(template_name, options = {})
      raise "Partial name '#{template_name}' must begin with an underscore." if File.basename(template_name)[0] != '_'

      b = binding
      b.local_variable_set(:target_name, @name)
      if options[:locals]
        options[:locals].each { |key, value| b.local_variable_set(key, value) }
      end

      # Assume the file is there. If not we raise a pretty obvious error
      if File.expand_path(template_name) == template_name # absolute path
        path = template_name
      else # relative to the current @filename
        path = File.join(File.dirname(@filename), template_name)
      end

      begin
        Cosmos.set_working_dir(File.dirname(path)) do
          return ERB.new(File.read(path), trim_mode: "-").result(b)
        end
      rescue => error
        raise "ERB error parsing: #{path}: #{error.formatted}"
      end
    end

    def build_target_archive(rubys3_client, temp_dir, target_folder)
      target_files = []
      Find.find(target_folder) { |file| target_files << file }
      target_files.sort!
      hash = Cosmos.hash_files(target_files, nil, 'SHA256').hexdigest
      File.open(File.join(target_folder, 'target_id.txt'), 'wb') { |file| file.write(hash) }
      key = "#{@scope}/targets/#{@name}/target_id.txt"
      rubys3_client.put_object(bucket: 'config', key: key, body: hash)

      # Create target archive zip file
      prefix = File.dirname(target_folder) + '/'
      output_file = File.join(temp_dir, @name + '_' + hash + '.zip')
      Zip.continue_on_exists_proc = true
      Zip::File.open(output_file, Zip::File::CREATE) do |zipfile|
        target_files.each do |target_file|
          zip_file_path = target_file.delete_prefix(prefix)
          if File.directory?(target_file)
            zipfile.mkdir(zip_file_path)
          else
            zipfile.add(zip_file_path, target_file)
          end
        end
      end

      # Write Target Archive to S3 Bucket
      File.open(output_file, 'rb') do |file|
        s3_key = key = "#{@scope}/target_archives/#{@name}/#{@name}_current.zip"
        rubys3_client.put_object(bucket: 'config', key: s3_key, body: file)
      end
      File.open(output_file, 'rb') do |file|
        s3_key = key = "#{@scope}/target_archives/#{@name}/#{@name}_#{hash}.zip"
        rubys3_client.put_object(bucket: 'config', key: s3_key, body: file)
      end
    end

    def update_store(system)
      target = system.targets[@name]

      # Add in the information from the target and update
      @requires = target.requires
      @ignored_parameters = target.ignored_parameters
      @ignored_items = target.ignored_items
      @cmd_tlm_files = target.cmd_tlm_files
      @cmd_unique_id_mode = target.cmd_unique_id_mode
      @tlm_unique_id_mode = target.tlm_unique_id_mode
      @id = target.id
      @limits_groups = system.limits.groups.keys
      update()

      # Store Packet Definitions
      system.telemetry.all.each do |target_name, packets|
        Store.del("#{@scope}__cosmostlm__#{target_name}")
        packets.each do |packet_name, packet|
          Logger.info "Configuring tlm packet: #{target_name} #{packet_name}"
          begin
            Store.hset("#{@scope}__cosmostlm__#{target_name}", packet_name, JSON.generate(packet.as_json))
          rescue JSON::GeneratorError => err
            Logger.error("Invalid text present in #{target_name} #{packet_name} tlm packet")
            raise err
          end
          json_hash = Hash.new
          packet.sorted_items.each do |item|
            json_hash[item.name] = nil
          end
          CvtModel.set(json_hash, target_name: packet.target_name, packet_name: packet.packet_name, scope: @scope)
        end
      end
      system.commands.all.each do |target_name, packets|
        Store.del("#{@scope}__cosmoscmd__#{target_name}")
        packets.each do |packet_name, packet|
          Logger.info "Configuring cmd packet: #{target_name} #{packet_name}"
          begin
            Store.hset("#{@scope}__cosmoscmd__#{target_name}", packet_name, JSON.generate(packet.as_json))
          rescue JSON::GeneratorError => err
            Logger.error("Invalid text present in #{target_name} #{packet_name} cmd packet")
            raise err
          end
        end
      end
      # Store Limits Groups
      system.limits.groups.each do |group, items|
        begin
          Store.hset("#{@scope}__limits_groups", group, JSON.generate(items))
        rescue JSON::GeneratorError => err
          Logger.error("Invalid text present in #{group} limits group")
          raise err
        end
      end
      # Merge in Limits Sets
      sets = Store.hgetall("#{@scope}__limits_sets")
      sets ||= {}
      system.limits.sets.each do |set|
        sets[set.to_s] = "false" unless sets.key?(set.to_s)
      end
      Store.hmset("#{@scope}__limits_sets", *sets)

      return system
    end

    def deploy_microservices(gem_path, variables, system)
      command_topic_list = []
      decom_command_topic_list = []
      packet_topic_list = []
      decom_topic_list = []
      begin
        system.commands.packets(@name).each do |packet_name, packet|
          command_topic_list << "#{@scope}__COMMAND__{#{@name}}__#{packet_name}"
          decom_command_topic_list << "#{@scope}__DECOMCMD__{#{@name}}__#{packet_name}"
        end
      rescue
        # No command packets for this target
      end
      begin
        system.telemetry.packets(@name).each do |packet_name, packet|
          packet_topic_list << "#{@scope}__TELEMETRY__{#{@name}}__#{packet_name}"
          decom_topic_list  << "#{@scope}__DECOM__{#{@name}}__#{packet_name}"
        end
      rescue
        # No telemetry packets for this target
      end
      # It's ok to call initialize_streams with an empty array
      Topic.initialize_streams(command_topic_list)
      Topic.initialize_streams(decom_command_topic_list)
      Topic.initialize_streams(packet_topic_list)
      Topic.initialize_streams(decom_topic_list)

      unless command_topic_list.empty?
        # CommandLog Microservice
        microservice_name = "#{@scope}__COMMANDLOG__#{@name}"
        microservice = MicroserviceModel.new(
          name: microservice_name,
          folder_name: @folder_name,
          cmd: ["ruby", "log_microservice.rb", microservice_name],
          work_dir: '/cosmos/lib/cosmos/microservices',
          options: [
            ["RAW_OR_DECOM", "RAW"],
            ["CMD_OR_TLM", "CMD"],
            ["CYCLE_TIME", @cmd_log_cycle_time],
            ["CYCLE_SIZE", @cmd_log_cycle_size]
          ],
          topics: command_topic_list,
          target_names: [@name],
          plugin: @plugin,
          needs_dependencies: @needs_dependencies,
          scope: @scope
        )
        microservice.create
        microservice.deploy(gem_path, variables)
        Logger.info "Configured microservice #{microservice_name}"

        # DecomCmdLog Microservice
        microservice_name = "#{@scope}__DECOMCMDLOG__#{@name}"
        microservice = MicroserviceModel.new(
          name: microservice_name,
          folder_name: @folder_name,
          cmd: ["ruby", "log_microservice.rb", microservice_name],
          work_dir: '/cosmos/lib/cosmos/microservices',
          options: [
            ["RAW_OR_DECOM", "DECOM"],
            ["CMD_OR_TLM", "CMD"],
            ["CYCLE_TIME", @cmd_decom_log_cycle_time],
            ["CYCLE_SIZE", @cmd_decom_log_cycle_size]
          ],
          topics: decom_command_topic_list,
          target_names: [@name],
          plugin: @plugin,
          needs_dependencies: @needs_dependencies,
          scope: @scope
        )
        microservice.create
        microservice.deploy(gem_path, variables)
        Logger.info "Configured microservice #{microservice_name}"
      end

      unless packet_topic_list.empty?
        # PacketLog Microservice
        microservice_name = "#{@scope}__PACKETLOG__#{@name}"
        microservice = MicroserviceModel.new(
          name: microservice_name,
          folder_name: @folder_name,
          cmd: ["ruby", "log_microservice.rb", microservice_name],
          work_dir: '/cosmos/lib/cosmos/microservices',
          options: [
            ["RAW_OR_DECOM", "RAW"],
            ["CMD_OR_TLM", "TLM"],
            ["CYCLE_TIME", @tlm_log_cycle_time],
            ["CYCLE_SIZE", @tlm_log_cycle_size]
          ],
          topics: packet_topic_list,
          target_names: [@name],
          plugin: @plugin,
          needs_dependencies: @needs_dependencies,
          scope: @scope
        )
        microservice.create
        microservice.deploy(gem_path, variables)
        Logger.info "Configured microservice #{microservice_name}"

        # DecomLog Microservice
        microservice_name = "#{@scope}__DECOMLOG__#{@name}"
        microservice = MicroserviceModel.new(
          name: microservice_name,
          folder_name: @folder_name,
          cmd: ["ruby", "log_microservice.rb", microservice_name],
          work_dir: '/cosmos/lib/cosmos/microservices',
          options: [
            ["RAW_OR_DECOM", "DECOM"],
            ["CMD_OR_TLM", "TLM"],
            ["CYCLE_TIME", @tlm_decom_log_cycle_time],
            ["CYCLE_SIZE", @tlm_decom_log_cycle_size]
          ],
          topics: decom_topic_list,
          target_names: [@name],
          plugin: @plugin,
          needs_dependencies: @needs_dependencies,
          scope: @scope
        )
        microservice.create
        microservice.deploy(gem_path, variables)
        Logger.info "Configured microservice #{microservice_name}"

        # Decommutation Microservice
        microservice_name = "#{@scope}__DECOM__#{@name}"
        microservice = MicroserviceModel.new(
          name: microservice_name,
          folder_name: @folder_name,
          cmd: ["ruby", "decom_microservice.rb", microservice_name],
          work_dir: '/cosmos/lib/cosmos/microservices',
          topics: packet_topic_list,
          target_names: [@name],
          plugin: @plugin,
          needs_dependencies: @needs_dependencies,
          scope: @scope
        )
        microservice.create
        microservice.deploy(gem_path, variables)
        Logger.info "Configured microservice #{microservice_name}"

        # Reducer Microservice
        microservice_name = "#{@scope}__REDUCER__#{@name}"
        microservice = MicroserviceModel.new(
          name: microservice_name,
          folder_name: @folder_name,
          cmd: ["ruby", "reducer_microservice.rb", microservice_name],
          work_dir: '/cosmos/lib/cosmos/microservices',
          topics: decom_topic_list,
          plugin: @plugin,
          needs_dependencies: @needs_dependencies,
          scope: @scope
        )
        microservice.create
        microservice.deploy(gem_path, variables)
        Logger.info "Configured microservice #{microservice_name}"
      end

      if @cmd_log_retain_time or @cmd_decom_log_retain_time or @tlm_log_retain_time or @tlm_decom_log_retain_time or
         @reduced_minute_log_retain_time or @reduced_hour_log_retain_time or @reduced_day_log_retain_time
        # Cleanup Microservice
        microservice_name = "#{@scope}__CLEANUP__#{@name}"
        microservice = MicroserviceModel.new(
          name: microservice_name,
          cmd: ["ruby", "cleanup_microservice.rb", microservice_name],
          work_dir: '/cosmos/lib/cosmos/microservices',
          plugin: @plugin,
          scope: @scope
        )
        microservice.create
        microservice.deploy(gem_path, variables)
        Logger.info "Configured microservice #{microservice_name}"
      end
    end
  end
end