def add_from_args(self, args: argparse.Namespace) -> int: """Parse args for add element to file.""" file_path = pathlib.Path(args.file).resolve() # Get parent model and then load json into parent model parent_model, _ = ModelUtils.get_stripped_model_type(file_path, args.trestle_root) parent_object = parent_model.oscal_read(file_path) parent_element = Element(parent_object, classname_to_alias(parent_model.__name__, AliasMode.JSON)) add_plan = Plan() # Do _add for each element_path specified in args element_paths: List[str] = args.element.split(',') for elm_path_str in element_paths: element_path = ElementPath(elm_path_str) update_action, parent_element = self.add(element_path, parent_element, args.include_optional_fields) add_plan.add_action(update_action) create_action = CreatePathAction(file_path, True) # this will output json or yaml based on type of input file write_action = WriteFileAction(file_path, parent_element, FileContentType.to_content_type(file_path.suffix)) add_plan.add_action(create_action) add_plan.add_action(write_action) add_plan.execute() return CmdReturnCodes.SUCCESS.value
def write(self, model: OscalBaseModel) -> bool: """Write OSCAL model to repository.""" logger.debug(f'Writing model {self._model_name}.') model_alias = classname_to_alias(model.__class__.__name__, AliasMode.JSON) if parser.to_full_model_name(model_alias) is None: raise TrestleError(f'Given model {model_alias} is not a top level model.') # split directory if the model was split split_dir = pathlib.Path(self.model_dir, self.model_alias) # Prepare actions; delete split model dir if any, recreate model file, and write to filepath top_element = Element(model) remove_action = RemovePathAction(split_dir) create_action = CreatePathAction(self.filepath, True) write_action = WriteFileAction(self.filepath, top_element, self.file_content_type) # create a plan to create the directory and imported file. import_plan = Plan() import_plan.add_action(remove_action) import_plan.add_action(create_action) import_plan.add_action(write_action) import_plan.execute() logger.debug(f'Model {self._model_name} written to repository') return True
def assemble_model(cls, model_alias: str, args: argparse.Namespace) -> int: """Assemble a top level OSCAL model within the trestle dist directory.""" log.set_log_level_from_args(args) logger.info(f'Assembling models of type {model_alias}.') trestle_root = args.trestle_root # trestle root is set via command line in args. Default is cwd. if not trestle_root or not file_utils.is_valid_project_root( args.trestle_root): raise TrestleRootError( f'Given directory {trestle_root} is not a trestle project.') model_names = [] if args.name: model_names = [args.name] logger.info( f'Assembling single model of type {model_alias}: {args.name}.') else: model_names = ModelUtils.get_models_of_type( model_alias, trestle_root) nmodels = len(model_names) logger.info( f'Assembling {nmodels} found models of type {model_alias}.') if len(model_names) == 0: logger.info(f'No models found to assemble of type {model_alias}.') return CmdReturnCodes.SUCCESS.value for model_name in model_names: # contruct path to the model file name root_model_dir = trestle_root / ModelUtils.model_type_to_model_dir( model_alias) model_file_type = file_utils.get_contextual_file_type( root_model_dir / model_name) model_file_name = f'{model_alias}{FileContentType.to_file_extension(model_file_type)}' root_model_filepath = root_model_dir / model_name / model_file_name if not root_model_filepath.exists(): raise TrestleError( f'No top level model file at {root_model_dir}') # distributed load _, _, assembled_model = ModelUtils.load_distributed( root_model_filepath, args.trestle_root) plural_alias = ModelUtils.model_type_to_model_dir(model_alias) assembled_model_dir = trestle_root / const.TRESTLE_DIST_DIR / plural_alias assembled_model_filepath = assembled_model_dir / f'{model_name}.{args.extension}' plan = Plan() plan.add_action(CreatePathAction(assembled_model_filepath, True)) plan.add_action( WriteFileAction( assembled_model_filepath, Element(assembled_model), FileContentType.to_content_type(f'.{args.extension}'))) plan.execute() return CmdReturnCodes.SUCCESS.value
def import_model(self, model: OscalBaseModel, name: str, content_type='json') -> ManagedOSCAL: """Import OSCAL object into trestle repository.""" logger.debug(f'Importing model {name} of type {model.__class__.__name__}.') model_alias = classname_to_alias(model.__class__.__name__, AliasMode.JSON) if parser.to_full_model_name(model_alias) is None: raise TrestleError(f'Given model {model_alias} is not a top level model.') # Work out output directory and file plural_path = ModelUtils.model_type_to_model_dir(model_alias) desired_model_dir = self._root_dir / plural_path desired_model_path = desired_model_dir / name / (model_alias + '.' + content_type) desired_model_path = desired_model_path.resolve() if desired_model_path.exists(): raise TrestleError(f'OSCAL file to be created here: {desired_model_path} exists.') content_type = FileContentType.to_content_type(pathlib.Path(desired_model_path).suffix) # Prepare actions top_element = Element(model) create_action = CreatePathAction(desired_model_path, True) write_action = WriteFileAction(desired_model_path, top_element, content_type) # create a plan to create the directory and imported file. import_plan = Plan() import_plan.add_action(create_action) import_plan.add_action(write_action) import_plan.execute() # Validate the imported file, rollback if unsuccessful success = False errmsg = '' try: success = self.validate_model(model.__class__, name) if not success: errmsg = f'Validation of model {name} did not pass' logger.error(errmsg) except Exception as err: logger.error(errmsg) errmsg = f'Import of model {name} failed. Validation failed with error: {err}' if not success: # rollback in case of validation error or failure logger.debug(f'Rolling back import of model {name} to {desired_model_path}') try: import_plan.rollback() except TrestleError as err: logger.error(f'Failed to rollback: {err}. Remove {desired_model_path} to resolve state.') else: logger.debug(f'Successful rollback of import to {desired_model_path}') # raise trestle error raise TrestleError(errmsg) # all well; model was imported and validated successfully logger.debug(f'Model {name} of type {model.__class__.__name__} imported successfully.') return ManagedOSCAL(self._root_dir, model.__class__, name)
def replicate_object(cls, model_alias: str, args: argparse.Namespace) -> int: """ Core replicate routine invoked by subcommands. Args: model_alias: Name of the top level model in the trestle directory. args: CLI arguments Returns: A return code that can be used as standard posix codes. 0 is success. """ logger.debug('Entering replicate_object.') # 1 Bad working directory if not running from current working directory trestle_root = args.trestle_root # trestle root is set via command line in args. Default is cwd. if not trestle_root or not file_utils.is_valid_project_root(trestle_root): raise TrestleError(f'Given directory: {trestle_root} is not a trestle project.') plural_path = ModelUtils.model_type_to_model_dir(model_alias) # 2 Check that input file given exists. input_file_stem = trestle_root / plural_path / args.name / model_alias content_type = FileContentType.path_to_content_type(input_file_stem) if content_type == FileContentType.UNKNOWN: raise TrestleError( f'Input file {args.name} has no json or yaml file at expected location {input_file_stem}.' ) input_file = input_file_stem.with_suffix(FileContentType.to_file_extension(content_type)) # 3 Distributed load from file _, model_alias, model_instance = ModelUtils.load_distributed(input_file, trestle_root) rep_model_path = trestle_root / plural_path / args.output / ( model_alias + FileContentType.to_file_extension(content_type) ) if rep_model_path.exists(): raise TrestleError(f'OSCAL file to be replicated here: {rep_model_path} exists.') if args.regenerate: logger.debug(f'regenerating uuids for model {input_file}') model_instance, uuid_lut, n_refs_updated = ModelUtils.regenerate_uuids(model_instance) logger.debug(f'{len(uuid_lut)} uuids generated and {n_refs_updated} references updated') # 4 Prepare actions and plan top_element = Element(model_instance) create_action = CreatePathAction(rep_model_path, True) write_action = WriteFileAction(rep_model_path, top_element, content_type) # create a plan to create the directory and imported file. replicate_plan = Plan() replicate_plan.add_action(create_action) replicate_plan.add_action(write_action) replicate_plan.execute() return CmdReturnCodes.SUCCESS.value
def assemble_model(cls, model_alias: str, object_type: Type[TLO], args: argparse.Namespace) -> int: """Assemble a top level OSCAL model within the trestle dist directory.""" log.set_log_level_from_args(args) trestle_root = fs.get_trestle_project_root(Path.cwd()) if not trestle_root: logger.error( f'Current working directory {Path.cwd()} is not with a trestle project.' ) return 1 if not trestle_root == Path.cwd(): logger.error( f'Current working directory {Path.cwd()} is not the top level trestle project directory.' ) return 1 # contruct path to the model file name root_model_dir = Path.cwd() / f'{model_alias}s' try: model_file_type = fs.get_contextual_file_type(root_model_dir / args.name) except Exception as e: logger.error('No files found in the specified model directory.') logger.debug(e) return 1 model_file_name = f'{model_alias}{FileContentType.to_file_extension(model_file_type)}' root_model_filepath = root_model_dir / args.name / model_file_name if not root_model_filepath.exists(): logger.error(f'No top level model file at {root_model_dir}') return 1 # distributed load _, _, assembled_model = load_distributed(root_model_filepath) plural_alias = model_alias if model_alias[ -1] == 's' else model_alias + 's' assembled_model_dir = trestle_root / const.TRESTLE_DIST_DIR / plural_alias assembled_model_filepath = assembled_model_dir / f'{args.name}.{args.extension}' plan = Plan() plan.add_action(CreatePathAction(assembled_model_filepath, True)) plan.add_action( WriteFileAction( assembled_model_filepath, Element(assembled_model), FileContentType.to_content_type(f'.{args.extension}'))) try: plan.simulate() plan.execute() return 0 except Exception as e: logger.error( 'Unknown error executing trestle create operations. Rolling back.' ) logger.debug(e) return 1
def create_object(cls, model_alias: str, object_type: Type[TLO], args: argparse.Namespace) -> int: """Create a top level OSCAL object within the trestle directory, leveraging functionality in add.""" log.set_log_level_from_args(args) trestle_root = fs.get_trestle_project_root(Path.cwd()) if not trestle_root: logger.error( f'Current working directory {Path.cwd()} is not with a trestle project.' ) return 1 plural_path: str # Cater to POAM if model_alias[-1] == 's': plural_path = model_alias else: plural_path = model_alias + 's' desired_model_dir = trestle_root / plural_path / args.name desired_model_path = desired_model_dir / (model_alias + '.' + args.extension) if desired_model_path.exists(): logger.error( f'OSCAL file to be created here: {desired_model_path} exists.') logger.error('Aborting trestle create.') return 1 # Create sample model. sample_model = generators.generate_sample_model(object_type) # Presuming top level level model not sure how to do the typing for this. sample_model.metadata.title = f'Generic {model_alias} created by trestle.' # type: ignore sample_model.metadata.last_modified = datetime.now().astimezone() sample_model.metadata.oscal_version = trestle.oscal.OSCAL_VERSION sample_model.metadata.version = '0.0.0' top_element = Element(sample_model, model_alias) create_action = CreatePathAction(desired_model_path.absolute(), True) write_action = WriteFileAction( desired_model_path.absolute(), top_element, FileContentType.to_content_type(desired_model_path.suffix)) # create a plan to write the directory and file. try: create_plan = Plan() create_plan.add_action(create_action) create_plan.add_action(write_action) create_plan.simulate() create_plan.execute() return 0 except Exception as e: logger.error( 'Unknown error executing trestle create operations. Rolling back.' ) logger.debug(e) return 1
def add(cls, file_path, element_path, parent_model, parent_element): """For a file_path and element_path, add a child model to the parent_element of a given parent_model. First we find the child model at the specified element path and instantiate it with default values. Then we check if there's already existing element at that path, in which case we append the child model to the existing list of dict. Then we set up an action plan to update the model (specified by file_path) in memory, create a file at the same location and write the file. """ element_path_list = element_path.get_full_path_parts() if '*' in element_path_list: raise err.TrestleError( 'trestle add does not support Wildcard element path.') # Get child model try: child_model = utils.get_target_model(element_path_list, parent_model) # Create child element with sample values child_object = utils.get_sample_model(child_model) if parent_element.get_at(element_path) is not None: # The element already exists if type(parent_element.get_at(element_path)) is list: child_object = parent_element.get_at( element_path) + child_object elif type(parent_element.get_at(element_path)) is dict: child_object = { **parent_element.get_at(element_path), **child_object } else: raise err.TrestleError( 'Already exists and is not a list or dictionary.') except Exception as e: raise err.TrestleError(f'Bad element path. {str(e)}') update_action = UpdateAction(sub_element=child_object, dest_element=parent_element, sub_element_path=element_path) create_action = CreatePathAction(file_path.absolute(), True) write_action = WriteFileAction( file_path.absolute(), parent_element, FileContentType.to_content_type(file_path.suffix)) add_plan = Plan() add_plan.add_action(update_action) add_plan.add_action(create_action) add_plan.add_action(write_action) add_plan.simulate() add_plan.execute()
def _run(self, args: argparse.Namespace) -> int: """Add an OSCAL component/subcomponent to the specified component. This method takes input a filename and a list of comma-seperated element path. Element paths are field aliases. The method first finds the parent model from the file and loads the file into the model. Then the method executes 'add' for each of the element paths specified. """ log.set_log_level_from_args(args) try: args_dict = args.__dict__ file_path = pathlib.Path(args_dict[const.ARG_FILE]) # Get parent model and then load json into parent model parent_model, parent_alias = fs.get_stripped_contextual_model( file_path.absolute()) parent_object = parent_model.oscal_read(file_path.absolute()) # FIXME : handle YAML files after detecting file type parent_element = Element( parent_object, utils.classname_to_alias(parent_model.__name__, 'json')) add_plan = Plan() # Do _add for each element_path specified in args element_paths: List[str] = args_dict[const.ARG_ELEMENT].split(',') for elm_path_str in element_paths: element_path = ElementPath(elm_path_str) update_action, parent_element = self.add( element_path, parent_model, parent_element) add_plan.add_action(update_action) create_action = CreatePathAction(file_path.absolute(), True) write_action = WriteFileAction( file_path.absolute(), parent_element, FileContentType.to_content_type(file_path.suffix)) add_plan.add_action(create_action) add_plan.add_action(write_action) add_plan.simulate() add_plan.execute() except BaseException as err: logger.error(f'Add failed: {err}') return 1 return 0
def _run(self, args: argparse.Namespace) -> int: """Remove an OSCAL component/subcomponent to the specified component. This method takes input a filename and a list of comma-seperated element path. Element paths are field aliases. The method first finds the parent model from the file and loads the file into the model. Then the method executes 'remove' for each of the element paths specified. """ try: log.set_log_level_from_args(args) args_dict = args.__dict__ file_path = pathlib.Path(args_dict[const.ARG_FILE]).resolve() relative_path = file_path.relative_to(args.trestle_root) # Get parent model and then load json into parent model parent_model, parent_alias = ModelUtils.get_relative_model_type( relative_path) parent_object = parent_model.oscal_read(file_path) parent_element = Element(parent_object, parent_alias) add_plan = Plan() # Do _remove for each element_path specified in args element_paths: List[str] = str( args_dict[const.ARG_ELEMENT]).split(',') for elm_path_str in element_paths: element_path = ElementPath(elm_path_str) remove_action, parent_element = self.remove( element_path, parent_element) add_plan.add_action(remove_action) create_action = CreatePathAction(file_path, True) write_action = WriteFileAction( file_path, parent_element, FileContentType.to_content_type(file_path.suffix)) add_plan.add_action(remove_action) add_plan.add_action(create_action) add_plan.add_action(write_action) add_plan.execute() return CmdReturnCodes.SUCCESS.value except Exception as e: return err.handle_generic_command_exception( e, logger, 'Error while removing OSCAL component')
def test_plan_execution(tmp_path, sample_nist_component_def: component.ComponentDefinition): """Test successful execution of a valid plan.""" content_type = FileContentType.YAML base_dir: pathlib.Path = pathlib.Path.joinpath(tmp_path, 'mycomponent') targets_dir: pathlib.Path = pathlib.Path.joinpath(base_dir, 'components') metadata_yaml: pathlib.Path = pathlib.Path.joinpath(base_dir, 'metadata.yaml') test_utils.ensure_trestle_config_dir(base_dir) # hand craft a split plan split_plan = Plan() split_plan.add_action(CreatePathAction(metadata_yaml)) split_plan.add_action( WriteFileAction( metadata_yaml, Element(sample_nist_component_def.metadata, 'component-definition'), content_type ) ) # Test stringing a plan stringed = str(split_plan) assert len(stringed) > 0 target_files: List[pathlib.Path] = [] for index in range(len(sample_nist_component_def.components)): target_file: pathlib.Path = pathlib.Path.joinpath(targets_dir, f'component_{index}.yaml') target_files.append(target_file) split_plan.add_action(CreatePathAction(target_file)) split_plan.add_action( WriteFileAction(target_file, Element(sample_nist_component_def.components[index], 'target'), content_type) ) # execute the plan split_plan.execute() assert base_dir.exists() assert targets_dir.exists() assert metadata_yaml.exists() for target_file in target_files: assert target_file.exists() split_plan.rollback() assert base_dir.exists() is True assert targets_dir.exists() is False assert metadata_yaml.exists() is False for target_file in target_files: target_file.exists()
def create_object(cls, model_alias: str, object_type: Type[TopLevelOscalModel], args: argparse.Namespace) -> int: """Create a top level OSCAL object within the trestle directory, leveraging functionality in add.""" log.set_log_level_from_args(args) trestle_root = args.trestle_root # trestle root is set via command line in args. Default is cwd. if not trestle_root or not file_utils.is_valid_project_root( args.trestle_root): raise err.TrestleRootError( f'Given directory {trestle_root} is not a trestle project.') plural_path = ModelUtils.model_type_to_model_dir(model_alias) desired_model_dir = trestle_root / plural_path / args.output desired_model_path = desired_model_dir / (model_alias + '.' + args.extension) if desired_model_path.exists(): raise err.TrestleError( f'OSCAL file to be created here: {desired_model_path} exists.') # Create sample model. sample_model = generators.generate_sample_model( object_type, include_optional=args.include_optional_fields) # Presuming top level level model not sure how to do the typing for this. sample_model.metadata.title = f'Generic {model_alias} created by trestle named {args.output}.' # type: ignore sample_model.metadata.last_modified = datetime.now().astimezone() sample_model.metadata.oscal_version = trestle.oscal.OSCAL_VERSION sample_model.metadata.version = '0.0.0' top_element = Element(sample_model, model_alias) create_action = CreatePathAction(desired_model_path.resolve(), True) write_action = WriteFileAction( desired_model_path.resolve(), top_element, FileContentType.to_content_type(desired_model_path.suffix)) # create a plan to write the directory and file. create_plan = Plan() create_plan.add_action(create_action) create_plan.add_action(write_action) create_plan.execute() return CmdReturnCodes.SUCCESS.value
def test_plan_execution(tmp_dir, sample_target_def: target.TargetDefinition): """Test successful execution of a valid plan.""" content_type = FileContentType.YAML base_dir: pathlib.Path = pathlib.Path.joinpath(tmp_dir, 'mytarget') targets_dir: pathlib.Path = pathlib.Path.joinpath(base_dir, 'targets') metadata_yaml: pathlib.Path = pathlib.Path.joinpath( base_dir, 'metadata.yaml') test_utils.ensure_trestle_config_dir(base_dir) # hand craft a split plan split_plan = Plan() split_plan.add_action(CreatePathAction(metadata_yaml)) split_plan.add_action( WriteFileAction( metadata_yaml, Element(sample_target_def.metadata, 'target-definition'), content_type)) target_files: List[pathlib.Path] = [] for tid, t in sample_target_def.targets.items(): target_file: pathlib.Path = pathlib.Path.joinpath( targets_dir, tid + '.yaml') target_files.append(target_file) split_plan.add_action(CreatePathAction(target_file)) split_plan.add_action( WriteFileAction(target_file, Element(t, 'target'), content_type)) # execute the plan split_plan.execute() assert base_dir.exists() assert targets_dir.exists() assert metadata_yaml.exists() for target_file in target_files: assert target_file.exists() split_plan.rollback() assert base_dir.exists() is True assert targets_dir.exists() is False assert metadata_yaml.exists() is False for target_file in target_files: target_file.exists() is False
def test_subsequent_split_model( tmp_path: pathlib.Path, sample_target_def: ostarget.TargetDefinition) -> None: """Test subsequent split of sub models.""" # Assume we are running a command like below # trestle split -f target-definition.yaml -e target-definition.metadata content_type = FileContentType.YAML # prepare trestle project dir with the file target_def_dir, target_def_file = test_utils.prepare_trestle_project_dir( tmp_path, content_type, sample_target_def, test_utils.TARGET_DEFS_DIR) # first split the target-def into metadata target_def = ostarget.TargetDefinition.oscal_read(target_def_file) element = Element(target_def, 'target-definition') element_args = ['target-definition.metadata'] element_paths = test_utils.prepare_element_paths(target_def_dir, element_args) metadata_file = target_def_dir / element_paths[0].to_file_path( content_type) metadata: ostarget.Metadata = element.get_at(element_paths[0]) root_file = target_def_dir / element_paths[0].to_root_path(content_type) metadata_field_alias = element_paths[0].get_element_name() stripped_root = element.get().stripped_instance( stripped_fields_aliases=[metadata_field_alias]) root_wrapper_alias = utils.classname_to_alias( stripped_root.__class__.__name__, 'json') first_plan = Plan() first_plan.add_action(CreatePathAction(metadata_file)) first_plan.add_action( WriteFileAction(metadata_file, Element(metadata, metadata_field_alias), content_type)) first_plan.add_action(CreatePathAction(root_file, True)) first_plan.add_action( WriteFileAction(root_file, Element(stripped_root, root_wrapper_alias), content_type)) first_plan.execute() # this will split the files in the temp directory # now, prepare the expected plan to split metadta at parties second_plan = Plan() metadata_file_dir = target_def_dir / element_paths[0].to_root_path() metadata2 = ostarget.Metadata.oscal_read(metadata_file) element = Element(metadata2, metadata_field_alias) element_args = ['metadata.parties.*'] element_paths = test_utils.prepare_element_paths(target_def_dir, element_args) parties_dir = metadata_file_dir / element_paths[0].to_file_path() for i, party in enumerate(element.get_at(element_paths[0])): prefix = str(i).zfill(const.FILE_DIGIT_PREFIX_LENGTH) sub_model_actions = SplitCmd.prepare_sub_model_split_actions( party, parties_dir, prefix, content_type) second_plan.add_actions(sub_model_actions) # stripped metadata stripped_metadata = metadata2.stripped_instance( stripped_fields_aliases=['parties']) second_plan.add_action(CreatePathAction(metadata_file, True)) second_plan.add_action( WriteFileAction(metadata_file, Element(stripped_metadata, metadata_field_alias), content_type)) # call the split command and compare the plans split_plan = SplitCmd.split_model(metadata, element_paths, metadata_file_dir, content_type) assert second_plan == split_plan
def test_remove(tmp_path, sample_catalog_minimal): """Test RemoveCmd.remove() method for trestle remove: removing Roles and Responsible-Parties.""" # 1. Remove responsible-parties # Note: minimal catalog does have responsible-parties but doesn't have Roles. file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog.json') catalog_with_responsible_parties = Element(Catalog.oscal_read(file_path)) # minimal catalog with responsible-parties (dict) removed file_path = pathlib.Path.joinpath( test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog_no_responsible-parties.json') expected_catalog_responsible_parties_removed = Element( Catalog.oscal_read(file_path)) # Target path for removal: element_path = ElementPath('catalog.metadata.responsible-parties') expected_remove_action = RemoveAction(catalog_with_responsible_parties, element_path) # Call remove() method actual_remove_action, actual_catalog_removed_responsible_parties = RemoveCmd.remove( element_path, Catalog, catalog_with_responsible_parties) # 1.1 Assertion about action assert expected_remove_action == actual_remove_action add_plan = Plan() add_plan.add_action(actual_remove_action) add_plan.simulate() add_plan.execute() # 1.2 Assertion about resulting element after removal assert expected_catalog_responsible_parties_removed == actual_catalog_removed_responsible_parties # 2. Remove roles # Note: minimal catalog does have responsible-parties but doesn't have Roles. file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog.json') catalog_without_roles = Element(Catalog.oscal_read(file_path)) # minimal catalog with Roles file_path = pathlib.Path.joinpath(test_utils.JSON_TEST_DATA_PATH, 'minimal_catalog_roles.json') catalog_with_roles = Element(Catalog.oscal_read(file_path)) # Target path for removal: element_path = ElementPath('catalog.metadata.roles') expected_remove_action = RemoveAction(catalog_with_roles, element_path) # Call remove() method actual_remove_action, actual_catalog_removed_roles = RemoveCmd.remove( element_path, Catalog, catalog_with_roles) # 2.1 Assertion about action assert expected_remove_action == actual_remove_action add_plan = Plan() add_plan.add_action(actual_remove_action) add_plan.simulate() add_plan.execute() # 2.2 Assertion about resulting element after removal assert catalog_without_roles == actual_catalog_removed_roles
def _run(self, args: argparse.Namespace) -> int: """Top level import run command.""" log.set_log_level_from_args(args) logger.debug('Entering import run.') # 1. Validate input arguments are as expected. # This code block may never be reached as the argument is declared to be required. # 1.1 Check that input file given exists. input_file = pathlib.Path(args.file) if not input_file.exists(): logger.error(f'Input file {args.file} does not exist.') return 1 # 1.2 Bad working directory if not running from current working directory cwd = pathlib.Path.cwd().resolve() trestle_root = fs.get_trestle_project_root(cwd) if trestle_root is None: logger.error( f'Current working directory: {cwd} is not within a trestle project.' ) return 1 # 2. Importing a file that is already inside a trestle-initialized dir is bad trestle_root = trestle_root.resolve() try: input_file.absolute().relative_to(trestle_root) except ValueError: # An exception here is good: it means that the input file is not inside a trestle dir. pass else: logger.error( 'Input file cannot be from current trestle project. Use duplicate instead.' ) return 1 # 3. Work out typing information from input suffix. try: content_type = FileContentType.to_content_type(input_file.suffix) except TrestleError as err: logger.debug(f'FileContentType.to_content_type() failed: {err}') logger.error( f'Import failed, could not work out content type from file suffix: {err}' ) return 1 # 4. Load input and parse for model # 4.1 Load from file try: data = fs.load_file(input_file.absolute()) except JSONDecodeError as err: logger.debug(f'fs.load_file() failed: {err}') logger.error(f'Import failed, JSON error loading file: {err}') return 1 except TrestleError as err: logger.debug(f'fs.load_file() failed: {err}') logger.error(f'Import failed, error loading file: {err}') return 1 except PermissionError as err: logger.debug(f'fs.load_file() failed: {err}') logger.error( f'Import failed, access permission error loading file: {err}') return 1 # 4.2 root key check try: parent_alias = parser.root_key(data) except TrestleError as err: logger.debug(f'parser.root_key() failed: {err}') logger.error( f'Import failed, failed to parse input file for root key: {err}' ) return 1 # 4.3 parse the model parent_model_name = parser.to_full_model_name(parent_alias) try: parent_model = parser.parse_file(input_file.absolute(), parent_model_name) except TrestleError as err: logger.debug(f'parser.parse_file() failed: {err}') logger.error( f'Import failed, failed to parse valid contents of input file: {err}' ) return 1 # 5. Work out output directory and file plural_path: str plural_path = parent_alias # Cater to POAM if parent_alias[-1] != 's': plural_path = parent_alias + 's' desired_model_dir = trestle_root / plural_path # args.output is presumed to be assured as it is declared to be required if args.output: desired_model_path = desired_model_dir / args.output / ( parent_alias + input_file.suffix) if desired_model_path.exists(): logger.error( f'OSCAL file to be created here: {desired_model_path} exists.') logger.error('Aborting trestle import.') return 1 # 6. Prepare actions and plan top_element = Element(parent_model.oscal_read(input_file)) create_action = CreatePathAction(desired_model_path.absolute(), True) write_action = WriteFileAction(desired_model_path.absolute(), top_element, content_type) # create a plan to create the directory and imported file. import_plan = Plan() import_plan.add_action(create_action) import_plan.add_action(write_action) try: import_plan.simulate() except TrestleError as err: logger.debug(f'import_plan.simulate() failed: {err}') logger.error( f'Import failed, error in testing import operation: {err}') return 1 try: import_plan.execute() except TrestleError as err: logger.debug(f'import_plan.execute() failed: {err}') logger.error( f'Import failed, error in actual import operation: {err}') return 1 # 7. Leave the rest to trestle split return 0
def _run(self, args: argparse.Namespace) -> int: """Top level import run command.""" try: log.set_log_level_from_args(args) trestle_root = args.trestle_root if not file_utils.is_valid_project_root(trestle_root): raise TrestleRootError( f'Attempt to import from non-valid trestle project root {trestle_root}' ) input_uri = args.file if cache.FetcherFactory.in_trestle_directory( trestle_root, input_uri): raise TrestleError( f'Imported file {input_uri} cannot be from current trestle project. Use duplicate instead.' ) content_type = FileContentType.to_content_type( '.' + input_uri.split('.')[-1]) fetcher = cache.FetcherFactory.get_fetcher(trestle_root, str(input_uri)) model_read, parent_alias = fetcher.get_oscal(True) plural_path = ModelUtils.model_type_to_model_dir(parent_alias) output_name = args.output desired_model_dir = trestle_root / plural_path desired_model_path: pathlib.Path = desired_model_dir / output_name / parent_alias desired_model_path = desired_model_path.with_suffix( FileContentType.to_file_extension(content_type)).resolve() if desired_model_path.exists(): raise TrestleError( f'Cannot import because file to be imported here: {desired_model_path} already exists.' ) if args.regenerate: logger.debug( f'regenerating uuids in imported file {input_uri}') model_read, lut, nchanged = ModelUtils.regenerate_uuids( model_read) logger.debug( f'uuid lut has {len(lut.items())} entries and {nchanged} refs were updated' ) top_element = Element(model_read) create_action = CreatePathAction(desired_model_path, True) write_action = WriteFileAction(desired_model_path, top_element, content_type) # create a plan to create the directory and write the imported file. import_plan = Plan() import_plan.add_action(create_action) import_plan.add_action(write_action) import_plan.execute() args = argparse.Namespace(file=desired_model_path, verbose=args.verbose, trestle_root=args.trestle_root, type=None, all=None) rollback = False try: rc = validatecmd.ValidateCmd()._run(args) if rc > 0: logger.warning( f'Validation of imported file {desired_model_path} did not pass' ) rollback = True except TrestleError as err: logger.warning( f'Import of {str(input_uri)} failed with validation error: {err}' ) rollback = True if rollback: logger.debug( f'Rolling back import of {str(input_uri)} to {desired_model_path}' ) try: import_plan.rollback() except TrestleError as err: raise TrestleError( f'Import failed in plan rollback: {err}. Manually remove {desired_model_path} to recover.' ) logger.debug( f'Successful rollback of import to {desired_model_path}') return CmdReturnCodes.COMMAND_ERROR.value return CmdReturnCodes.SUCCESS.value except Exception as e: # pragma: no cover return handle_generic_command_exception( e, logger, 'Error while importing OSCAL file')
def _run(self, args: argparse.Namespace) -> int: """Remove an OSCAL component/subcomponent to the specified component. This method takes input a filename and a list of comma-seperated element path. Element paths are field aliases. The method first finds the parent model from the file and loads the file into the model. Then the method executes 'remove' for each of the element paths specified. """ log.set_log_level_from_args(args) args_dict = args.__dict__ file_path = pathlib.Path(args_dict[const.ARG_FILE]) # Get parent model and then load json into parent model try: parent_model, parent_alias = fs.get_contextual_model_type(file_path.absolute()) except Exception as err: logger.debug(f'fs.get_contextual_model_type() failed: {err}') logger.error(f'Remove failed (fs.get_contextual_model_type()): {err}') return 1 try: parent_object = parent_model.oscal_read(file_path.absolute()) except Exception as err: logger.debug(f'parent_model.oscal_read() failed: {err}') logger.error(f'Remove failed (parent_model.oscal_read()): {err}') return 1 parent_element = Element(parent_object, utils.classname_to_alias(parent_model.__name__, 'json')) add_plan = Plan() # Do _remove for each element_path specified in args element_paths: List[str] = str(args_dict[const.ARG_ELEMENT]).split(',') for elm_path_str in element_paths: element_path = ElementPath(elm_path_str) try: remove_action, parent_element = self.remove(element_path, parent_model, parent_element) except TrestleError as err: logger.debug(f'self.remove() failed: {err}') logger.error(f'Remove failed (self.remove()): {err}') return 1 add_plan.add_action(remove_action) create_action = CreatePathAction(file_path.absolute(), True) write_action = WriteFileAction( file_path.absolute(), parent_element, FileContentType.to_content_type(file_path.suffix) ) add_plan.add_action(remove_action) add_plan.add_action(create_action) add_plan.add_action(write_action) try: add_plan.simulate() except TrestleError as err: logger.debug(f'Remove failed at simulate(): {err}') logger.error(f'Remove failed (simulate()): {err}') return 1 try: add_plan.execute() except TrestleError as err: logger.debug(f'Remove failed at execute(): {err}') logger.error(f'Remove failed (execute()): {err}') return 1 return 0