1 # -*- coding: utf-8 -*-
3 # Copyright 2020 Whitestack, LLC
4 # *************************************************************
6 # This file is part of OSM common repository.
7 # All Rights Reserved to Whitestack, LLC
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
13 # http://www.apache.org/licenses/LICENSE-2.0
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
21 # For those usages not covered by the Apache License, Version 2.0 please
22 # contact: agarcia@whitestack.com
25 """Python module for interacting with ETSI GS NFV-SOL004 compliant packages
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:
31 SOL004 with metadata directory SOL004 without metadata directory
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
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
49 └── simple │ ├── install
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"
75 class SOL004PackageException(Exception):
80 def __init__(self
, package_path
=""):
81 self
._package
_path
= package_path
82 self
._package
_metadata
= self
._parse
_package
_metadata
()
83 self
._manifest
_data
= self
._parse
_manifest
_data
()
85 def _parse_package_metadata(self
):
87 return self
._parse
_package
_metadata
_with
_metadata
_dir
()
88 except FileNotFoundError
:
89 return self
._parse
_package
_metadata
_without
_metadata
_dir
()
91 def _parse_package_metadata_with_metadata_dir(self
):
93 return self
._parse
_file
_in
_blocks
(_METADATA_FILE_PATH
)
94 except FileNotFoundError
as e
:
96 except (Exception, OSError) as e
:
97 raise SOL004PackageException(
98 "Error parsing {}: {}".format(_METADATA_FILE_PATH
, e
)
101 def _parse_package_metadata_without_metadata_dir(self
):
102 package_root_files
= {f
for f
in os
.listdir(self
._package
_path
)}
103 package_root_yamls
= [
104 f
for f
in package_root_files
if f
.endswith(".yml") or f
.endswith(".yaml")
106 if len(package_root_yamls
) != 1:
107 error_msg
= "Error parsing package metadata: there should be exactly 1 descriptor YAML, found {}"
108 raise SOL004PackageException(error_msg
.format(len(package_root_yamls
)))
109 # TODO: Parse extra metadata from descriptor YAML?
112 _METADATA_DESCRIPTOR_FIELD
: package_root_yamls
[0],
113 _METADATA_MANIFEST_FIELD
: "{}.mf".format(
114 os
.path
.splitext(package_root_yamls
[0])[0]
116 _METADATA_CHANGELOG_FIELD
: _METADATA_DEFAULT_CHANGELOG_PATH
,
117 _METADATA_LICENSES_FIELD
: _METADATA_DEFAULT_LICENSES_PATH
,
121 def _parse_manifest_data(self
):
123 for tosca_meta
in self
._package
_metadata
:
124 if _METADATA_MANIFEST_FIELD
in tosca_meta
:
125 manifest_path
= tosca_meta
[_METADATA_MANIFEST_FIELD
]
128 error_msg
= "Error parsing {}: no {} field on path".format(
129 _METADATA_FILE_PATH
, _METADATA_MANIFEST_FIELD
131 raise SOL004PackageException(error_msg
)
134 return self
._parse
_file
_in
_blocks
(manifest_path
)
135 except (Exception, OSError) as e
:
136 raise SOL004PackageException(
137 "Error parsing {}: {}".format(manifest_path
, e
)
140 def _get_package_file_full_path(self
, file_relative_path
):
141 return os
.path
.join(self
._package
_path
, file_relative_path
)
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
:
146 blocks
= f
.read().split("\n\n")
147 parsed_blocks
= map(yaml
.safe_load
, blocks
)
148 return [block
for block
in parsed_blocks
if block
is not None]
150 def _get_package_file_manifest_data(self
, file_relative_path
):
151 for file_data
in self
._manifest
_data
:
152 if file_data
.get(_MANIFEST_FILE_PATH_FIELD
, "") == file_relative_path
:
156 "Error parsing {} manifest data: file not found on manifest file".format(
160 raise SOL004PackageException(error_msg
)
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
)
166 return file_manifest_data
[_MANIFEST_FILE_HASH_DIGEST_FIELD
]
167 except Exception as e
:
168 raise SOL004PackageException(
169 "Error parsing {} hash digest: {}".format(file_relative_path
, e
)
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
)
176 return file_manifest_data
[_MANIFEST_FILE_HASH_ALGORITHM_FIELD
]
177 except Exception as e
:
178 raise SOL004PackageException(
179 "Error parsing {} hash digest: {}".format(file_relative_path
, e
)
183 def _get_hash_function_from_hash_algorithm(hash_algorithm
):
184 function_to_algorithm
= {"SHA-256": hashlib
.sha256
, "SHA-512": hashlib
.sha512
}
185 if hash_algorithm
not in function_to_algorithm
:
187 "Error checking hash function: hash algorithm {} not supported".format(
191 raise SOL004PackageException(error_msg
)
192 return function_to_algorithm
[hash_algorithm
]
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
)
198 with
open(file_path
, "rb") as f
:
199 return hash_function(f
.read()).hexdigest()
200 except Exception as e
:
201 raise SOL004PackageException(
202 "Error hashing {}: {}".format(file_relative_path
, e
)
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."""
207 hash_algorithm
= self
.get_package_file_hash_algorithm_from_manifest(
210 file_hash
= self
._calculate
_file
_hash
(file_relative_path
, hash_algorithm
)
211 expected_file_hash
= self
.get_package_file_hash_digest_from_manifest(
214 if file_hash
!= expected_file_hash
:
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
)
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
)
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
]
233 error_msg
= "Error: no {} entry found on {}".format(
234 _METADATA_DESCRIPTOR_FIELD
, _METADATA_FILE_PATH
236 raise SOL004PackageException(error_msg
)