--- /dev/null
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# Copyright 2016 RIFT.io Inc
+
+
+import argparse
+import logging
+import logging.config
+import os
+import shutil
+import stat
+import subprocess
+import tempfile
+import zipfile
+
+import magic
+
+import yaml
+
+from rift.mano.tosca_translator.common.utils import _
+from rift.mano.tosca_translator.common.utils import ChecksumUtils
+from rift.mano.tosca_translator.rwmano.syntax.mano_template import ManoTemplate
+from rift.mano.tosca_translator.rwmano.tosca_translator import TOSCATranslator
+
+from toscaparser.tosca_template import ToscaTemplate
+
+
+"""
+Test the tosca translation from command line as:
+#translator
+ --template-file=<path to the YAML template or CSAR>
+ --template-type=<type of template e.g. tosca>
+ --parameters="purpose=test"
+ --output_dir=<output directory>
+ --archive
+ --validate_only
+Takes following user arguments,
+. Path to the file that needs to be translated (required)
+. Input parameters (optional)
+. Write to output files in a dir (optional), else print on screen
+. Create archive or not
+
+In order to use translator to only validate template,
+without actual translation, pass --validate-only along with
+other required arguments.
+
+"""
+
+
+class ToscaShellError(Exception):
+ pass
+
+
+class ToscaEntryFileError(ToscaShellError):
+ pass
+
+
+class ToscaNoEntryDefinitionError(ToscaShellError):
+ pass
+
+
+class ToscaEntryFileNotFoundError(ToscaShellError):
+ pass
+
+
+class ToscaCreateArchiveError(ToscaShellError):
+ pass
+
+
+class TranslatorShell(object):
+
+ SUPPORTED_TYPES = ['tosca']
+ COPY_DIRS = ['images']
+ SUPPORTED_INPUTS = (YAML, ZIP) = ('yaml', 'zip')
+
+ def __init__(self, log=None):
+ self.log = log
+
+ def main(self, raw_args=None):
+ args = self._parse_args(raw_args)
+
+ if self.log is None:
+ if args.debug:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ logging.basicConfig(level=logging.ERROR)
+ self.log = logging.getLogger("tosca-translator")
+
+ self.template_file = args.template_file
+
+ parsed_params = {}
+ if args.parameters:
+ parsed_params = self._parse_parameters(args.parameters)
+
+ self.archive = False
+ if args.archive:
+ self.archive = True
+
+ self.tmpdir = None
+
+ if args.validate_only:
+ a_file = os.path.isfile(args.template_file)
+ tpl = ToscaTemplate(self.template_file, parsed_params, a_file)
+ self.log.debug(_('Template = {}').format(tpl.__dict__))
+ msg = (_('The input {} successfully passed ' \
+ 'validation.').format(self.template_file))
+ print(msg)
+ else:
+ self.use_gi = not args.no_gi
+ tpl = self._translate("tosca", parsed_params)
+ if tpl:
+ return self._write_output(tpl, args.output_dir)
+
+ def translate(self,
+ template_file,
+ output_dir=None,
+ use_gi=True,
+ archive=False,):
+ self.template_file = template_file
+
+ # Check the input file
+ path = os.path.abspath(template_file)
+ self.in_file = path
+ a_file = os.path.isfile(path)
+ if not a_file:
+ msg = _("The path {0} is not a valid file.").format(template_file)
+ self.log.error(msg)
+ raise ValueError(msg)
+
+ # Get the file type
+ self.ftype = self._get_file_type()
+ self.log.debug(_("Input file {0} is of type {1}").
+ format(path, self.ftype))
+
+ self.archive = archive
+
+ self.tmpdir = None
+
+ self.use_gi = use_gi
+
+ tpl = self._translate("tosca", {})
+ if tpl:
+ return self._write_output(tpl, output_dir)
+
+ def _parse_args(self, raw_args=None):
+ parser = argparse.ArgumentParser(
+ description='RIFT TOSCA translator for descriptors')
+
+ parser.add_argument(
+ "-f",
+ "--template-file",
+ required=True,
+ help="Template file to translate")
+
+ parser.add_argument(
+ "-o",
+ "--output-dir",
+ help="Directory to output")
+
+ parser.add_argument(
+ "-p", "--parameters",
+ help="Input parameters")
+
+ parser.add_argument(
+ "-a", "--archive",
+ action="store_true",
+ help="Archive the translated files")
+
+ parser.add_argument(
+ "--no-gi",
+ help="Do not use the YANG GI to generate descriptors",
+ action="store_true")
+
+ parser.add_argument(
+ "--validate-only",
+ help="Validate template, no translation",
+ action="store_true")
+
+ parser.add_argument(
+ "--debug",
+ help="Enable debug logging",
+ action="store_true")
+
+ if raw_args:
+ args = parser.parse_args(raw_args)
+ else:
+ args = parser.parse_args()
+ return args
+
+ def _parse_parameters(self, parameter_list):
+ parsed_inputs = {}
+ if parameter_list:
+ # Parameters are semi-colon separated
+ inputs = parameter_list.replace('"', '').split(';')
+ # Each parameter should be an assignment
+ for param in inputs:
+ keyvalue = param.split('=')
+ # Validate the parameter has both a name and value
+ msg = _("'%(param)s' is not a well-formed parameter.") % {
+ 'param': param}
+ if keyvalue.__len__() is 2:
+ # Assure parameter name is not zero-length or whitespace
+ stripped_name = keyvalue[0].strip()
+ if not stripped_name:
+ self.log.error(msg)
+ raise ValueError(msg)
+ # Add the valid parameter to the dictionary
+ parsed_inputs[keyvalue[0]] = keyvalue[1]
+ else:
+ self.log.error(msg)
+ raise ValueError(msg)
+ return parsed_inputs
+
+ def get_entry_file(self):
+ # Extract the archive and get the entry file
+ if self.ftype == self.YAML:
+ return self.in_file
+
+ self.prefix = ''
+ if self.ftype == self.ZIP:
+ self.tmpdir = tempfile.mkdtemp()
+ prevdir = os.getcwd()
+ try:
+ with zipfile.ZipFile(self.in_file) as zf:
+ self.prefix = os.path.commonprefix(zf.namelist())
+ self.log.debug(_("Zipfile prefix is {0}").
+ format(self.prefix))
+ zf.extractall(self.tmpdir)
+
+ # Set the execute bits on scripts as zipfile
+ # does not restore the permissions bits
+ os.chdir(self.tmpdir)
+ for fname in zf.namelist():
+ if (fname.startswith('scripts/') and
+ os.path.isfile(fname)):
+ # Assume this is a script file
+ # Give all permissions to owner and read+execute
+ # for group and others
+ os.chmod(fname,
+ stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
+
+ # TODO (pjoseph): Use the below code instead of extract all
+ # once unzip is installed on launchpad VMs
+ # zfile = os.path.abspath(self.in_file)
+ # os.chdir(self.tmpdir)
+ # zip_cmd = "unzip {}".format(zfile)
+ # subprocess.check_call(zip_cmd,
+ # #stdout=subprocess.PIPE,
+ # #stderr=subprocess.PIPE,
+ # shell=True,)
+
+ except Exception as e:
+ msg = _("Exception extracting input file {0}: {1}"). \
+ format(self.in_file, e)
+ self.log.error(msg)
+ self.log.exception(e)
+ os.chdir(prevdir)
+ shutil.rmtree(self.tmpdir)
+ self.tmpdir = None
+ raise ToscaEntryFileError(msg)
+
+ os.chdir(self.tmpdir)
+
+ try:
+ # Goto the TOSAC Metadata file
+ prefix_dir = os.path.join(self.tmpdir, self.prefix)
+ meta_file = os.path.join(prefix_dir, 'TOSCA-Metadata',
+ 'TOSCA.meta')
+ self.log.debug(_("Checking metadata file {0}").format(meta_file))
+ if not os.path.exists(meta_file):
+ self.log.error(_("Not able to find metadata file in archive"))
+ return
+
+ # Open the metadata file and get the entry file
+ with open(meta_file, 'r') as f:
+ meta = yaml.load(f)
+
+ if 'Entry-Definitions' in meta:
+ entry_file = os.path.join(prefix_dir,
+ meta['Entry-Definitions'])
+ if os.path.exists(entry_file):
+ self.log.debug(_("TOSCA entry file is {0}").
+ format(entry_file))
+ return entry_file
+
+ else:
+ msg = _("Unable to get the entry file: {0}"). \
+ format(entry_file)
+ self.log.error(msg)
+ raise ToscaEntryFileNotFoundError(msg)
+
+ else:
+ msg = _("Did not find entry definition " \
+ "in metadata: {0}").format(meta)
+ self.log.error(msg)
+ raise ToscaNoEntryDefinitionError(msg)
+
+ except Exception as e:
+ msg = _('Exception parsing metadata file {0}: {1}'). \
+ format(meta_file, e)
+ self.log.error(msg)
+ self.log.exception(e)
+ raise ToscaEntryFileError(msg)
+
+ finally:
+ os.chdir(prevdir)
+
+ def _translate(self, sourcetype, parsed_params):
+ output = None
+
+ # Check the input file
+ path = os.path.abspath(self.template_file)
+ self.in_file = path
+ a_file = os.path.isfile(path)
+ if not a_file:
+ msg = _("The path {} is not a valid file."). \
+ format(self.template_file)
+ self.log.error(msg)
+ raise ValueError(msg)
+
+ # Get the file type
+ self.ftype = self._get_file_type()
+ self.log.debug(_("Input file {0} is of type {1}").
+ format(path, self.ftype))
+
+ if sourcetype == "tosca":
+ entry_file = self.get_entry_file()
+ if entry_file:
+ self.log.debug(_('Loading the tosca template.'))
+ tosca = ToscaTemplate(entry_file, parsed_params, True)
+ self.log.debug(_('TOSCA Template: {}').format(tosca.__dict__))
+ translator = TOSCATranslator(self.log, tosca, parsed_params,
+ use_gi=self.use_gi)
+ self.log.debug(_('Translating the tosca template.'))
+ output = translator.translate()
+ return output
+
+ def _copy_supporting_files(self, output_dir, files):
+ # Copy supporting files, if present in archive
+ if self.tmpdir:
+ # The files are refered relative to the definitions directory
+ arc_dir = os.path.join(self.tmpdir,
+ self.prefix,
+ 'Definitions')
+ prevdir = os.getcwd()
+ try:
+ os.chdir(arc_dir)
+ for fn in files:
+ fname = fn['name']
+ fpath = os.path.abspath(fname)
+ ty = fn['type']
+ if ty == 'image':
+ dest = os.path.join(output_dir, 'images')
+ elif ty == 'script':
+ dest = os.path.join(output_dir, 'scripts')
+ elif ty == 'cloud_init':
+ dest = os.path.join(output_dir, 'cloud_init')
+ else:
+ self.log.warn(_("Unknown file type {0} for {1}").
+ format(ty, fname))
+ continue
+
+ self.log.debug(_("File type {0} copy from {1} to {2}").
+ format(ty, fpath, dest))
+ if os.path.exists(fpath):
+ # Copy the files to the appropriate dir
+ self.log.debug(_("Copy file(s) {0} to {1}").
+ format(fpath, dest))
+ if os.path.isdir(fpath):
+ # Copy directory structure like charm dir
+ shutil.copytree(fpath, dest)
+ else:
+ # Copy a single file
+ os.makedirs(dest, exist_ok=True)
+ shutil.copy2(fpath, dest)
+
+ else:
+ self.log.warn(_("Could not find file {0} at {1}").
+ format(fname, fpath))
+
+ except Exception as e:
+ self.log.error(_("Exception copying files {0}: {1}").
+ format(arc_dir, e))
+ self.log.exception(e)
+
+ finally:
+ os.chdir(prevdir)
+
+ def _create_checksum_file(self, output_dir):
+ # Create checkum for all files
+ flist = {}
+ for root, dirs, files in os.walk(output_dir):
+ rel_dir = root.replace(output_dir, '').lstrip('/')
+
+ for f in files:
+ fpath = os.path.join(root, f)
+ # TODO (pjoseph): To be fixed when we can
+ # retrieve image files from Launchpad
+ if os.path.getsize(fpath) != 0:
+ flist[os.path.join(rel_dir, f)] = \
+ ChecksumUtils.get_md5(fpath)
+ self.log.debug(_("Files in output_dir: {}").format(flist))
+
+ chksumfile = os.path.join(output_dir, 'checksums.txt')
+ with open(chksumfile, 'w') as c:
+ for key in sorted(flist.keys()):
+ c.write("{} {}\n".format(flist[key], key))
+
+ def _create_archive(self, desc_id, output_dir):
+ """Create a tar.gz archive for the descriptor"""
+ aname = desc_id + '.tar.gz'
+ apath = os.path.join(output_dir, aname)
+ self.log.debug(_("Generating archive: {}").format(apath))
+
+ prevdir = os.getcwd()
+ os.chdir(output_dir)
+
+ # Generate the archive
+ tar_cmd = "tar zcvf {} {}".format(apath, desc_id)
+ self.log.debug(_("Generate archive: {}").format(tar_cmd))
+
+ try:
+ subprocess.check_call(tar_cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=True)
+ return apath
+
+ except subprocess.CalledProcessError as e:
+ msg = _("Error creating archive with {}: {}"). \
+ format(tar_cmd, e)
+ self.log.error(msg)
+ raise ToscaCreateArchiveError(msg)
+
+ finally:
+ os.chdir(prevdir)
+
+ def _write_output(self, output, output_dir=None):
+ out_files = []
+
+ if output_dir:
+ output_dir = os.path.abspath(output_dir)
+
+ if output:
+ # Do the VNFDs first and then NSDs as later when
+ # loading in launchpad, VNFDs need to be loaded first
+ for key in [ManoTemplate.VNFD, ManoTemplate.NSD]:
+ for desc in output[key]:
+ if output_dir:
+ desc_id = desc[ManoTemplate.ID]
+ # Create separate directories for each descriptors
+ # Use the descriptor id to avoid name clash
+ subdir = os.path.join(output_dir, desc_id)
+ os.makedirs(subdir)
+
+ output_file = os.path.join(subdir,
+ desc[ManoTemplate.NAME]+'.yml')
+ self.log.debug(_("Writing file {0}").
+ format(output_file))
+ with open(output_file, 'w+') as f:
+ f.write(desc[ManoTemplate.YANG])
+
+ if ManoTemplate.FILES in desc:
+ self._copy_supporting_files(subdir,
+ desc[ManoTemplate.FILES])
+
+ if self.archive:
+ # Create checksum file
+ self._create_checksum_file(subdir)
+ out_files.append(self._create_archive(desc_id,
+ output_dir))
+ # Remove the desc directory
+ shutil.rmtree(subdir)
+ else:
+ print(_("Descriptor {0}:\n{1}").
+ format(desc[ManoTemplate.NAME],
+ desc[ManoTemplate.YANG]))
+
+ if output_dir and self.archive:
+ # Return the list of archive files
+ return out_files
+
+ def _get_file_type(self):
+ m = magic.open(magic.MAGIC_MIME)
+ m.load()
+ typ = m.file(self.in_file)
+ if typ.startswith('text/plain'):
+ # Assume to be yaml
+ return self.YAML
+ elif typ.startswith('application/zip'):
+ return self.ZIP
+ else:
+ msg = _("The file {0} is not a supported type: {1}"). \
+ format(self.in_file, typ)
+ self.log.error(msg)
+ raise ValueError(msg)
+
+
+def main(args=None, log=None):
+ TranslatorShell(log=log).main(raw_args=args)
+
+
+if __name__ == '__main__':
+ main()