Update project maturity status
[multicloud/azure.git] / azure / aria / aria-extension-cloudify / src / aria / aria / cli / csar.py
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
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
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.
15
16 """
17 Support for the CSAR (Cloud Service ARchive) packaging specification.
18
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>`__
21 """
22
23 import os
24 import logging
25 import pprint
26 import tempfile
27 import zipfile
28
29 import requests
30 from ruamel import yaml
31
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'
41 BASE_METADATA = {
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,
45 }
46
47
48 def write(service_template_path, destination, logger):
49
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)
53
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):
72             for file in files:
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))
79
80
81 class _CSARReader(object):
82
83     def __init__(self, source, destination, logger):
84         self.logger = 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
90         if downloaded_csar:
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)
97         self.metadata = {}
98         try:
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))
104             self._extract()
105             self._read_metadata()
106             self._validate()
107         finally:
108             if downloaded_csar:
109                 os.remove(self.source)
110
111     @property
112     def created_by(self):
113         return self.metadata.get(META_CREATED_BY_KEY)
114
115     @property
116     def csar_version(self):
117         return self.metadata.get(META_CSAR_VERSION_KEY)
118
119     @property
120     def meta_file_version(self):
121         return self.metadata.get(META_FILE_VERSION_KEY)
122
123     @property
124     def entry_definitions(self):
125         return self.metadata.get(META_ENTRY_DEFINITIONS_KEY)
126
127     @property
128     def entry_definitions_yaml(self):
129         with open(os.path.join(self.destination, self.entry_definitions)) as f:
130             return yaml.load(f)
131
132     def _extract(self):
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')
139
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)))
149
150     def _validate(self):
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))
167
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):
176                 if chunk:
177                     f.write(chunk)
178
179
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)
184
185
186 def is_csar_archive(source):
187     return source.endswith(CSAR_FILE_EXTENSION)