Ravn::Tactical::

Mission class

An entity class for missions in Ravn::Tactical applications. It knows how to read and write valid mission config files from Ravn::BDE’s mission directory.

Constants

ARCHIVE_DIRNAME

The name of the subdirectory of the mission directory that stores archives until they can be offloaded.

DEFAULT_BOLTS

Bolts that are added to missions by default

DEFAULT_FORMAT_VERSION

Use the format version that is implemented by the currently-loaded BDE library

DEFAULT_NAME

The default name used for new missions

DEFAULT_VALUES

Defaults to use in the constructor

VALID_INDEX_ATTRIBUTES

Valid arguments to the index operators that translate into attribute methods.

VERSION_PATTERN

The strftime pattern used to create/update a config version

Attributes

bolts R

The mission’s bolts as Ravn::Tactical::Bolt objects in a Hash keyed by ID

callsigns R

The mission’s callsigns in a Hash keyed by the device ID of their compute pack.

format_version RW

The mission’s format version number

id RW

The mission’s id (as a String)

name RW

The mission’s name

pois R

The Array of POIs that the mission starts up with

segments R

The mission’s segments in a Hash keyed by name

timestamp RW

The timestamp when the mission was last modified

version RW

The mission’s version (as a String)

Public Class Methods

all()

Return an enumerator that yields Ravn::Tactical::Mission objects for each mission config file found in the current mission directory.

    # File lib/ravn/tactical/mission.rb
192 def self::all
193     paths = Pathname.glob( Ravn::BDE.mission_directory + '*.yml' )
194     return Enumerator.new do |yielder|
195         paths.each do |path|
196             next if path.basename == Ravn::BDE.mission_config_filename ||
197                 path.basename == Ravn::Tactical::BoltPackage::FILENAME
198             object = self.load( path )
199             yielder.yield( object ) if object
200         end
201     end
202 end
all_templates()

Return an enumerator that yields Ravn::Tactical::Mission objects for each mission template found in the current data directory.

    # File lib/ravn/tactical/mission.rb
207 def self::all_templates
208     names = self.available_templates
209     return Enumerator.new do |yielder|
210         names.each do |name|
211             object = self.fetch_template( name )
212             yielder.yield( object )
213         end
214     end
215 end
archive_directory()

Return the Pathname to the currently-configured archive directory.

    # File lib/ravn/tactical/mission.rb
219 def self::archive_directory
220     return Ravn::BDE.mission_directory / ARCHIVE_DIRNAME
221 end
available_templates()

Return an Array of the names of all mission templates in this release.

    # File lib/ravn/tactical/mission.rb
181 def self::available_templates
182     template_dir = Ravn::Tactical.data_dir / 'missions'
183     mission_files = Pathname.glob( template_dir / '*.yml' )
184     return mission_files.map do |file|
185         file.basename( '.yml' ).to_s.to_sym
186     end
187 end
create( name, **values )

Create a new Mission config from the given values or reasonable defaults.

   # File lib/ravn/tactical/mission.rb
71 def self::create( name, **values )
72     values[:name]           = name
73 
74     values[:id]             ||= Ravn.uuid.generate
75     values[:format_version] ||= DEFAULT_FORMAT_VERSION
76     values[:bolts]          ||= DEFAULT_BOLTS.dup
77     values[:callsigns]      ||= {}
78     values[:segments]       ||= Ravn::Tactical::BoltPackage.load.role_segments_hash
79     values[:pois]           ||= []
80 
81     return new( **values )
82 end
create_with_serial_name( name=DEFAULT_NAME )

Create a new mission with the default name, incrementing an appended serial number if there is already a mission with the default name.

   # File lib/ravn/tactical/mission.rb
87 def self::create_with_serial_name( name=DEFAULT_NAME )
88     name = self.next_variant_for_name( name )
89     return self.create( name )
90 end
current()

Return the current mission config as a Ravn::Tactical::Mission.

    # File lib/ravn/tactical/mission.rb
118 def self::current
119     self.log.info "Loading current mission as a %p" % [ self ]
120     return nil unless Ravn::BDE.mission_config_path.exist?
121 
122     values = Ravn::BDE.load_mission_config or return nil
123 
124     return new( **values )
125 end
fetch( uuid )

Fetch a mission by its UUID (id). Returns nil if a mission file with the specified uuid doesn’t exist.

    # File lib/ravn/tactical/mission.rb
141 def self::fetch( uuid )
142     path = Ravn::BDE.mission_directory + "%s.yml" % [ uuid ]
143 
144     return nil unless path.file?
145     return self.load( path )
146 end
fetch_or_create( uuid, **values )

Fetch a mission bu its UUID (id), creating and returning a new one if it doesn’t exist. If it exists, it will be updated with the specified values, but not saved.

    # File lib/ravn/tactical/mission.rb
169 def self::fetch_or_create( uuid, **values )
170     if (instance = self.fetch( uuid ))
171         instance.update( **values )
172     else
173         instance = self.new( id: uuid, **values )
174     end
175 
176     return instance
177 end
fetch_template( raw_name )

Fetch a template mission by its name. The new instance will be disassociated from the template file, so you can safely write it, and it will be added to the mission directory.

    # File lib/ravn/tactical/mission.rb
152 def self::fetch_template( raw_name )
153     name = raw_name[ /\A(\w+)\z/, 1 ] or raise "invalid template name %p" % [ raw_name ]
154     file = "%s.yml" % [ name ]
155     path = Ravn::Tactical.data_dir / 'missions' / file
156 
157     mission = self.load( path ) or return nil
158 
159     mission.id = Ravn.uuid.generate
160     mission.version = self.make_timestamp_version
161 
162     return mission
163 end
filename_for( id )

Return the filename that corresponds to a mission with the given id.

    # File lib/ravn/tactical/mission.rb
225 def self::filename_for( id )
226     return "%s.yml" % [ id ]
227 end
latest_name_variant( name )

Return the latest serialized variant of the given name in existing missions.

    # File lib/ravn/tactical/mission.rb
105 def self::latest_name_variant( name )
106     pattern = /\A#{Regexp.escape(name)}( \d+)?\z/
107 
108     return self.all.map( &:name ).
109         select do |mission_name|
110             mission_name.match?( pattern )
111         end.
112         sort_by {|mission_name| mission_name[/ (\d+)\z/, 1]&.to_i || 0 }.
113         last
114 end
load( path )

Load the mission config file at the given path and return it as a Ravn::Tactical::Mission.

    # File lib/ravn/tactical/mission.rb
130 def self::load( path )
131     values = Ravn::BDE.read_mission_config_file( path )
132     return new( **values )
133 rescue => err
134     self.log.error "%p while loading %s: %s" % [ err.class, path, err.message ]
135     return nil
136 end
make_timestamp_version()

Generate a version out of a timestamp and return it.

    # File lib/ravn/tactical/mission.rb
231 def self::make_timestamp_version
232     return Time.now.strftime( VERSION_PATTERN )
233 end
new( id:, name:, **kwargs )

Create a new instance of a Mission config using the given id, name, and bolts config Hash.

    # File lib/ravn/tactical/mission.rb
242 def initialize( id:, name:, **kwargs )
243     kwargs = DEFAULT_VALUES.merge( kwargs )
244 
245     @id             = id
246     @name           = name
247 
248     @format_version = kwargs[ :format_version ]
249     @version        = kwargs[ :version ] || self.class.make_timestamp_version
250     @timestamp      = kwargs[ :timestamp ]
251 
252     # Use the accessors for normalization/validation
253     self.bolts      = kwargs[ :bolts ]
254     self.callsigns  = kwargs[ :callsigns ]
255     self.segments   = kwargs[ :segments ]
256     self.pois       = kwargs[ :pois ]
257 end
next_variant_for_name( name=DEFAULT_NAME )

Return the “next” variant of the specified name, e.g., for “New Mission” it might return “New Mission 4” if there’s already a “New Mission 3”.

    # File lib/ravn/tactical/mission.rb
 95 def self::next_variant_for_name( name=DEFAULT_NAME )
 96     current_variant = self.latest_name_variant( name ) or
 97         return name
 98 
 99     current_variant += ' 1' unless current_variant.match?( / \d+\z/ )
100     return current_variant.succ
101 end

Public Instance Methods

==( other )

Returns true if the receiver and the other object are equivalent configurations.

    # File lib/ravn/tactical/mission.rb
728 def ==( other )
729     return other.is_a?( self.class ) &&
730         other.to_h == self.to_h
731 end
[]( key )

Index operator; look up the mission attribute that corresponds to key and return it.

    # File lib/ravn/tactical/mission.rb
395 def []( key )
396     key = key.to_sym
397     return nil unless VALID_INDEX_ATTRIBUTES.include?( key )
398 
399     return self.public_send( key )
400 end
all_nodes_archived?()

Return true if all device archive directories contain files.

    # File lib/ravn/tactical/mission.rb
674 def all_nodes_archived?
675     return self.kit_directory_paths.none?( &:empty? )
676 end
archive()

Archive the mission.

    # File lib/ravn/tactical/mission.rb
556 def archive
557     raise "this is not the current mission" unless self.current?
558 
559     self.log.info "archiving mission %s" % [ self.id ]
560     self.unmake_current
561     self.archive_bde_state
562 
563     if Ravn::Tactical.is_control_node?
564         self.log.info "making archive directories for %d nodes" % [ self.callsigns.size ]
565         self.make_kit_directories
566     else
567         self.log.debug "skipping peer node directories: not a control"
568     end
569 end
archive_bde_state()

Make archival links for the current BDE logs/databases.

    # File lib/ravn/tactical/mission.rb
690 def archive_bde_state
691     self.log.info "Archiving current BDE state"
692     archive_dir = self.device_archive_directory_path
693     archive_dir.mkpath
694     link_file = archive_dir / Ravn::BDE.history_db_path.basename
695 
696     if Ravn::BDE.history_db_path.exist?
697         if link_file.exist?
698             self.log.warn "  linked history DB already exists!"
699         else
700             self.log.info "  linking history DB to %s" % [ link_file ]
701             link_file.make_link( Ravn::BDE.history_db_path )
702         end
703     else
704         self.log.info "  no history database; making an empty one in the archive"
705         link_file.write( '' )
706     end
707 end
archive_directory_path()

Return a Pathname to the mission’s archive directory, which exists only if it’s been archived or is in the process of being archived (on the Control).

    # File lib/ravn/tactical/mission.rb
603 def archive_directory_path
604     return self.class.archive_directory / self.id
605 end
archive_filepath()

Return the Pathname of the Mission file when it’s been archived.

    # File lib/ravn/tactical/mission.rb
596 def archive_filepath
597     return self.archive_directory_path / 'mission.yml'
598 end
archive_files()

Return an Array of Pathnames of all of this mission’s archive files. This will be the same as device_archive_files unless the host is a Control, or empty if the mission hasn’t yet been archived.

    # File lib/ravn/tactical/mission.rb
713 def archive_files
714     return Pathname.glob( self.archive_directory_path / '**/*' )
715 end
archived?()

Returns true if the mission archive is complete.

    # File lib/ravn/tactical/mission.rb
588 def archived?
589     return Ravn::Tactical.is_control_node? ?
590         self.archive_filepath.exist? :
591         self.archive_directory_path.exist?
592 end
archiving?()

Returns true if the mission archive has been set up but not completed.

    # File lib/ravn/tactical/mission.rb
582 def archiving?
583     return !self.archived? && self.archive_directory_path.exist?
584 end
bolts=( new_bolts )

Replace the mission’s existing bolts with the specified new_bolts.

    # File lib/ravn/tactical/mission.rb
346 def bolts=( new_bolts )
347     bolt_hash = case new_bolts
348         when Array
349             bolts_hash_from_array( new_bolts )
350         when Hash
351             bolts_hash_from_hash( new_bolts )
352         else
353             raise "can't set bolts from %p" % [ new_bolts ]
354         end
355 
356     @bolts = bolt_hash
357 end
bolts_hash_from_array( bolts )

Return a Hash of Bolts keyed by ID from the given Array of bolts hashes.

    # File lib/ravn/tactical/mission.rb
361 def bolts_hash_from_array( bolts )
362     return bolts.each_with_object( {} ) do |raw_hash, bolts_hash|
363         bolt = Ravn::Tactical::Bolt.new( **raw_hash )
364         bolts_hash[ bolt.id ] = bolt
365     end
366 end
bolts_hash_from_hash( bolts )

Return a Hash of Bolts keyed by ID from the given Hash of bolts hashes.

    # File lib/ravn/tactical/mission.rb
370 def bolts_hash_from_hash( bolts )
371     return bolts.each_with_object( {} ) do |(id, raw_hash), bolts_hash|
372         bolt = case raw_hash
373             when Ravn::Tactical::Bolt
374                 raw_hash
375             else
376                 raw_hash[ :id ] = id
377                 Ravn::Tactical::Bolt.new( **raw_hash )
378         end
379 
380         bolts_hash[ id ] = bolt
381     end
382 end
callsigns=( new_callsigns )

Replace the mission’s existing callsigns hash with the specified new_callsigns.

    # File lib/ravn/tactical/mission.rb
323 def callsigns=( new_callsigns )
324     new_callsigns ||= {}
325     @callsigns = new_callsigns.transform_keys( &:to_s )
326 end
checksum()
Alias for: chksum
chksum()

Return a checksum of the mission id + version for fast comparison.

    # File lib/ravn/tactical/mission.rb
316 def chksum
317     return Digest::SHA256.hexdigest( self.id + self.version )[0,8]
318 end
Also aliased as: checksum
current?()

Returns true if this mission is configured as the current one.

    # File lib/ravn/tactical/mission.rb
514 def current?
515     config_path = Ravn::BDE.mission_config_path
516     return config_path.symlink? &&
517         config_path.realdirpath == self.filepath.realdirpath
518 end
device_archive_directory_for( device_id )

Return a Pathname to the archive directory of the device with the specified device_id under the mission archive directory.

    # File lib/ravn/tactical/mission.rb
610 def device_archive_directory_for( device_id )
611     return self.archive_directory_path / device_id.to_s
612 end
device_archive_directory_path()

Return a Pathname to the compute pack’s archive directory under the mission archive directory.

    # File lib/ravn/tactical/mission.rb
617 def device_archive_directory_path
618     return self.device_archive_directory_for( Ravn.device_id )
619 end
device_archive_files()

Return an Array of Pathnames of all of this mission’s archive files for the local host. This Array will be empty if the mission hasn’t yet been archived.

    # File lib/ravn/tactical/mission.rb
721 def device_archive_files
722     return Pathname.glob( self.device_archive_directory_path / '**/*' )
723 end
fields()

Return the mission config as a Hash.

    # File lib/ravn/tactical/mission.rb
438 def fields
439     return {
440         id: self.id,
441         name: self.name,
442         version: self.version,
443         format_version: self.format_version,
444         bolts: self.bolts.transform_values( &:fields ),
445         callsigns: self.callsigns,
446         segments: self.segments,
447         pois: self.pois,
448         timestamp: self.timestamp.to_i,
449     }
450 end
Also aliased as: to_h
filename()

Return the name of the mission file that corresponds to this mission. Note that this does no mean that it exists.

    # File lib/ravn/tactical/mission.rb
405 def filename
406     return self.class.filename_for( self.id )
407 end
filepath()

Return the Pathname of the mission file. Note that this does not necessarily mean that it exists.

    # File lib/ravn/tactical/mission.rb
412 def filepath
413     return Ravn::BDE.mission_directory + self.filename
414 end
finish_archive( force: false )

Finish the archiving process.

    # File lib/ravn/tactical/mission.rb
573 def finish_archive( force: false )
574     raise "incomplete archive" unless force || self.all_nodes_archived?
575 
576     self.log.info "Finishing archive for mission %s." % [ self.id ]
577     self.archive_filepath.make_link( self.filepath )
578 end
initialize_copy( original )

Copy constructor; overloaded to make duplicates distinct.

    # File lib/ravn/tactical/mission.rb
261 def initialize_copy( original )
262     super
263 
264     @id = Ravn.uuid.generate
265     @name = self.class.next_variant_for_name( original.name )
266     @timestamp = nil
267     @version = self.class.make_timestamp_version
268 end
inspect_details()

Ravn::Inspection API – return the custom portion of the object details that make up inspect output.

    # File lib/ravn/tactical/mission.rb
736 def inspect_details
737     return "%s `%s` {%s} (format %d) %d bolts, %d segments, %d callsigns, %d POIs" % [
738         self.id,
739         self.name,
740         self.version,
741         self.format_version,
742         self.bolts.size,
743         self.segments.size,
744         self.callsigns.size,
745         self.pois.size,
746     ]
747 end
kit_directory_paths()

Return an Array of Pathnames that do or will contain archives for compute packs in the current mission.

    # File lib/ravn/tactical/mission.rb
653 def kit_directory_paths
654     return self.node_ids.map {|devid| self.archive_directory_path / devid.to_s }
655 end
make_current()

Write the Mission and change the symlink at the configured mission config path to point to the new/updated file. Raises if the mission config path is not a symlink.

    # File lib/ravn/tactical/mission.rb
523 def make_current
524     link_file = Ravn::BDE.mission_config_path
525     raise "refusing to replace non-symlink %s" % [ link_file ] if
526         link_file.exist? && !link_file.symlink?
527 
528     self.write( self.filepath.exist? )
529 
530     self.log.info "Linking %s -> %s" % [ link_file, self.filepath.realdirpath ]
531     link_file.unlink if link_file.symlink?
532     link_file.make_symlink( self.filepath.realdirpath )
533 
534     return true
535 end
make_device_directory()

Create the path to the current device’s mission archive directory.

    # File lib/ravn/tactical/mission.rb
680 def make_device_directory
681     path = self.device_archive_directory_path
682     self.log.info "Making mission archive directory for %s: %s" % [ Ravn.device_id, path ]
683     path.mkpath
684 
685     return path
686 end
make_kit_directories()

Make device directories for all the devices in the mission.

    # File lib/ravn/tactical/mission.rb
659 def make_kit_directories
660     paths = self.kit_directory_paths
661 
662     if paths.empty?
663         self.log.error "can't make kit directories: no callsigns in the current mission"
664     else
665         self.log.info "making kit archive directories for %d nodes" % [ paths.length ]
666         FileUtils.mkpath( paths )
667     end
668 
669     return paths
670 end
node_ids( include_local: true )

Return the device IDs of the nodes in this mission. If include_local is false, omit the ID of the local node.

    # File lib/ravn/tactical/mission.rb
624 def node_ids( include_local: true )
625     ids = callsigns.keys
626     ids -= [ Ravn.device_id ] if !include_local
627 
628     return ids
629 end
nodes( include_local: true )

Return Ravn::Tactical::Node objects for all the nodes in the this mission.

    # File lib/ravn/tactical/mission.rb
633 def nodes( include_local: true )
634     ids = self.node_ids( include_local: )
635     return Ravn::Tactical::Node.where( device_id: ids ).all
636 end
nodes=( new_nodes )

Replace the missions existing callsigns hash with one made from the specified new_nodes.

    # File lib/ravn/tactical/mission.rb
331 def nodes=( new_nodes )
332     self.callsigns = new_nodes.each_with_object( {} ) do |node, accum|
333         accum[ node.device_id ] = node.callsign
334     end
335 end
pois=( new_pois )

Replace the mission’s existing POIs with the specified new_pois.

    # File lib/ravn/tactical/mission.rb
386 def pois=( new_pois )
387     @pois = Array( new_pois ).map do |poi|
388         poi.transform_keys( &:to_sym )
389     end
390 end
queue_change( type, include_local: false, **config, &block )

Create and return Ravn::Tactical::Change queue entries of the given type and config for this mission’s nodes, excluding the Control unless include_control is set. If a block is given, each change will be yielded to it before it’s saved.

    # File lib/ravn/tactical/mission.rb
643 def queue_change( type, include_local: false, **config, &block )
644     nodes = self.nodes( include_local: )
645     self.log.info "Queueing a %p change for %d nodes." % [ type, nodes.size ]
646 
647     return Ravn::Tactical::Change.create_for_nodes( type, nodes, **config, &block )
648 end
save( update=self.written? )
Alias for: write
saved?()
Alias for: written?
segments=( new_segments )

Replace the mission’s existing segments hash with the specified new_segments.

    # File lib/ravn/tactical/mission.rb
339 def segments=( new_segments )
340     new_segments ||= {}
341     @segments = new_segments.transform_keys( &:to_s )
342 end
status()

Returns a Symbol that indicates the Mission’s disposition in the Kit.

[:planning] is not the current mission, and hasn’t been archived [:current] is the current mission [:archiving] is in the process of being archived [:archived] has been archived, but not yet downloaded

    # File lib/ravn/tactical/mission.rb
428 def status
429     return nil unless self.saved?
430     return :current if self.current?
431     return :archived if self.archived?
432     return :archiving if self.archiving?
433     return :planning
434 end
to_h()
Alias for: fields
unmake_current()

If the current mission is the current mission, remove the symlink that makes it so. Raises an exception if it’s not the current mission.

    # File lib/ravn/tactical/mission.rb
540 def unmake_current
541     raise "this is not the current mission" unless self.current?
542     Ravn::BDE.mission_config_path.unlink
543 end
update( **values )

Update the mission with new values and return the updated mission.

    # File lib/ravn/tactical/mission.rb
465 def update( **values )
466     self.name           = values[:name] if values.key?( :name )
467     self.format_version = values[:format_version] if values.key?( :format_version )
468     self.bolts          = values[:bolts] if values.key?( :bolts )
469     self.callsigns      = values[:callsigns] if values.key?( :callsigns )
470     self.segments       = values[:segments] if values.key?( :segments )
471     self.pois           = values[:pois] if values.key?( :pois )
472 
473     return self
474 end
update_version()

Update the version of the mission to indicate that it’s changed.

    # File lib/ravn/tactical/mission.rb
478 def update_version
479     self.version = self.class.make_timestamp_version
480 end
valid?()

Returns true if the current config is valid.

    # File lib/ravn/tactical/mission.rb
455 def valid?
456     Ravn::BDE.validate_mission_config( self.to_h )
457     return true
458 rescue Ravn::BDE::ValidationError => err
459     self.log.error "%p while validating: %s" % [ err.class, err.message ]
460     return false
461 end
write( update=self.written? )

Write the config to the current mission directory. If update is false, it will raise a Errno::EEXIST if the file already exists.

    # File lib/ravn/tactical/mission.rb
485 def write( update=self.written? )
486     self.update_version
487 
488     rawhash = self.to_h
489     Ravn::BDE.validate_mission_config( rawhash )
490     hash = stringify_keys( rawhash )
491 
492     path = self.filepath
493 
494     flags  = File::WRONLY | File::EXCL
495     if update
496         flags |= File::TRUNC
497     else
498         flags |= File::CREAT
499     end
500 
501     self.log.info "Writing mission %s to %s" % [ self.id, path ]
502     path.open( flags, 0644, encoding: 'utf-8' ) do |fh|
503         Psych.safe_dump( hash, fh )
504     end
505 
506     self.timestamp = path.mtime
507 
508     return self
509 end
Also aliased as: save
written?()

Returns true if there is a corresponding (non-archived) file for this mission. It does not do any checking to ensure the file is a valid mission config or contains the latest values from the receiving object.

    # File lib/ravn/tactical/mission.rb
549 def written?
550     return self.filepath.file?
551 end
Also aliased as: saved?