| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
| 3 | # Copyright 2020 Whitestack, LLC |
| 4 | # ************************************************************* |
| 5 | # |
| 6 | # This file is part of OSM common repository. |
| 7 | # All Rights Reserved to Whitestack, LLC |
| 8 | # |
| 9 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 10 | # not use this file except in compliance with the License. You may obtain |
| 11 | # a copy of the License at |
| 12 | # |
| 13 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 14 | # |
| 15 | # Unless required by applicable law or agreed to in writing, software |
| 16 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 17 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 18 | # License for the specific language governing permissions and limitations |
| 19 | # under the License. |
| 20 | # |
| 21 | # For those usages not covered by the Apache License, Version 2.0 please |
| 22 | # contact: agarcia@whitestack.com |
| 23 | ## |
| 24 | |
| 25 | """Python module for interacting with ETSI GS NFV-SOL004 compliant packages |
| 26 | |
| 27 | This module provides a SOL004Package class for validating and interacting with |
| 28 | ETSI SOL004 packages. A valid SOL004 package may have its files arranged according |
| 29 | to one of the following two structures: |
| 30 | |
| 31 | SOL004 with metadata directory SOL004 without metadata directory |
| 32 | |
| 33 | native_charm_vnf/ native_charm_vnf/ |
| 34 | ├── TOSCA-Metadata ├── native_charm_vnfd.mf |
| 35 | │ └── TOSCA.meta ├── native_charm_vnfd.yaml |
| 36 | ├── manifest.mf ├── ChangeLog.txt |
| 37 | ├── Definitions ├── Licenses |
| 38 | │ └── native_charm_vnfd.yaml │ └── license.lic |
| 39 | ├── Files ├── Files |
| 40 | │ ├── icons │ └── icons |
| 41 | │ │ └── osm.png │ └── osm.png |
| 42 | │ ├── Licenses └── Scripts |
| 43 | │ │ └── license.lic ├── cloud_init |
| 44 | │ └── changelog.txt │ └── cloud-config.txt |
| 45 | └── Scripts └── charms |
| 46 | ├── cloud_init └── simple |
| 47 | │ └── cloud-config.txt ├── config.yaml |
| 48 | └── charms ├── hooks |
| 49 | └── simple │ ├── install |
| 50 | ├── config.yaml ... |
| 51 | ├── hooks │ |
| 52 | │ ├── install └── src |
| 53 | ... └── charm.py |
| 54 | └── src |
| 55 | └── charm.py |
| 56 | """ |
| 57 | |
| 58 | import yaml |
| 59 | import os |
| 60 | import hashlib |
| 61 | |
| 62 | |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 63 | _METADATA_FILE_PATH = "TOSCA-Metadata/TOSCA.meta" |
| 64 | _METADATA_DESCRIPTOR_FIELD = "Entry-Definitions" |
| 65 | _METADATA_MANIFEST_FIELD = "ETSI-Entry-Manifest" |
| 66 | _METADATA_CHANGELOG_FIELD = "ETSI-Entry-Change-Log" |
| 67 | _METADATA_LICENSES_FIELD = "ETSI-Entry-Licenses" |
| 68 | _METADATA_DEFAULT_CHANGELOG_PATH = "ChangeLog.txt" |
| 69 | _METADATA_DEFAULT_LICENSES_PATH = "Licenses" |
| 70 | _MANIFEST_FILE_PATH_FIELD = "Source" |
| 71 | _MANIFEST_FILE_HASH_ALGORITHM_FIELD = "Algorithm" |
| 72 | _MANIFEST_FILE_HASH_DIGEST_FIELD = "Hash" |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 73 | |
| 74 | |
| 75 | class SOL004PackageException(Exception): |
| 76 | pass |
| 77 | |
| 78 | |
| 79 | class SOL004Package: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 80 | def __init__(self, package_path=""): |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 81 | self._package_path = package_path |
| 82 | self._package_metadata = self._parse_package_metadata() |
| 83 | self._manifest_data = self._parse_manifest_data() |
| 84 | |
| 85 | def _parse_package_metadata(self): |
| 86 | try: |
| 87 | return self._parse_package_metadata_with_metadata_dir() |
| 88 | except FileNotFoundError: |
| 89 | return self._parse_package_metadata_without_metadata_dir() |
| 90 | |
| 91 | def _parse_package_metadata_with_metadata_dir(self): |
| 92 | try: |
| 93 | return self._parse_file_in_blocks(_METADATA_FILE_PATH) |
| 94 | except FileNotFoundError as e: |
| 95 | raise e |
| 96 | except (Exception, OSError) as e: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 97 | raise SOL004PackageException( |
| 98 | "Error parsing {}: {}".format(_METADATA_FILE_PATH, e) |
| 99 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 100 | |
| 101 | def _parse_package_metadata_without_metadata_dir(self): |
| 102 | package_root_files = {f for f in os.listdir(self._package_path)} |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 103 | package_root_yamls = [ |
| 104 | f for f in package_root_files if f.endswith(".yml") or f.endswith(".yaml") |
| 105 | ] |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 106 | if len(package_root_yamls) != 1: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 107 | error_msg = "Error parsing package metadata: there should be exactly 1 descriptor YAML, found {}" |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 108 | raise SOL004PackageException(error_msg.format(len(package_root_yamls))) |
| 109 | # TODO: Parse extra metadata from descriptor YAML? |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 110 | return [ |
| 111 | { |
| 112 | _METADATA_DESCRIPTOR_FIELD: package_root_yamls[0], |
| 113 | _METADATA_MANIFEST_FIELD: "{}.mf".format( |
| 114 | os.path.splitext(package_root_yamls[0])[0] |
| 115 | ), |
| 116 | _METADATA_CHANGELOG_FIELD: _METADATA_DEFAULT_CHANGELOG_PATH, |
| 117 | _METADATA_LICENSES_FIELD: _METADATA_DEFAULT_LICENSES_PATH, |
| 118 | } |
| 119 | ] |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 120 | |
| 121 | def _parse_manifest_data(self): |
| 122 | manifest_path = None |
| 123 | for tosca_meta in self._package_metadata: |
| 124 | if _METADATA_MANIFEST_FIELD in tosca_meta: |
| 125 | manifest_path = tosca_meta[_METADATA_MANIFEST_FIELD] |
| 126 | break |
| 127 | else: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 128 | error_msg = "Error parsing {}: no {} field on path".format( |
| 129 | _METADATA_FILE_PATH, _METADATA_MANIFEST_FIELD |
| 130 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 131 | raise SOL004PackageException(error_msg) |
| 132 | |
| 133 | try: |
| 134 | return self._parse_file_in_blocks(manifest_path) |
| 135 | except (Exception, OSError) as e: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 136 | raise SOL004PackageException( |
| 137 | "Error parsing {}: {}".format(manifest_path, e) |
| 138 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 139 | |
| 140 | def _get_package_file_full_path(self, file_relative_path): |
| 141 | return os.path.join(self._package_path, file_relative_path) |
| 142 | |
| 143 | def _parse_file_in_blocks(self, file_relative_path): |
| 144 | file_path = self._get_package_file_full_path(file_relative_path) |
| 145 | with open(file_path) as f: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 146 | blocks = f.read().split("\n\n") |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 147 | parsed_blocks = map(yaml.safe_load, blocks) |
| 148 | return [block for block in parsed_blocks if block is not None] |
| 149 | |
| 150 | def _get_package_file_manifest_data(self, file_relative_path): |
| 151 | for file_data in self._manifest_data: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 152 | if file_data.get(_MANIFEST_FILE_PATH_FIELD, "") == file_relative_path: |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 153 | return file_data |
| 154 | |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 155 | error_msg = ( |
| 156 | "Error parsing {} manifest data: file not found on manifest file".format( |
| 157 | file_relative_path |
| 158 | ) |
| 159 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 160 | raise SOL004PackageException(error_msg) |
| 161 | |
| 162 | def get_package_file_hash_digest_from_manifest(self, file_relative_path): |
| 163 | """Returns the hash digest of a file inside this package as specified on the manifest file.""" |
| 164 | file_manifest_data = self._get_package_file_manifest_data(file_relative_path) |
| 165 | try: |
| 166 | return file_manifest_data[_MANIFEST_FILE_HASH_DIGEST_FIELD] |
| 167 | except Exception as e: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 168 | raise SOL004PackageException( |
| 169 | "Error parsing {} hash digest: {}".format(file_relative_path, e) |
| 170 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 171 | |
| 172 | def get_package_file_hash_algorithm_from_manifest(self, file_relative_path): |
| 173 | """Returns the hash algorithm of a file inside this package as specified on the manifest file.""" |
| 174 | file_manifest_data = self._get_package_file_manifest_data(file_relative_path) |
| 175 | try: |
| 176 | return file_manifest_data[_MANIFEST_FILE_HASH_ALGORITHM_FIELD] |
| 177 | except Exception as e: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 178 | raise SOL004PackageException( |
| 179 | "Error parsing {} hash digest: {}".format(file_relative_path, e) |
| 180 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 181 | |
| 182 | @staticmethod |
| 183 | def _get_hash_function_from_hash_algorithm(hash_algorithm): |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 184 | function_to_algorithm = {"SHA-256": hashlib.sha256, "SHA-512": hashlib.sha512} |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 185 | if hash_algorithm not in function_to_algorithm: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 186 | error_msg = ( |
| 187 | "Error checking hash function: hash algorithm {} not supported".format( |
| 188 | hash_algorithm |
| 189 | ) |
| 190 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 191 | raise SOL004PackageException(error_msg) |
| 192 | return function_to_algorithm[hash_algorithm] |
| 193 | |
| 194 | def _calculate_file_hash(self, file_relative_path, hash_algorithm): |
| 195 | file_path = self._get_package_file_full_path(file_relative_path) |
| 196 | hash_function = self._get_hash_function_from_hash_algorithm(hash_algorithm) |
| 197 | try: |
| 198 | with open(file_path, "rb") as f: |
| 199 | return hash_function(f.read()).hexdigest() |
| 200 | except Exception as e: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 201 | raise SOL004PackageException( |
| 202 | "Error hashing {}: {}".format(file_relative_path, e) |
| 203 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 204 | |
| 205 | def validate_package_file_hash(self, file_relative_path): |
| 206 | """Validates the integrity of a file using the hash algorithm and digest on the package manifest.""" |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 207 | hash_algorithm = self.get_package_file_hash_algorithm_from_manifest( |
| 208 | file_relative_path |
| 209 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 210 | file_hash = self._calculate_file_hash(file_relative_path, hash_algorithm) |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 211 | expected_file_hash = self.get_package_file_hash_digest_from_manifest( |
| 212 | file_relative_path |
| 213 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 214 | if file_hash != expected_file_hash: |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 215 | error_msg = "Error validating {} hash: calculated hash {} is different than manifest hash {}" |
| 216 | raise SOL004PackageException( |
| 217 | error_msg.format(file_relative_path, file_hash, expected_file_hash) |
| 218 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 219 | |
| 220 | def validate_package_hashes(self): |
| 221 | """Validates the integrity of all files listed on the package manifest.""" |
| 222 | for file_data in self._manifest_data: |
| 223 | if _MANIFEST_FILE_PATH_FIELD in file_data: |
| 224 | file_relative_path = file_data[_MANIFEST_FILE_PATH_FIELD] |
| 225 | self.validate_package_file_hash(file_relative_path) |
| 226 | |
| 227 | def get_descriptor_location(self): |
| 228 | """Returns this package descriptor location as a relative path from the package root.""" |
| 229 | for tosca_meta in self._package_metadata: |
| 230 | if _METADATA_DESCRIPTOR_FIELD in tosca_meta: |
| 231 | return tosca_meta[_METADATA_DESCRIPTOR_FIELD] |
| 232 | |
| garciadeblas | 2644b76 | 2021-03-24 09:21:01 +0100 | [diff] [blame] | 233 | error_msg = "Error: no {} entry found on {}".format( |
| 234 | _METADATA_DESCRIPTOR_FIELD, _METADATA_FILE_PATH |
| 235 | ) |
| garciaale | 0839503 | 2021-01-15 13:04:05 -0300 | [diff] [blame] | 236 | raise SOL004PackageException(error_msg) |