1 # Licensed to the Apache Software Foundation (ASF) under one or more
2 # contributor license agreements. See the NOTICE file distributed with
3 # this work for additional information regarding copyright ownership.
4 # The ASF licenses this file to You under the Apache License, Version 2.0
5 # (the "License"); you may not use this file except in compliance with
6 # the License. You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 Support for the CSAR (Cloud Service ARchive) packaging specification.
19 See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca
20 /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html#_Toc461787381>`__
30 from ruamel import yaml
32 CSAR_FILE_EXTENSION = '.csar'
33 META_FILE = 'TOSCA-Metadata/TOSCA.meta'
34 META_FILE_VERSION_KEY = 'TOSCA-Meta-File-Version'
35 META_FILE_VERSION_VALUE = '1.0'
36 META_CSAR_VERSION_KEY = 'CSAR-Version'
37 META_CSAR_VERSION_VALUE = '1.1'
38 META_CREATED_BY_KEY = 'Created-By'
39 META_CREATED_BY_VALUE = 'ARIA'
40 META_ENTRY_DEFINITIONS_KEY = 'Entry-Definitions'
42 META_FILE_VERSION_KEY: META_FILE_VERSION_VALUE,
43 META_CSAR_VERSION_KEY: META_CSAR_VERSION_VALUE,
44 META_CREATED_BY_KEY: META_CREATED_BY_VALUE,
48 def write(service_template_path, destination, logger):
50 service_template_path = os.path.abspath(os.path.expanduser(service_template_path))
51 source = os.path.dirname(service_template_path)
52 entry = os.path.basename(service_template_path)
54 meta_file = os.path.join(source, META_FILE)
55 if not os.path.isdir(source):
56 raise ValueError('{0} is not a directory. Please specify the service template '
57 'directory.'.format(source))
58 if not os.path.isfile(service_template_path):
59 raise ValueError('{0} does not exists. Please specify a valid entry point.'
60 .format(service_template_path))
61 if os.path.exists(destination):
62 raise ValueError('{0} already exists. Please provide a path to where the CSAR should be '
63 'created.'.format(destination))
64 if os.path.exists(meta_file):
65 raise ValueError('{0} already exists. This commands generates a meta file for you. Please '
66 'remove the existing metafile.'.format(meta_file))
67 metadata = BASE_METADATA.copy()
68 metadata[META_ENTRY_DEFINITIONS_KEY] = entry
69 logger.debug('Compressing root directory to ZIP')
70 with zipfile.ZipFile(destination, 'w', zipfile.ZIP_DEFLATED) as f:
71 for root, _, files in os.walk(source):
73 file_full_path = os.path.join(root, file)
74 file_relative_path = os.path.relpath(file_full_path, source)
75 logger.debug('Writing to archive: {0}'.format(file_relative_path))
76 f.write(file_full_path, file_relative_path)
77 logger.debug('Writing new metadata file to {0}'.format(META_FILE))
78 f.writestr(META_FILE, yaml.dump(metadata, default_flow_style=False))
81 class _CSARReader(object):
83 def __init__(self, source, destination, logger):
85 if os.path.isdir(destination) and os.listdir(destination):
86 raise ValueError('{0} already exists and is not empty. '
87 'Please specify the location where the CSAR '
88 'should be extracted.'.format(destination))
89 downloaded_csar = '://' in source
91 file_descriptor, download_target = tempfile.mkstemp()
92 os.close(file_descriptor)
93 self._download(source, download_target)
94 source = download_target
95 self.source = os.path.expanduser(source)
96 self.destination = os.path.expanduser(destination)
99 if not os.path.exists(self.source):
100 raise ValueError('{0} does not exists. Please specify a valid CSAR path.'
101 .format(self.source))
102 if not zipfile.is_zipfile(self.source):
103 raise ValueError('{0} is not a valid CSAR.'.format(self.source))
105 self._read_metadata()
109 os.remove(self.source)
112 def created_by(self):
113 return self.metadata.get(META_CREATED_BY_KEY)
116 def csar_version(self):
117 return self.metadata.get(META_CSAR_VERSION_KEY)
120 def meta_file_version(self):
121 return self.metadata.get(META_FILE_VERSION_KEY)
124 def entry_definitions(self):
125 return self.metadata.get(META_ENTRY_DEFINITIONS_KEY)
128 def entry_definitions_yaml(self):
129 with open(os.path.join(self.destination, self.entry_definitions)) as f:
133 self.logger.debug('Extracting CSAR contents')
134 if not os.path.exists(self.destination):
135 os.mkdir(self.destination)
136 with zipfile.ZipFile(self.source) as f:
137 f.extractall(self.destination)
138 self.logger.debug('CSAR contents successfully extracted')
140 def _read_metadata(self):
141 csar_metafile = os.path.join(self.destination, META_FILE)
142 if not os.path.exists(csar_metafile):
143 raise ValueError('Metadata file {0} is missing from the CSAR'.format(csar_metafile))
144 self.logger.debug('CSAR metadata file: {0}'.format(csar_metafile))
145 self.logger.debug('Attempting to parse CSAR metadata YAML')
146 with open(csar_metafile) as f:
147 self.metadata.update(yaml.load(f))
148 self.logger.debug('CSAR metadata:{0}{1}'.format(os.linesep, pprint.pformat(self.metadata)))
151 def validate_key(key, expected=None):
152 if not self.metadata.get(key):
153 raise ValueError('{0} is missing from the metadata file.'.format(key))
154 actual = str(self.metadata[key])
155 if expected and actual != expected:
156 raise ValueError('{0} is expected to be {1} in the metadata file while it is in '
157 'fact {2}.'.format(key, expected, actual))
158 validate_key(META_FILE_VERSION_KEY, expected=META_FILE_VERSION_VALUE)
159 validate_key(META_CSAR_VERSION_KEY, expected=META_CSAR_VERSION_VALUE)
160 validate_key(META_CREATED_BY_KEY)
161 validate_key(META_ENTRY_DEFINITIONS_KEY)
162 self.logger.debug('CSAR entry definitions: {0}'.format(self.entry_definitions))
163 entry_definitions_path = os.path.join(self.destination, self.entry_definitions)
164 if not os.path.isfile(entry_definitions_path):
165 raise ValueError('The entry definitions {0} referenced by the metadata file does not '
166 'exist.'.format(entry_definitions_path))
168 def _download(self, url, target):
169 response = requests.get(url, stream=True)
170 if response.status_code != 200:
171 raise ValueError('Server at {0} returned a {1} status code'
172 .format(url, response.status_code))
173 self.logger.info('Downloading {0} to {1}'.format(url, target))
174 with open(target, 'wb') as f:
175 for chunk in response.iter_content(chunk_size=8192):
180 def read(source, destination=None, logger=None):
181 destination = destination or tempfile.mkdtemp()
182 logger = logger or logging.getLogger('dummy')
183 return _CSARReader(source=source, destination=destination, logger=logger)
186 def is_csar_archive(source):
187 return source.endswith(CSAR_FILE_EXTENSION)