def test_to_content_type(): """Test to_content_type method.""" assert FileContentType.to_content_type('.json') == FileContentType.JSON assert FileContentType.to_content_type('.yaml') == FileContentType.YAML with pytest.raises(TrestleError): FileContentType.to_content_type('.invalid')
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 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 oscal_write(self, path: pathlib.Path) -> None: """ Write out a pydantic data model in an oscal friendly way. OSCAL schema mandates that top level elements are wrapped in a singular json/yaml field. This function handles both json and yaml output as well as formatting of the json. Args: path: The output file location for the oscal object. Raises: err.TrestleError: If a unknown file extension is provided. """ content_type = FileContentType.to_content_type(path.suffix) # The output will have \r\n newlines on windows and \n newlines elsewhere if content_type == FileContentType.YAML: write_file = pathlib.Path(path).open('w', encoding=const.FILE_ENCODING) yaml = YAML(typ='safe') yaml.dump(yaml.load(self.oscal_serialize_json()), write_file) write_file.flush() write_file.close() elif content_type == FileContentType.JSON: write_file = pathlib.Path(path).open('wb') write_file.write(self.oscal_serialize_json_bytes(pretty=True)) # Flush / close required (by experience) due to flushing issues in tests. write_file.flush() write_file.close()
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 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 oscal_read(cls, path: pathlib.Path) -> Optional['OscalBaseModel']: """ Read OSCAL objects. Handles the fact OSCAL wraps top level elements and also deals with both yaml and json. Args: path: The path of the oscal object to read. Returns: The oscal object read into trestle oscal models. """ # Create the wrapper model. alias = classname_to_alias(cls.__name__, AliasMode.JSON) content_type = FileContentType.to_content_type(path.suffix) logger.debug( f'oscal_read content type {content_type} and alias {alias} from {path}' ) if not path.exists(): logger.warning(f'path does not exist in oscal_read: {path}') return None obj: Dict[str, Any] = {} try: if content_type == FileContentType.YAML: yaml = YAML(typ='safe') fh = path.open('r', encoding=const.FILE_ENCODING) obj = yaml.load(fh) fh.close() elif content_type == FileContentType.JSON: obj = load_file( path, json_loads=cls.__config__.json_loads, ) except Exception as e: raise err.TrestleError(f'Error loading file {path} {str(e)}') try: if not len(obj) == 1: raise err.TrestleError( f'Invalid OSCAL file structure, oscal file ' f'does not have a single top level key wrapping it. It has {len(obj)} keys.' ) parsed = cls.parse_obj(obj[alias]) except KeyError: raise err.TrestleError( f'Provided oscal file does not have top level key key: {alias}' ) except Exception as e: raise err.TrestleError(f'Error parsing file {path} {str(e)}') return parsed
def load_file(file_name: pathlib.Path) -> Dict[str, Any]: """ Load JSON or YAML file content into a dict. This is not intended to be the default load mechanism. It should only be used if a OSCAL object type is unknown but the context a user is in. """ content_type = FileContentType.to_content_type(file_name.suffix) with file_name.open('r', encoding=const.FILE_ENCODING) as f: if content_type == FileContentType.YAML: return yaml.load(f, yaml.FullLoader) elif content_type == FileContentType.JSON: return json.load(f)
def get_contextual_file_type(path: pathlib.Path) -> FileContentType: """Return the file content type for files in the given directory, if it's a trestle project.""" if not is_valid_project_model_path(path): raise err.TrestleError('Trestle project not found.') for file_or_directory in path.iterdir(): if file_or_directory.is_file(): return FileContentType.to_content_type(file_or_directory.suffix) for file_or_directory in path.iterdir(): if file_or_directory.is_dir(): return get_contextual_file_type(file_or_directory) raise err.TrestleError('No files found in the project.')
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 _run(self, args: argparse.Namespace) -> int: """Split an OSCAL file into elements.""" logger.debug('Entering trestle split.') log.set_log_level_from_args(args) # get the Model args_raw = args.__dict__ if args_raw[const.ARG_FILE] is None: logger.error(f'Argument "-{const.ARG_FILE_SHORT}" is required') return 1 file_path = pathlib.Path(args_raw[const.ARG_FILE]) if not file_path.exists(): logger.error(f'File {file_path} does not exist.') return 1 content_type = FileContentType.to_content_type(file_path.suffix) # find the base directory of the file file_absolute_path = pathlib.Path(file_path.absolute()) base_dir = file_absolute_path.parent model_type, _ = fs.get_stripped_contextual_model(file_absolute_path) # FIXME: Handle list/dicts model: OscalBaseModel = model_type.oscal_read(file_path) element_paths: List[ElementPath] = cmd_utils.parse_element_args( args_raw[const.ARG_ELEMENT].split(',')) split_plan = self.split_model(model, element_paths, base_dir, content_type, root_file_name=args_raw[const.ARG_FILE]) # Simulate the plan # if it fails, it would throw errors and get out of this command split_plan.simulate() # If we are here then simulation passed # so move the original file to the trash trash.store(file_path, True) # execute the plan split_plan.execute() return 0
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 oscal_write(self, path: pathlib.Path) -> None: """ Write out a pydantic data model in an oscal friendly way. OSCAL schema mandates that top level elements are wrapped in a singular json/yaml field. This function handles both json and yaml output as well as formatting of the json. Args: path: The output file location for the oscal object. Raises: err.TrestleError: If a unknown file extension is provided. """ class_name = self.__class__.__name__ # It would be nice to pass through the description but I can't seem to and # it does not affect the output dynamic_parser = {} dynamic_parser[classname_to_alias( class_name, 'field')] = (self.__class__, Field(self, title=classname_to_alias(class_name, 'field'), alias=classname_to_alias(class_name, 'json'))) wrapper_model = create_model(class_name, __base__=OscalBaseModel, **dynamic_parser) # type: ignore # Default behaviour is strange here. wrapped_model = wrapper_model( **{classname_to_alias(class_name, 'json'): self}) # content_type = FileContentType.to_content_type(path.suffix) write_file = pathlib.Path(path).open('w', encoding=const.FILE_ENCODING) if content_type == FileContentType.YAML: yaml.dump( yaml.safe_load( wrapped_model.json(exclude_none=True, by_alias=True)), write_file) elif content_type == FileContentType.JSON: write_file.write( wrapped_model.json(exclude_none=True, by_alias=True, indent=2))
def oscal_read(cls, path: pathlib.Path) -> 'OscalBaseModel': """ Read OSCAL objects. Handles the fact OSCAL wrap's top level elements and also deals with both yaml and json. """ # Create the wrapper model. alias = classname_to_alias(cls.__name__, 'json') content_type = FileContentType.to_content_type(path.suffix) if content_type == FileContentType.YAML: return cls.parse_obj(yaml.safe_load(path.open())[alias]) elif content_type == FileContentType.JSON: obj = load_file( path, json_loads=cls.__config__.json_loads, ) return cls.parse_obj(obj[alias]) else: raise err.TrestleError('Unknown file type')
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
def perform_split( cls, effective_cwd: pathlib.Path, file_name: str, elements: str, trestle_root: pathlib.Path ) -> int: """Perform the split operation. Args: effective_cwd: effective directory in which the the split operation is performed file_name: file name of model to split, or '' if deduced from elements and cwd elements: comma separated list of paths to strip from the file, with quotes removed Returns: 0 on success and 1 on failure """ file_path_list: List[Tuple[str, str]] = [] if file_name: file_path_list.append((file_name, elements)) else: # cwd must be in the model directory if file to split is not specified # find top directory for this model based on trestle root and cwd model_dir = file_utils.extract_project_model_path(effective_cwd) if model_dir is None: raise TrestleError('Current directory must be within a model directory if file is not specified') content_type: FileContentType = FileContentType.dir_to_content_type(model_dir) # determine the file needed for each split path element_paths = elements.split(',') for path in element_paths: element_path = ElementPath(path) # if element path is relative use directory context to determine absolute path element_path.make_absolute(model_dir, effective_cwd) file_path = element_path.find_last_file_in_path(content_type, model_dir) # now make the element path relative to the model file to be loaded if file_path is None or element_path.make_relative(file_path.relative_to(model_dir)) != 0: raise TrestleError(f'Unable to match element path with files in model directory {element_path}') file_path_list.append((file_path, element_path.to_string())) # match paths to corresponding files since several paths may be split from the same file file_path_dict: Dict[str, str] = {} for file_path in file_path_list: key = file_path[0] path = file_path[1] if key not in file_path_dict: file_path_dict[key] = path else: current_path = file_path_dict[key] file_path_dict[key] = f'{current_path},{path}' for raw_file_name, element_path in file_path_dict.items(): file_path = file_utils.relative_resolve(pathlib.Path(raw_file_name), effective_cwd) # this makes assumptions that the path is relative. if not file_path.exists(): raise TrestleError(f'File {file_path} does not exist.') content_type = FileContentType.to_content_type(file_path.suffix) # find the base directory of the file base_dir = file_path.parent model_type, _ = ModelUtils.get_stripped_model_type(file_path, trestle_root) model: OscalBaseModel = model_type.oscal_read(file_path) if cmd_utils.split_is_too_fine(element_path, model): raise TrestleError('Cannot split the model to the level of uuids, strings, etc.') # use the model itself to resolve any wildcards and create list of element paths logger.debug(f'split calling parse_element_args on {element_path}') # use contextual mode to parse element_paths: List[ElementPath] = cmd_utils.parse_element_args( model, element_path.split(','), base_dir.relative_to(trestle_root) ) # analyze the split tree and determine which aliases should be stripped from each file aliases_to_strip = cls.find_aliases_to_strip(element_paths) # need the file name relative to the base directory file_name_no_path = str(file_path.name) split_plan = cls.split_model( model, element_paths, base_dir, content_type, file_name_no_path, aliases_to_strip ) trash.store(file_path, True) try: split_plan.execute() except Exception as e: trash.recover(file_path, True) raise TrestleError(f'Split has failed with error: {e}.') return CmdReturnCodes.SUCCESS.value
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: """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