blob: e6b40b450d58d46bc63c25da2e96662a13cab03a [file] [log] [blame]
garciaale08395032021-01-15 13:04:05 -03001# -*- 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
27This module provides a SOL004Package class for validating and interacting with
28ETSI SOL004 packages. A valid SOL004 package may have its files arranged according
29to one of the following two structures:
30
31SOL004 with metadata directory SOL004 without metadata directory
32
33native_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
58import yaml
59import os
60import hashlib
61
62
garciadeblas2644b762021-03-24 09:21:01 +010063_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"
garciaale08395032021-01-15 13:04:05 -030073
74
75class SOL004PackageException(Exception):
76 pass
77
78
79class SOL004Package:
garciadeblas2644b762021-03-24 09:21:01 +010080 def __init__(self, package_path=""):
garciaale08395032021-01-15 13:04:05 -030081 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:
garciadeblas2644b762021-03-24 09:21:01 +010097 raise SOL004PackageException(
98 "Error parsing {}: {}".format(_METADATA_FILE_PATH, e)
99 )
garciaale08395032021-01-15 13:04:05 -0300100
101 def _parse_package_metadata_without_metadata_dir(self):
102 package_root_files = {f for f in os.listdir(self._package_path)}
garciadeblas2644b762021-03-24 09:21:01 +0100103 package_root_yamls = [
104 f for f in package_root_files if f.endswith(".yml") or f.endswith(".yaml")
105 ]
garciaale08395032021-01-15 13:04:05 -0300106 if len(package_root_yamls) != 1:
garciadeblas2644b762021-03-24 09:21:01 +0100107 error_msg = "Error parsing package metadata: there should be exactly 1 descriptor YAML, found {}"
garciaale08395032021-01-15 13:04:05 -0300108 raise SOL004PackageException(error_msg.format(len(package_root_yamls)))
109 # TODO: Parse extra metadata from descriptor YAML?
garciadeblas2644b762021-03-24 09:21:01 +0100110 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 ]
garciaale08395032021-01-15 13:04:05 -0300120
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:
garciadeblas2644b762021-03-24 09:21:01 +0100128 error_msg = "Error parsing {}: no {} field on path".format(
129 _METADATA_FILE_PATH, _METADATA_MANIFEST_FIELD
130 )
garciaale08395032021-01-15 13:04:05 -0300131 raise SOL004PackageException(error_msg)
132
133 try:
134 return self._parse_file_in_blocks(manifest_path)
135 except (Exception, OSError) as e:
garciadeblas2644b762021-03-24 09:21:01 +0100136 raise SOL004PackageException(
137 "Error parsing {}: {}".format(manifest_path, e)
138 )
garciaale08395032021-01-15 13:04:05 -0300139
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:
garciadeblas2644b762021-03-24 09:21:01 +0100146 blocks = f.read().split("\n\n")
garciaale08395032021-01-15 13:04:05 -0300147 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:
garciadeblas2644b762021-03-24 09:21:01 +0100152 if file_data.get(_MANIFEST_FILE_PATH_FIELD, "") == file_relative_path:
garciaale08395032021-01-15 13:04:05 -0300153 return file_data
154
garciadeblas2644b762021-03-24 09:21:01 +0100155 error_msg = (
156 "Error parsing {} manifest data: file not found on manifest file".format(
157 file_relative_path
158 )
159 )
garciaale08395032021-01-15 13:04:05 -0300160 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:
garciadeblas2644b762021-03-24 09:21:01 +0100168 raise SOL004PackageException(
169 "Error parsing {} hash digest: {}".format(file_relative_path, e)
170 )
garciaale08395032021-01-15 13:04:05 -0300171
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:
garciadeblas2644b762021-03-24 09:21:01 +0100178 raise SOL004PackageException(
179 "Error parsing {} hash digest: {}".format(file_relative_path, e)
180 )
garciaale08395032021-01-15 13:04:05 -0300181
182 @staticmethod
183 def _get_hash_function_from_hash_algorithm(hash_algorithm):
garciadeblas2644b762021-03-24 09:21:01 +0100184 function_to_algorithm = {"SHA-256": hashlib.sha256, "SHA-512": hashlib.sha512}
garciaale08395032021-01-15 13:04:05 -0300185 if hash_algorithm not in function_to_algorithm:
garciadeblas2644b762021-03-24 09:21:01 +0100186 error_msg = (
187 "Error checking hash function: hash algorithm {} not supported".format(
188 hash_algorithm
189 )
190 )
garciaale08395032021-01-15 13:04:05 -0300191 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:
garciadeblas2644b762021-03-24 09:21:01 +0100201 raise SOL004PackageException(
202 "Error hashing {}: {}".format(file_relative_path, e)
203 )
garciaale08395032021-01-15 13:04:05 -0300204
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."""
garciadeblas2644b762021-03-24 09:21:01 +0100207 hash_algorithm = self.get_package_file_hash_algorithm_from_manifest(
208 file_relative_path
209 )
garciaale08395032021-01-15 13:04:05 -0300210 file_hash = self._calculate_file_hash(file_relative_path, hash_algorithm)
garciadeblas2644b762021-03-24 09:21:01 +0100211 expected_file_hash = self.get_package_file_hash_digest_from_manifest(
212 file_relative_path
213 )
garciaale08395032021-01-15 13:04:05 -0300214 if file_hash != expected_file_hash:
garciadeblas2644b762021-03-24 09:21:01 +0100215 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 )
garciaale08395032021-01-15 13:04:05 -0300219
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
garciadeblas2644b762021-03-24 09:21:01 +0100233 error_msg = "Error: no {} entry found on {}".format(
234 _METADATA_DESCRIPTOR_FIELD, _METADATA_FILE_PATH
235 )
garciaale08395032021-01-15 13:04:05 -0300236 raise SOL004PackageException(error_msg)