Lianhao Lu | 432bca4 | 2018-03-24 22:36:08 +0800 | [diff] [blame] | 1 | # Copyright (c) 2018 Intel Corp. All rights reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 4 | # not use this file except in compliance with the License. You may obtain |
| 5 | # a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 12 | # License for the specific language governing permissions and limitations |
| 13 | # under the License. |
| 14 | # |
| 15 | |
| 16 | from collections import namedtuple |
| 17 | import os |
Lianhao Lu | cd02d1f | 2018-03-26 13:35:22 +0800 | [diff] [blame^] | 18 | import tempfile |
Lianhao Lu | 432bca4 | 2018-03-24 22:36:08 +0800 | [diff] [blame] | 19 | |
| 20 | import udatetime |
| 21 | |
| 22 | from vnfsdk_pkgtools.packager import utils |
| 23 | |
| 24 | METADATA_KEYS = [ 'vnf_provider_id', |
| 25 | 'vnf_product_name', |
| 26 | 'vnf_release_data_time', |
| 27 | 'vnf_package_version'] |
| 28 | DIGEST_KEYS = [ 'Source', 'Algorithm', 'Hash' ] |
| 29 | SUPPORTED_HASH_ALGO = ['SHA256', 'SHA512'] |
| 30 | |
| 31 | class ManifestException(Exception): |
| 32 | pass |
| 33 | |
| 34 | class Manifest(object): |
| 35 | ' Manifest file in CSAR package' |
| 36 | def __init__(self, root_path, manifest_path): |
| 37 | self.path = manifest_path |
| 38 | self.root = root_path |
| 39 | self.metadata = {} |
| 40 | # digest dict |
| 41 | # :key = source |
| 42 | # :value = (algorithm, hash) |
| 43 | self.digests = {} |
| 44 | self.signature = None |
| 45 | self.blocks = [ ] |
| 46 | self._split_blocks() |
| 47 | self._parse_blocks() |
| 48 | |
| 49 | @staticmethod |
| 50 | def __split_line(s): |
| 51 | remain=s |
| 52 | try: |
| 53 | (key, value)=s.split(':', 1) |
| 54 | value = value.strip() |
| 55 | remain = None |
| 56 | except ValueError: |
| 57 | key = None |
| 58 | value = None |
| 59 | return (key, value, remain) |
| 60 | |
| 61 | def _split_blocks(self): |
| 62 | ''' |
| 63 | Split manifest file into blocks, each block is seperated by a empty |
| 64 | line or a line with only spaces and tabs. |
| 65 | ''' |
| 66 | block_content = [ ] |
| 67 | with open(os.path.join(self.root, self.path),'rU') as fp: |
| 68 | for line in fp: |
| 69 | line = line.strip(' \t\n') |
| 70 | if line: |
| 71 | block_content.append(line) |
| 72 | else: |
| 73 | if len(block_content): |
| 74 | self.blocks.append(block_content) |
| 75 | block_content = [] |
| 76 | if len(block_content): |
| 77 | self.blocks.append(block_content) |
| 78 | |
| 79 | def _parse_blocks(self): |
| 80 | for block in self.blocks: |
| 81 | (key, value, remain) = self.__split_line(block.pop(0)) |
| 82 | if key == 'metadata': |
| 83 | # metadata block |
| 84 | for line in block: |
| 85 | (key, value, remain) = self.__split_line(line) |
| 86 | if key in METADATA_KEYS: |
| 87 | self.metadata[key] = value |
| 88 | else: |
| 89 | raise ManifestException("Unrecognized metadata %s:" % line) |
| 90 | #validate metadata keys |
| 91 | missing_keys = set(METADATA_KEYS) - set(self.metadata.keys()) |
| 92 | if missing_keys: |
| 93 | raise ManifestException("Missing metadata keys: %s" % ','.join(missing_keys)) |
| 94 | # validate vnf_release_data_time |
| 95 | try: |
| 96 | udatetime.from_string(self.metadata['vnf_release_data_time']) |
| 97 | except ValueError: |
| 98 | raise ManifestException("Non IETF RFC 3339 vnf_release_data_time: %s" |
| 99 | % self.metadata['vnf_release_data_time']) |
| 100 | elif key in DIGEST_KEYS: |
| 101 | # file digest block |
| 102 | desc = {} |
| 103 | desc[key] = value |
| 104 | for line in block: |
| 105 | (key, value, remain) = self.__split_line(line) |
| 106 | if key in DIGEST_KEYS: |
| 107 | desc[key] = value |
| 108 | else: |
| 109 | raise ManifestException("Unrecognized file digest line %s:" % line) |
| 110 | # validate file digest keys |
| 111 | missing_keys = set(DIGEST_KEYS) - set(desc.keys()) |
| 112 | if missing_keys: |
| 113 | raise ManifestException("Missing file digest keys: %s" % ','.join(missing_keys)) |
| 114 | # validate file digest algo |
| 115 | desc['Algorithm'] = desc['Algorithm'].upper() |
| 116 | if desc['Algorithm'] not in SUPPORTED_HASH_ALGO: |
| 117 | raise ManifestException("Unsupported hash algorithm: %s" % desc['Algorithm']) |
| 118 | # validate file digest hash |
| 119 | # TODO need to support remote file |
| 120 | if "://" not in desc['Source']: |
| 121 | hash = utils.cal_file_hash(self.root, desc['Source'], desc['Algorithm']) |
| 122 | if hash != desc['Hash']: |
| 123 | raise ManifestException("Mismatched hash for file %s" % desc['Source']) |
| 124 | # nothing is wrong, let's store this |
| 125 | self.digests[desc['Source']] = (desc['Algorithm'], desc['Hash']) |
| 126 | elif key: |
| 127 | raise ManifestException("Unknown key in line '%s:%s'" % (key, value)) |
| 128 | else: |
| 129 | # TODO signature block |
| 130 | pass |
| 131 | |
| 132 | if not self.metadata: |
| 133 | raise ManifestException("No metadata") |
| 134 | |
| 135 | def add_file(self, rel_path, algo='SHA256'): |
| 136 | '''Add file to the manifest and calculate the digest |
| 137 | ''' |
| 138 | algo = algo.upper() |
| 139 | if algo not in SUPPORTED_HASH_ALGO: |
| 140 | raise ManifestException("Unsupported hash algorithm: %s" % algo) |
| 141 | hash = utils.cal_file_hash(self.root, rel_path, algo) |
| 142 | self.digests[rel_path] = (algo, hash) |
| 143 | |
| 144 | def return_as_string(self): |
| 145 | '''Return the manifest file content as a string |
| 146 | ''' |
| 147 | ret = "" |
| 148 | # metadata |
| 149 | ret += "metadata:\n" |
| 150 | ret += "vnf_product_name: %s\n" % (self.metadata['vnf_product_name']) |
| 151 | ret += "vnf_provider_id: %s\n" % (self.metadata['vnf_provider_id']) |
| 152 | ret += "vnf_package_version: %s\n" % (self.metadata['vnf_package_version']) |
| 153 | ret += "vnf_release_data_time: %s\n" % (self.metadata['vnf_release_data_time']) |
| 154 | # degist |
| 155 | for (key, digest) in self.digests.iteritems(): |
| 156 | ret += "\n" |
| 157 | ret += "Source: %s\n" % key |
| 158 | ret += "Algorithm: %s\n" % digest[0] |
| 159 | ret += "Hash: %s\n" % digest[1] |
| 160 | return ret |
| 161 | |
Lianhao Lu | cd02d1f | 2018-03-26 13:35:22 +0800 | [diff] [blame^] | 162 | def update_to_file(self, temporary=False): |
Lianhao Lu | 432bca4 | 2018-03-24 22:36:08 +0800 | [diff] [blame] | 163 | content = self.return_as_string() |
Lianhao Lu | cd02d1f | 2018-03-26 13:35:22 +0800 | [diff] [blame^] | 164 | if temporary: |
| 165 | abs_path = tempfile.mktemp() |
| 166 | else: |
| 167 | abs_path = os.path.abspath(os.path.join(self.root, self.path)) |
| 168 | |
| 169 | with open(abs_path, 'w') as fp: |
Lianhao Lu | 432bca4 | 2018-03-24 22:36:08 +0800 | [diff] [blame] | 170 | fp.write(content) |
Lianhao Lu | cd02d1f | 2018-03-26 13:35:22 +0800 | [diff] [blame^] | 171 | return abs_path |