def template_validate(self) -> int: """Validate the integrity of the template files.""" logger.info('Checking template file integrity') for template_file in self.template_dir.iterdir(): if (template_file.name not in author_const.REFERENCE_TEMPLATES.values() and template_file.name.lower() != 'readme.md'): raise TrestleError( f'Unexpected template file {self.rel_dir(template_file)}') if template_file.suffix == '.md': try: md_api = MarkdownAPI() md_api.load_validator_with_template( template_file, True, False) except Exception as ex: raise TrestleError( f'Template for task {self.task_name} failed to validate due to {ex}' ) elif template_file.suffix == '.drawio': try: _ = DrawIOMetadataValidator(template_file) except Exception as ex: raise TrestleError( f'Template for task {self.task_name} failed to validate due to {ex}' ) logger.info('Templates validated') return CmdReturnCodes.SUCCESS.value
def parse_element_arg( model_obj: Union[OscalBaseModel, None], element_arg: str, relative_path: Optional[pathlib.Path] = None ) -> List[ElementPath]: """Parse an element arg string into a list of ElementPath. Args: model_obj: The OscalBaseModel being inspected to determine available elements that can be split element_arg: Single element path, as a string. relative_path: Optional relative path (from trestle root) used to validate element args are valid. Returns: The requested parsed list of ElementPath for use in split """ element_arg = element_arg.strip() if element_arg == '*': raise TrestleError('Invalid element path containing only a single wildcard.') if element_arg == '': raise TrestleError('Invalid element path is empty string.') # search for wildcards and create paths with its parent path path_parts = element_arg.split(ElementPath.PATH_SEPARATOR) if len(path_parts) <= 1: raise TrestleError(f'Invalid element path "{element_arg}" with only one element and no wildcard') element_paths = parse_chain(model_obj, path_parts, relative_path) if len(element_paths) <= 0: # don't complain if nothing to split pass return element_paths
def validate(self, validate_header: bool, validate_only_header: bool, governed_heading: str, readme_validate: bool, template_version: str, ignore: str) -> int: """Validate task.""" if not self.task_path.is_dir(): raise TrestleError( f'Task directory {self.task_path} does not exist. Exiting validate.' ) for task_instance in self.task_path.iterdir(): if task_instance.is_dir(): if file_utils.is_symlink(task_instance): continue result = self._measure_template_folder( task_instance, validate_header, validate_only_header, governed_heading, readme_validate, template_version, ignore) if not result: raise TrestleError( 'Governed-folder validation failed for task' + f'{self.task_name} on directory {self.rel_dir(task_instance)}' ) else: logger.warning( f'Unexpected file {self.rel_dir(task_instance)} identified in {self.task_name}' + ' directory, ignoring.') return CmdReturnCodes.SUCCESS.value
def setup_template(self, template_version: str) -> int: """Create structure to allow markdown template enforcement.""" if not self.task_path.exists(): self.task_path.mkdir(exist_ok=True, parents=True) elif self.task_path.is_file(): raise TrestleError( f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.' ) if not self.template_dir.exists(): self.template_dir.mkdir(exist_ok=True, parents=True) elif self.template_dir.is_file(): raise TrestleError( f'Template path: {self.rel_dir(self.template_dir)} is a file not a directory.' ) template_file_a_md = self.template_dir / 'a_template.md' template_file_another_md = self.template_dir / 'another_template.md' template_file_drawio = self.template_dir / 'architecture.drawio' TemplateVersioning.write_versioned_template('template.md', self.template_dir, template_file_a_md, template_version) TemplateVersioning.write_versioned_template('template.md', self.template_dir, template_file_another_md, template_version) TemplateVersioning.write_versioned_template('template.drawio', self.template_dir, template_file_drawio, template_version) return CmdReturnCodes.SUCCESS.value
def load_validator_with_template( self, md_template_path: pathlib.Path, validate_yaml_header: bool, validate_md_body: bool, md_header_to_validate: Optional[str] = None) -> None: """Load and initialize markdown validator.""" try: self.processor.governed_header = md_header_to_validate template_header, template_tree = self.processor.process_markdown( md_template_path) if len(template_header) == 0 and validate_yaml_header: raise TrestleError( f'Expected yaml header for markdown template where none exists {md_template_path}' ) self.validator = MarkdownValidator(md_template_path, template_header, template_tree, validate_yaml_header, validate_md_body, md_header_to_validate) except TrestleError as e: raise TrestleError( f'Error while loading markdown template {md_template_path}: {e}.' )
def delete_model(self, model_type: Type[OscalBaseModel], name: str) -> bool: """Delete an OSCAL model from repository.""" logger.debug(f'Deleting model {name} of type {model_type.__name__}.') model_alias = classname_to_alias(model_type.__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.') plural_path = ModelUtils.model_type_to_model_dir(model_alias) desired_model_dir = self._root_dir / plural_path / name if not desired_model_dir.exists() or not desired_model_dir.is_dir(): raise TrestleError(f'Model {name} does not exist.') shutil.rmtree(desired_model_dir) # remove model from dist directory if it exists dist_model_dir = self._root_dir / const.TRESTLE_DIST_DIR / plural_path file_content_type = FileContentType.path_to_content_type(dist_model_dir / name) if file_content_type != FileContentType.UNKNOWN: file_path = pathlib.Path( dist_model_dir, name + FileContentType.path_to_file_extension(dist_model_dir / name) ) logger.debug(f'Deleting model {name} from dist directory.') os.remove(file_path) logger.debug(f'Model {name} deleted successfully.') return True
def setup_template_governed_docs(self, template_version: str) -> int: """Create structure to allow markdown template enforcement. Returns: Unix return code. """ if not self.task_path.exists(): self.task_path.mkdir(exist_ok=True, parents=True) elif self.task_path.is_file(): raise TrestleError( f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.' ) if not self.template_dir.exists(): self.template_dir.mkdir(exist_ok=True, parents=True) elif self.template_dir.is_file(): raise TrestleError( f'Template path: {self.rel_dir(self.template_dir)} is a file not a directory.' ) logger.debug(self.template_dir) if not self._validate_template_dir(): raise TrestleError('Aborting setup') template_file = self.template_dir / self.template_name if template_file.is_file(): return CmdReturnCodes.SUCCESS.value TemplateVersioning.write_versioned_template('template.md', self.template_dir, template_file, template_version) logger.info( f'Template file setup for task {self.task_name} at {self.rel_dir(template_file)}' ) logger.info(f'Task directory is {self.rel_dir(self.task_path)}') return CmdReturnCodes.SUCCESS.value
def relative_resolve(candidate: pathlib.Path, cwd: pathlib.Path) -> pathlib.Path: """Resolve a candidate file path relative to a provided cwd. This is to circumvent bad behaviour for resolve on windows platforms where the path must exist. If a relative dir is passed it presumes the directory is relative to the PROVIDED cwd. If relative expansions exist (e.g. ../) the final result must still be within the cwd. If an absolute path is provided it tests whether the path is within the cwd or not. """ # Expand user first if applicable. candidate = candidate.expanduser() if not cwd.is_absolute(): raise TrestleError('Error handling current working directory. CWD is expected to be absolute.') if not candidate.is_absolute(): new = pathlib.Path(cwd / candidate).resolve() else: new = candidate.resolve() try: new.relative_to(cwd) except ValueError: raise TrestleError(f'Provided dir {candidate} is not relative to {cwd}') return new
def parse_dict(data: Dict[str, Any], model_name: str) -> OscalBaseModel: """Load a model from the data dict. This functionality is provided for situations when the OSCAL data type is not known ahead of time. Here the model has been loaded into memory using json loads or similar and passed as a dict. Args: data: Oscal data loaded into memory as a dictionary with the `root key` removed. model_name: should be of the form 'module.class' from trestle.oscal.* modules Returns: The oscal model of the desired model. """ if data is None: raise TrestleError('data name is required') if model_name is None: raise TrestleError('model_name is required') parts = model_name.split('.') class_name = parts.pop() module_name = '.'.join(parts) logger.debug(f'Loading class "{class_name}" from "{module_name}"') module = importlib.import_module(module_name) mclass: OscalBaseModel = getattr(module, class_name) if mclass is None: raise TrestleError( f'class "{class_name}" could not be found in "{module_name}"') instance = mclass.parse_obj(data) return instance
def _load(self) -> None: """Load the file.""" if not self.file_path.exists() or self.file_path.is_dir(): raise TrestleError( f'Candidate drawio file {str(self.file_path)} does not exist or is a directory' ) try: self.raw_xml = defusedxml.ElementTree.parse(self.file_path, forbid_dtd=True) except Exception as e: raise TrestleError( f'Exception loading Element tree from file: {e}') self.mx_file = self.raw_xml.getroot() if not self.mx_file.tag == 'mxfile': raise TrestleError('DrawIO file is not a draw io file (mxfile)') self.diagrams = [] for diagram in list(self.mx_file): # Determine if compressed or not # Assumption 1 mxGraphModel n_children = len(list(diagram)) if n_children == 0: # Compressed object self.diagrams.append(self._uncompress(diagram.text)) elif n_children == 1: self.diagrams.append(list(diagram)[0]) else: raise TrestleError('Unhandled behaviour in drawio read.')
def __init__(self, root_dir: pathlib.Path, model_type: Type[OscalBaseModel], name: str) -> None: """Initialize repository OSCAL model object.""" if not file_utils.is_valid_project_root(root_dir): raise TrestleError(f'Provided root directory {str(root_dir)} is not a valid Trestle root directory.') self._root_dir = root_dir self._model_type = model_type self._model_name = name # set model alais and dir self.model_alias = classname_to_alias(self._model_type.__name__, AliasMode.JSON) if parser.to_full_model_name(self.model_alias) is None: raise TrestleError(f'Given model {self.model_alias} is not a top level model.') plural_path = ModelUtils.model_type_to_model_dir(self.model_alias) self.model_dir = self._root_dir / plural_path / self._model_name if not self.model_dir.exists() or not self.model_dir.is_dir(): raise TrestleError(f'Model dir {self._model_name} does not exist.') file_content_type = FileContentType.path_to_content_type(self.model_dir / self.model_alias) if file_content_type == FileContentType.UNKNOWN: raise TrestleError(f'Model file for model {self._model_name} does not exist.') self.file_content_type = file_content_type filepath = pathlib.Path( self.model_dir, self.model_alias + FileContentType.path_to_file_extension(self.model_dir / self.model_alias) ) self.filepath = filepath
def __init__( self, tmp_path: pathlib.Path, template_header: Dict, template_tree: MarkdownNode, validate_yaml_header: bool, validate_md_body: bool, md_header_to_validate: Optional[str] = None ): """Initialize markdown validator.""" self._validate_yaml_header = validate_yaml_header self._validate_md_body = validate_md_body self.md_header_to_validate = md_header_to_validate.strip(' ') if md_header_to_validate is not None else None self.template_header = template_header self.template_tree = template_tree self.template_path = tmp_path self.template_version = self.extract_template_version(self.template_header) if self.template_version not in str(self.template_path): raise TrestleError( f'Version of the template {self.template_version} does not match the path {self.template_path}.' + f'Move the template to the folder {self.template_version}' ) if 'Version' in self.template_header.keys() and self.template_header['Version'] != self.template_version: raise TrestleError(f'Version does not match template-version in template: {self.template_path}.') self._ignore_headers = [] for key in self.template_header.keys(): if key.lower().startswith('x-trestle-'): self._ignore_headers.append(key.lower()) if key.lower() == 'x-trestle-ignore': for key2 in template_header['x-trestle-ignore']: self._ignore_headers.append(key2.lower())
def _do_fetch(self) -> None: auth = None verify = None # This order reflects requests library behavior: REQUESTS_CA_BUNDLE comes first. for env_var_name in ['REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE']: if env_var_name in os.environ: if pathlib.Path(os.environ[env_var_name]).exists(): verify = os.environ[env_var_name] break else: err_str = f'Env var ${env_var_name} found but path does not exist: {os.environ[env_var_name]}' logger.warning(err_str) raise TrestleError(f'Cache update failure with bad inputenv var: {err_str}') if self._username is not None and self._password is not None: auth = HTTPBasicAuth(self._username, self._password) try: response = requests.get(self._url, auth=auth, verify=verify) except Exception as e: raise TrestleError(f'Cache update failure to connect via HTTPS: {self._url} ({e})') if response.status_code == 200: try: result = response.text except Exception as err: raise TrestleError(f'Cache update failure reading response via HTTPS: {self._url} ({err})') else: self._cached_object_path.write_text(result, encoding=const.FILE_ENCODING) else: raise TrestleError(f'GET returned code {response.status_code}: {self._uri}')
def __init__(self, trestle_root: pathlib.Path, uri: str) -> None: """Initialize SFTP fetcher. Update the expected cache path as per caching specs. Args: trestle_root: Path of the Trestle project path, i.e., within which .trestle is to be found. uri: Reference to the remote file to cache that can be fetched using the sftp:// scheme. """ logger.debug(f'initialize SFTPFetcher for uri {uri}') super().__init__(trestle_root, uri) # Is this a valid URI, however? Username and password are optional, of course. try: u = parse.urlparse(self._uri) except Exception as e: logger.warning(f'SFTP fetcher unable to parse uri {self._uri} error {e}') raise TrestleError(f'Unable to parse malformed url {self._uri} error {e}') logger.debug(f'SFTP fetcher with parsed uri {u}') if not u.hostname: logger.debug('SFTP fetcher uri missing hostname') logger.warning(f'Malformed URI, cannot parse hostname in URL {self._uri}') raise TrestleError(f'Cache request for invalid input URI: missing hostname {self._uri}') if not u.path: logger.debug('SFTP fetcher uri missing path') logger.warning(f'Malformed URI, cannot parse path in URL {self._uri}') raise TrestleError(f'Cache request for invalid input URI: missing file path {self._uri}') sftp_cached_dir = self._trestle_cache_path / u.hostname # Skip any number of back- or forward slashes preceding the URL path (u.path) path_parent = pathlib.Path(u.path[re.search('[^/\\\\]', u.path).span()[0]:]).parent sftp_cached_dir = sftp_cached_dir / path_parent sftp_cached_dir.mkdir(parents=True, exist_ok=True) self._cached_object_path = sftp_cached_dir / pathlib.Path(pathlib.Path(u.path).name)
def __init__(self, template_path: pathlib.Path, must_be_first_tab: bool = True) -> None: """ Initialize drawio validator. Args: template_path: Path to a templated drawio file where metadata will be looked up on the first tab only. must_be_first_tab: Whether to search the candidate file for a metadata across multiple tabs. """ self.template_path = template_path self.must_be_first_tab = must_be_first_tab # Load metadat from template template_drawio = DrawIO(self.template_path) # Zero index as must be first tab self.template_metadata = template_drawio.get_metadata()[0] self.template_version = MarkdownValidator.extract_template_version( self.template_metadata) if self.template_version not in str(self.template_path): raise TrestleError( f'Version of the template {self.template_version} does not match the path {self.template_path}.' + f'Move the template to the folder {self.template_version}') if 'Version' in self.template_metadata.keys( ) and self.template_metadata['Version'] != self.template_version: raise TrestleError( f'Version does not match template-version in template: {self.template_path}.' )
def set_at(self, element_path: ElementPath, sub_element: OscalBaseModel) -> 'Element': """Set a sub_element at the path in the current element. Sub element can be Element, OscalBaseModel, list or None type It returns the element itself so that chaining operation can be done such as `element.set_at(path, sub-element).get()`. """ # convert the element_path to ElementPath if needed if isinstance(element_path, str): element_path = ElementPath(element_path) # convert sub-element to OscalBaseModel if needed model_obj = self._get_sub_element_obj(sub_element) # find the root-model and element path parts _, path_parts = self._split_element_path(element_path) # TODO validate that self._elem is of same type as root_model # If wildcard is present, check the input type and determine the preceding element if element_path.get_last() == ElementPath.WILDCARD: # validate the type is either list or OscalBaseModel if not isinstance(model_obj, list) and not isinstance( model_obj, OscalBaseModel): raise TrestleError( f'The model object needs to be a List or OscalBaseModel for path with "{ElementPath.WILDCARD}"' ) # since wildcard * is there, we need to go one level up for preceding element in the path preceding_elm = self.get_preceding_element( element_path.get_preceding_path()) else: # get the preceding element in the path preceding_elm = self.get_preceding_element(element_path) if preceding_elm is None: raise TrestleError( f'Invalid sub element path {element_path} with no valid preceding element' ) # check if it can be a valid sub_element of the parent sub_element_name = element_path.get_element_name().replace('-', '_') if hasattr(preceding_elm, sub_element_name) is False: raise TrestleError( f'Element "{preceding_elm.__class__}" does not have the attribute "{sub_element_name}" ' f'of type "{model_obj.__class__}"') # set the sub-element try: setattr(preceding_elm, sub_element_name, model_obj) except ValidationError: sub_element_class = self.get_sub_element_class( preceding_elm, sub_element_name) raise TrestleError( f'Validation error: {sub_element_name} is expected to be "{sub_element_class}", ' f'but found "{model_obj.__class__}"') # returning self will allow to do 'chaining' of commands after set return self
def _check_if_exists_and_dir(task_path: Path) -> None: if not task_path.exists(): raise TrestleError(f'Path: {task_path} does not exists.') if not task_path.is_dir(): raise TrestleError( f'File {task_path} passed, however template directory is expected.' )
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 execute(self) -> None: """Execute the action.""" if self._element is None: raise TrestleError('Element is empty and cannot write') if not self._is_writer_valid(): raise TrestleError('Writer is not provided or closed') self._writer.write(self._encode()) self._writer.flush() self._mark_executed()
def write_drawio_with_metadata(self, path: pathlib.Path, metadata: Dict, diagram_metadata_idx: int, target_path: pathlib.Path = None) -> None: """ Write modified metadata to drawio file. Writes given metadata to 'object' element attributes inside of the selected drawio diagram element. Currently supports writing only uncompressed elements. Args: path: path to write modified drawio file to metadata: dictionary of modified metadata to insert to drawio diagram_metadata_idx: index of diagram which metadata was modified target_path: if not provided the changes will be written to path """ flattened_dict = self._flatten_dictionary(metadata) if diagram_metadata_idx >= len(list(self.diagrams)): raise TrestleError( f'Drawio file {path} does not contain a diagram for index {diagram_metadata_idx}' ) diagram = list(self.diagrams)[diagram_metadata_idx] children = list(diagram) root_obj = children[0] md_objects = root_obj.findall('object') if len(md_objects) == 0: raise TrestleError( f'Unable to write metadata, diagram in drawio file {path} does not have objects.' ) for key in md_objects[0].attrib.copy(): if key not in flattened_dict.keys( ) and key not in self.banned_keys: # outdated key delete del md_objects[0].attrib[key] continue if key in self.banned_keys: continue md_objects[0].attrib[key] = flattened_dict[key] for key in flattened_dict.keys(): if key in self.banned_keys: continue md_objects[0].attrib[key] = flattened_dict[key] parent_diagram = self.mx_file.findall('diagram')[diagram_metadata_idx] if len(parent_diagram.findall('mxGraphModel')) == 0: parent_diagram.insert(0, diagram) if target_path: self.raw_xml.write(target_path) else: self.raw_xml.write(path)
def write_versioned_template(resource_name: str, task_path: Path, target_file: Path, version: Optional[str] = None) -> None: """ Write a template with the header or metadata of a specified version. If no version was given the latest version for the task will be used. Args: resource_name: Template resource name task_path: Task path target_file: File path where template will be written version: return a resource of a specific version Returns: A dotted path of a versioned template, list of all available versions """ TemplateVersioning._check_if_exists_and_dir(task_path) try: templates_resource_path = TRESTLE_RESOURCES + '.templates' generic_template = Path( resource_filename(templates_resource_path, resource_name)).resolve() if version is None: _, version = TemplateVersioning.get_latest_version_for_task( task_path) # modify header/metadata in the template if generic_template.suffix == '.md': md_api = MarkdownAPI() header, md_body = md_api.processor.read_markdown_wo_processing( generic_template) header[TEMPLATE_VERSION_HEADER] = version md_api.write_markdown_with_header(target_file, header, md_body) logger.debug( f'Successfully written template markdown to {target_file}') elif generic_template.suffix == '.drawio': drawio = DrawIO(generic_template) metadata = drawio.get_metadata()[0] metadata[TEMPLATE_VERSION_HEADER] = version drawio.write_drawio_with_metadata(generic_template, metadata, 0, target_file) logger.debug( f'Successfully written template drawio to {target_file}') else: raise TrestleError( f'Unsupported template file extension {generic_template.suffix}' ) except OSError as e: raise TrestleError(f'Error while updating template folder: {e}')
def get_model(self, model_type: Type[OscalBaseModel], name: str) -> ManagedOSCAL: """Get a specific OSCAL model from repository.""" logger.debug(f'Getting model {name} of type {model_type.__name__}.') model_alias = classname_to_alias(model_type.__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.') plural_path = ModelUtils.model_type_to_model_dir(model_alias) desired_model_dir = self._root_dir / plural_path / name if not desired_model_dir.exists() or not desired_model_dir.is_dir(): raise TrestleError(f'Model {name} does not exist.') return ManagedOSCAL(self._root_dir, model_type, name)
def _parse(self, element_path: str) -> List[str]: """Parse the element path and validate.""" parts: List[str] = element_path.split(self.PATH_SEPARATOR) for part in parts: if part == '': raise TrestleError( f'Invalid path "{element_path}" because there are empty path parts between "{self.PATH_SEPARATOR}" ' 'or in the beginning') if parts[0] == self.WILDCARD: raise TrestleError(f'Invalid path {element_path} with wildcard.') return parts
def rollback(self) -> None: """Rollback the action.""" if not self._is_writer_valid(): raise TrestleError('Writer is not provided or closed') if self._lastStreamPos < 0: raise TrestleError( 'Last stream position is not available to rollback to') if self.has_executed(): self._writer.seek(self._lastStreamPos) self._writer.truncate() self._mark_rollback()
def _copy_config_file(self, root: pathlib.Path) -> None: """Copy the initial config.ini file to .trestle directory.""" try: source_path = pathlib.Path( resource_filename('trestle.resources', const.TRESTLE_CONFIG_FILE)).resolve() destination_path = (root / pathlib.Path(const.TRESTLE_CONFIG_DIR) / const.TRESTLE_CONFIG_FILE).resolve() copyfile(source_path, destination_path) except (shutil.SameFileError, OSError) as e: raise TrestleError(f'Error while copying config file: {e}') except Exception as e: raise TrestleError( f'Unexpected error while copying config file: {e}')
def create_trestle_project_with_model( top_dir: pathlib.Path, model_obj: OscalBaseModel, model_name: str, monkeypatch: MonkeyPatch ) -> pathlib.Path: """Create initialized trestle project and import the model into it.""" cur_dir = pathlib.Path.cwd() # create subdirectory for trestle project trestle_root = top_dir / 'my_trestle' trestle_root.mkdir() os.chdir(trestle_root) try: testargs = ['trestle', 'init'] monkeypatch.setattr(sys, 'argv', testargs) assert Trestle().run() == 0 # place model object in top directory outside trestle project # so it can be imported tmp_model_path = top_dir / (model_name + '.json') model_obj.oscal_write(tmp_model_path) i = ImportCmd() args = argparse.Namespace( trestle_root=trestle_root, file=str(tmp_model_path), output=model_name, verbose=0, regenerate=False ) assert i._run(args) == 0 except Exception as e: raise TrestleError(f'Error creating trestle project with model: {e}') finally: os.chdir(cur_dir) return trestle_root
def get_oscal_with_model_type(self, model_type: Type[OscalBaseModel], force_update=False) -> OscalBaseModel: """Retrieve the cached file as a particular OSCAL model. Arguments: model_type: Type[OscalBaseModel] Specifies the OSCAL model type of the fetched object. """ self._update_cache(force_update) cache_file = self._cached_object_path if not cache_file.exists(): raise TrestleError(f'get_oscal failure for {self._uri}') try: return model_type.oscal_read(cache_file) except Exception as e: logger.debug(f'get_oscal failed, error loading cache file for {self._uri} as {model_type}') raise TrestleError(f'get_oscal failure for {self._uri}: {e}.') from e
def load_top_level_model( trestle_root: pathlib.Path, model_name: str, model_class: Type[TopLevelOscalModel], file_content_type: Optional[FileContentType] = None ) -> Tuple[Union[OscalBaseModel, List[OscalBaseModel], Dict[ str, OscalBaseModel]], pathlib.Path]: """Load a model by name and model class and infer file content type if not specified. If you need to load an existing model but its content type may not be known, use this method. But the file content type should be specified if it is somehow known. """ root_model_path = ModelUtils._root_path_for_top_level_model( trestle_root, model_name, model_class) if file_content_type is None: file_content_type = FileContentType.path_to_content_type( root_model_path) if not FileContentType.is_readable_file(file_content_type): raise TrestleError( f'Unable to load model {model_name} without specifying json or yaml.' ) full_model_path = root_model_path.with_suffix( FileContentType.to_file_extension(file_content_type)) _, _, model = ModelUtils.load_distributed(full_model_path, trestle_root) return model, full_model_path
def setup(self, template_version: str) -> int: """Create template directory and templates.""" # Step 1 - validation if self.task_name and not self.task_path.exists(): self.task_path.mkdir(exist_ok=True, parents=True) elif self.task_name and self.task_path.is_file(): raise TrestleError( f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.' ) if not self.template_dir.exists(): self.template_dir.mkdir(exist_ok=True, parents=True) logger.info( f'Populating template files to {self.rel_dir(self.template_dir)}') for template in author_const.REFERENCE_TEMPLATES.values(): destination_path = self.template_dir / template TemplateVersioning.write_versioned_template( template, self.template_dir, destination_path, template_version) logger.info( f'Template directory populated {self.rel_dir(destination_path)}' ) return CmdReturnCodes.SUCCESS.value