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.
- 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
- 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)
Return an enumerator that yields Ravn::Tactical::Mission
objects for each mission config file found in the current mission directory.
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
Return an enumerator that yields Ravn::Tactical::Mission
objects for each mission template found in the current data directory.
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
Return the Pathname to the currently-configured archive directory.
219 def self::archive_directory
220 return Ravn::BDE.mission_directory / ARCHIVE_DIRNAME
221 end
Return an Array of the names of all mission templates in this release.
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 a new Mission
config from the given values
or reasonable defaults.
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.
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
Return the current mission config as a Ravn::Tactical::Mission
.
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 a mission by its UUID (id). Returns nil
if a mission file with the specified uuid
doesn’t exist.
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.
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.
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
Return the filename that corresponds to a mission with the given id
.
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.
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 the mission config file at the given path
and return it as a Ravn::Tactical::Mission
.
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
Generate a version out of a timestamp and return it.
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.
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
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”.
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
Returns true
if the receiver and the other
object are equivalent configurations.
728 def ==( other )
729 return other.is_a?( self.class ) &&
730 other.to_h == self.to_h
731 end
Index operator; look up the mission attribute that corresponds to key
and return it.
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
Return true if all device archive directories contain files.
674 def all_nodes_archived?
675 return self.kit_directory_paths.none?( &:empty? )
676 end
Archive the mission.
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
Make archival links for the current BDE logs/databases.
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
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).
603 def archive_directory_path
604 return self.class.archive_directory / self.id
605 end
Return the Pathname of the Mission
file when it’s been archived.
596 def archive_filepath
597 return self.archive_directory_path / 'mission.yml'
598 end
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.
713 def archive_files
714 return Pathname.glob( self.archive_directory_path / '**/*' )
715 end
Returns true
if the mission archive is complete.
588 def archived?
589 return Ravn::Tactical.is_control_node? ?
590 self.archive_filepath.exist? :
591 self.archive_directory_path.exist?
592 end
Returns true
if the mission archive has been set up but not completed.
582 def archiving?
583 return !self.archived? && self.archive_directory_path.exist?
584 end
Replace the mission’s existing bolts with the specified new_bolts
.
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.
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.
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
.
323 def callsigns=( new_callsigns )
324 new_callsigns ||= {}
325 @callsigns = new_callsigns.transform_keys( &:to_s )
326 end
Return a checksum of the mission id + version for fast comparison.
316 def chksum
317 return Digest::SHA256.hexdigest( self.id + self.version )[0,8]
318 end
Returns true
if this mission is configured as the current one.
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.
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.
617 def device_archive_directory_path
618 return self.device_archive_directory_for( Ravn.device_id )
619 end
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.
721 def device_archive_files
722 return Pathname.glob( self.device_archive_directory_path / '**/*' )
723 end
Return the mission config as a Hash.
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
Return the name of the mission file that corresponds to this mission. Note that this does no mean that it exists.
405 def filename
406 return self.class.filename_for( self.id )
407 end
Return the Pathname of the mission file. Note that this does not necessarily mean that it exists.
412 def filepath
413 return Ravn::BDE.mission_directory + self.filename
414 end
finish_archive( force: false )
Finish the archiving process.
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.
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
Ravn::Inspection API – return the custom portion of the object details that make up inspect output.
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
Return an Array of Pathnames that do or will contain archives for compute packs in the current mission.
653 def kit_directory_paths
654 return self.node_ids.map {|devid| self.archive_directory_path / devid.to_s }
655 end
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.
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
Create the path to the current device’s mission archive directory.
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 device directories for all the devices in the mission.
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.
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.
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
Replace the missions existing callsigns hash with one made from the specified new_nodes
.
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
Replace the mission’s existing POIs with the specified new_pois
.
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.
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? )
segments=( new_segments )
Replace the mission’s existing segments hash with the specified new_segments
.
339 def segments=( new_segments )
340 new_segments ||= {}
341 @segments = new_segments.transform_keys( &:to_s )
342 end
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
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
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.
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 the mission with new values
and return the updated mission.
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 the version of the mission to indicate that it’s changed.
478 def update_version
479 self.version = self.class.make_timestamp_version
480 end
Returns true
if the current config is valid.
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.
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
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.
549 def written?
550 return self.filepath.file?
551 end