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('Error parsing {}: {}'.format(_METADATA_FILE_PATH
, e
))
99 def _parse_package_metadata_without_metadata_dir(self
):
100 package_root_files
= {f
for f
in os
.listdir(self
._package
_path
)}
101 package_root_yamls
= [f
for f
in package_root_files
if f
.endswith('.yml') or f
.endswith('.yaml')]
102 if len(package_root_yamls
) != 1:
103 error_msg
= 'Error parsing package metadata: there should be exactly 1 descriptor YAML, found {}'
104 raise SOL004PackageException(error_msg
.format(len(package_root_yamls
)))
105 # TODO: Parse extra metadata from descriptor YAML?
107 _METADATA_DESCRIPTOR_FIELD
: package_root_yamls
[0],
108 _METADATA_MANIFEST_FIELD
: '{}.mf'.format(os
.path
.splitext(package_root_yamls
[0])[0]),
109 _METADATA_CHANGELOG_FIELD
: _METADATA_DEFAULT_CHANGELOG_PATH
,
110 _METADATA_LICENSES_FIELD
: _METADATA_DEFAULT_LICENSES_PATH
113 def _parse_manifest_data(self
):
115 for tosca_meta
in self
._package
_metadata
:
116 if _METADATA_MANIFEST_FIELD
in tosca_meta
:
117 manifest_path
= tosca_meta
[_METADATA_MANIFEST_FIELD
]
120 error_msg
= 'Error parsing {}: no {} field on path'.format(_METADATA_FILE_PATH
, _METADATA_MANIFEST_FIELD
)
121 raise SOL004PackageException(error_msg
)
124 return self
._parse
_file
_in
_blocks
(manifest_path
)
125 except (Exception, OSError) as e
:
126 raise SOL004PackageException('Error parsing {}: {}'.format(manifest_path
, e
))
128 def _get_package_file_full_path(self
, file_relative_path
):
129 return os
.path
.join(self
._package
_path
, file_relative_path
)
131 def _parse_file_in_blocks(self
, file_relative_path
):
132 file_path
= self
._get
_package
_file
_full
_path
(file_relative_path
)
133 with
open(file_path
) as f
:
134 blocks
= f
.read().split('\n\n')
135 parsed_blocks
= map(yaml
.safe_load
, blocks
)
136 return [block
for block
in parsed_blocks
if block
is not None]
138 def _get_package_file_manifest_data(self
, file_relative_path
):
139 for file_data
in self
._manifest
_data
:
140 if file_data
.get(_MANIFEST_FILE_PATH_FIELD
, '') == file_relative_path
:
143 error_msg
= 'Error parsing {} manifest data: file not found on manifest file'.format(file_relative_path
)
144 raise SOL004PackageException(error_msg
)
146 def get_package_file_hash_digest_from_manifest(self
, file_relative_path
):
147 """Returns the hash digest of a file inside this package as specified on the manifest file."""
148 file_manifest_data
= self
._get
_package
_file
_manifest
_data
(file_relative_path
)
150 return file_manifest_data
[_MANIFEST_FILE_HASH_DIGEST_FIELD
]
151 except Exception as e
:
152 raise SOL004PackageException('Error parsing {} hash digest: {}'.format(file_relative_path
, e
))
154 def get_package_file_hash_algorithm_from_manifest(self
, file_relative_path
):
155 """Returns the hash algorithm of a file inside this package as specified on the manifest file."""
156 file_manifest_data
= self
._get
_package
_file
_manifest
_data
(file_relative_path
)
158 return file_manifest_data
[_MANIFEST_FILE_HASH_ALGORITHM_FIELD
]
159 except Exception as e
:
160 raise SOL004PackageException('Error parsing {} hash digest: {}'.format(file_relative_path
, e
))
163 def _get_hash_function_from_hash_algorithm(hash_algorithm
):
164 function_to_algorithm
= {
165 'SHA-256': hashlib
.sha256
,
166 'SHA-512': hashlib
.sha512
168 if hash_algorithm
not in function_to_algorithm
:
169 error_msg
= 'Error checking hash function: hash algorithm {} not supported'.format(hash_algorithm
)
170 raise SOL004PackageException(error_msg
)
171 return function_to_algorithm
[hash_algorithm
]
173 def _calculate_file_hash(self
, file_relative_path
, hash_algorithm
):
174 file_path
= self
._get
_package
_file
_full
_path
(file_relative_path
)
175 hash_function
= self
._get
_hash
_function
_from
_hash
_algorithm
(hash_algorithm
)
177 with
open(file_path
, "rb") as f
:
178 return hash_function(f
.read()).hexdigest()
179 except Exception as e
:
180 raise SOL004PackageException('Error hashing {}: {}'.format(file_relative_path
, e
))
182 def validate_package_file_hash(self
, file_relative_path
):
183 """Validates the integrity of a file using the hash algorithm and digest on the package manifest."""
184 hash_algorithm
= self
.get_package_file_hash_algorithm_from_manifest(file_relative_path
)
185 file_hash
= self
._calculate
_file
_hash
(file_relative_path
, hash_algorithm
)
186 expected_file_hash
= self
.get_package_file_hash_digest_from_manifest(file_relative_path
)
187 if file_hash
!= expected_file_hash
:
188 error_msg
= 'Error validating {} hash: calculated hash {} is different than manifest hash {}'
189 raise SOL004PackageException(error_msg
.format(file_relative_path
, file_hash
, expected_file_hash
))
191 def validate_package_hashes(self
):
192 """Validates the integrity of all files listed on the package manifest."""
193 for file_data
in self
._manifest
_data
:
194 if _MANIFEST_FILE_PATH_FIELD
in file_data
:
195 file_relative_path
= file_data
[_MANIFEST_FILE_PATH_FIELD
]
196 self
.validate_package_file_hash(file_relative_path
)
198 def get_descriptor_location(self
):
199 """Returns this package descriptor location as a relative path from the package root."""
200 for tosca_meta
in self
._package
_metadata
:
201 if _METADATA_DESCRIPTOR_FIELD
in tosca_meta
:
202 return tosca_meta
[_METADATA_DESCRIPTOR_FIELD
]
204 error_msg
= 'Error: no {} entry found on {}'.format(_METADATA_DESCRIPTOR_FIELD
, _METADATA_FILE_PATH
)
205 raise SOL004PackageException(error_msg
)