From: Eduardo Sousa Date: Wed, 13 Feb 2019 12:28:56 +0000 (+0000) Subject: Adding an charm generator X-Git-Tag: v6.0.0~31^2 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=refs%2Fchanges%2F26%2F7226%2F5;p=osm%2Fdevops.git Adding an charm generator Current version only supports Ansible. It takes as input an empty charm with playbooks and generates the following files: - metadata.yaml - layers.yaml - reactive/.py - actions.yaml - actions/ After that, it is only required to build the charm. NOTE: In this version, it doesn't support parameters yet. Change-Id: Ia771185a38421a04c1adcef4ded3d7a55fa164ee Signed-off-by: Eduardo Sousa --- diff --git a/descriptor-packages/tools/charm-generator/LICENSE b/descriptor-packages/tools/charm-generator/LICENSE new file mode 100644 index 00000000..76d751a1 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Eduardo Sousa , + Gianpietro Lavado + + 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. diff --git a/descriptor-packages/tools/charm-generator/README.md b/descriptor-packages/tools/charm-generator/README.md new file mode 100644 index 00000000..fd8d7e0d --- /dev/null +++ b/descriptor-packages/tools/charm-generator/README.md @@ -0,0 +1,81 @@ +Charm Generator +=============== + +This tool was designed to help bootstrap the process of charm creation. +It should help beginners to have a charm working in a matter of minutes. + +The charm generator takes as input an initialized charm and some scripts +or description of HTTP requests and transforms it into a functional charm +with all the metadata filled in. + +The main flow is the following: + +1) charm create +2) copy the scripts or the requests definition into a specific directory +3) run the generator +4) charm build + +What types of charms can be generated? +-------------------------------------- + +Currently, the Charm Generator only supports Ansible scripts. + +In the roadmap, the following charms are planned: + +* HTTP Requests +* SOL002 Requests +* Python Scripts + +How to run the Charm Generator? +------------------------------- + +To run the Charm Generator, you should do the following steps: + +1) Download the OSM Devops git repository +2) Install the requirements.txt +3) Run the generator.py in the charm that you want to generate + +The only mandatory parameter is what type of charm you want to +generate: + +* Ansible (--ansible) +* HTTP Requests (--http) - not implemented +* SOL002 Requests (--sol002) - not implemented +* Python Scripts (--scripts) - not implemented + +There are some recommended parameters, such as: + +* Summary - summary of what the charm does (--summary) +* Maintainer - the name and email of the maintainer (--maintainer) +* Description - description of what the charm does (--description) +* Company - company name to be include in the license headers (--company) +* Email - email of the company/developer to be included in the license headers (--email) + +Specifics of the Ansible option +------------------------------- + +In order to create an Ansible charm, you need to create a directory +inside the charm and name it playbooks. Inside that directory, you +should put all the playbooks that are going to be executed in the charm. + +Note: the playbooks extension must be .yaml + +Your charm should look like this before running the generator: + +~~~ +. +├── config.yaml +├── icon.svg +├── layer.yaml +├── metadata.yaml +├── playbooks +│   ├── pb-1.yaml +│   ├── playbook2.yaml +│   └── playbook_3.yaml +├── reactive +│   └── testch.py +├── README.ex +└── tests + ├── 00-setup + └── 10-deploy +~~~ \ No newline at end of file diff --git a/descriptor-packages/tools/charm-generator/actions/__init__.py b/descriptor-packages/tools/charm-generator/actions/__init__.py new file mode 100644 index 00000000..e584ee55 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/actions/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## diff --git a/descriptor-packages/tools/charm-generator/actions/templates/action.j2 b/descriptor-packages/tools/charm-generator/actions/templates/action.j2 new file mode 100644 index 00000000..c1727c63 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/actions/templates/action.j2 @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +{#- +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +-#} +{%- if license is defined %} +# Copyright {{ license.year }} {{ license.company }} +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: {{ license.email }} +{%- endif %} + +import sys +sys.path.append('lib') + +from charms.reactive import main +from charms.reactive import set_state +from charmhelpers.core.hookenv import action_fail, action_name + +""" +`set_state` only works here because it's flushed to disk inside the `main()` +loop. remove_state will need to be called inside the action method. +""" +set_state('actions.{}'.format(action_name())) + +try: + main() +except Exception as e: + action_fail(repr(e)) + diff --git a/descriptor-packages/tools/charm-generator/actions/templates/actions.yaml.j2 b/descriptor-packages/tools/charm-generator/actions/templates/actions.yaml.j2 new file mode 100644 index 00000000..e9f29614 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/actions/templates/actions.yaml.j2 @@ -0,0 +1,44 @@ +{#- +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +-#} +{%- if license is defined -%} +# Copyright {{ license.year }} {{ license.company }} +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: {{ license.email }} +{% endif %} + +{%- for act in actions %} +{{ act.action_name }}: + description: "Insert description for this playbook." + {#- TODO: Insert parameters -#} + {#- TODO: Insert required -#} +{% endfor %} + diff --git a/descriptor-packages/tools/charm-generator/ansible-charm/__init__.py b/descriptor-packages/tools/charm-generator/ansible-charm/__init__.py new file mode 100644 index 00000000..e584ee55 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/ansible-charm/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## diff --git a/descriptor-packages/tools/charm-generator/ansible-charm/templates/ansible_charm.py.j2 b/descriptor-packages/tools/charm-generator/ansible-charm/templates/ansible_charm.py.j2 new file mode 100644 index 00000000..d9e2f551 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/ansible-charm/templates/ansible_charm.py.j2 @@ -0,0 +1,84 @@ +{#- +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +-#} +{%- if license is defined -%} +# Copyright {{ license.year }} {{ license.company }} +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: {{ license.email }} +{%- endif %} + + +import sys +import traceback + +from charmhelpers.core.hookenv import ( + action_get, + action_fail, + action_set, + config, + status_set, +) + +import charms.libansible + +from charms.reactive import ( + remove_state as remove_flag, + set_state as set_flag, + when, +) + + +# Sets the status of the charm to show in OSM: configured +@when('config.changed') +def config_changed(): + set_flag('{{ charm_name }}.configured') + status_set('active', 'ready!') + return + + +# Edits ansible config files and executes ansible-playbook +{% for pb in playbooks -%} +@when('{{ charm_name }}.configured') +@when('actions.{{ pb.action_name }}') +def {{ pb.function_name }}(): + try: + result = charms.libansible.execute_playbook('{{ pb.file }}') + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + err = traceback.format_exception(exc_type, exc_value, exc_traceback) + action_fail('{{ pb.action_name }} failed: ' + str(err)) + else: + action_set({'output': result}) + finally: + remove_flag('actions.{{ pb.action_name }}') + + +{% endfor -%} \ No newline at end of file diff --git a/descriptor-packages/tools/charm-generator/ansible-charm/templates/ansible_lib.py.j2 b/descriptor-packages/tools/charm-generator/ansible-charm/templates/ansible_lib.py.j2 new file mode 100644 index 00000000..109bae05 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/ansible-charm/templates/ansible_lib.py.j2 @@ -0,0 +1,93 @@ +{#- +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +-#} +{%- if license is defined -%} +# Copyright {{ license.year }} {{ license.company }} +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: {{ license.email }} +{%- endif %} + +import fnmatch +import os +import yaml +import subprocess + +from charmhelpers.core.hookenv import config + + +def create_hosts(cfg, hosts): + inventory_path = '/etc/ansible/hosts' + + with open(inventory_path, 'w') as f: + f.write('[{}]\n'.format(hosts)) + h1 = '{0} ansible_connection=ssh ansible_ssh_user={1} ansible_ssh_pass={2} ' \ + 'ansible_python_interpreter=/usr/bin/python3\n'.format(cfg['ssh-hostname'], cfg['ssh-username'], + cfg['ssh-password']) + f.write(h1) + + +def create_ansible_cfg(): + ansible_config_path = '/etc/ansible/ansible.cfg' + + with open(ansible_config_path, 'w') as f: + f.write('[defaults]\n') + f.write('host_key_checking = False\n') + + +# Function to find the playbook path +def find(pattern, path): + result = '' + for root, dirs, files in os.walk(path): + for name in files: + if fnmatch.fnmatch(name, pattern): + result = os.path.join(root, name) + return result + + +def execute_playbook(playbook_file, vars=None): + playbook_path = find(playbook_file, '/var/lib/juju/agents/') + + cfg = config() + + with open(playbook_path, 'r') as f: + playbook_data = yaml.load(f) + + hosts = 'all' + if 'hosts' in playbook_data[0].keys() and playbook_data[0]['hosts']: + hosts = playbook_data[0]['hosts'] + + create_ansible_cfg() + create_hosts(cfg, hosts) + + call = ['ansible-playbook', playbook_path] + result = subprocess.check_output(call) + + return result diff --git a/descriptor-packages/tools/charm-generator/ansible-charm/templates/layer.yaml.j2 b/descriptor-packages/tools/charm-generator/ansible-charm/templates/layer.yaml.j2 new file mode 100644 index 00000000..3b0fa839 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/ansible-charm/templates/layer.yaml.j2 @@ -0,0 +1,49 @@ +{#- +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +-#} +{%- if license is defined -%} +# Copyright {{ license.year }} {{ license.company }} +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: {{ license.email }} +{%- endif %} + +includes: +{% for lr in layers -%} +- "layer:{{ lr.name }}" +{% endfor -%} +options: +{%- for lr in layers if lr.options is defined %} + {{ lr.name }}: + {%- for op in lr.options %} + {{ op.name }}: {{ op.value }} + {%- endfor -%} +{% endfor %} + diff --git a/descriptor-packages/tools/charm-generator/generator.py b/descriptor-packages/tools/charm-generator/generator.py new file mode 100644 index 00000000..22ce1cba --- /dev/null +++ b/descriptor-packages/tools/charm-generator/generator.py @@ -0,0 +1,171 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## + +import argparse +import logging +import sys + +from datetime import datetime + +from generators.ansible_generator import AnsibleGenerator + + +def configure_logger(args): + global logger + logger = logging.getLogger() + + if args.verbose: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + handler = logging.StreamHandler(sys.stdout) + + if args.verbose: + handler.setLevel(logging.DEBUG) + else: + handler.setLevel(logging.INFO) + + formatter = logging.Formatter('[%(levelname)s] %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + +def verify_environment(args): + pass + + +def input_processing(): + parser = argparse.ArgumentParser(description='Charm generator for OSM VNFs') + + # Setting logger from INFO to DEBUG + parser.add_argument('-v', '--verbose', required=False, action='store_true', + help='increase output verbosity') + + # Backend selection + backend_parser = parser.add_mutually_exclusive_group(required=True) + + backend_parser.add_argument('--ansible', action='store_true', + help='generate an Ansible charm') + backend_parser.add_argument('--http', action='store_true', + help='generate a HTTP charm') + backend_parser.add_argument('--sol002', action='store_true', + help='generate a SOL002 charm') + backend_parser.add_argument('--scripts', action='store_true', + help='generate a Scripts charm') + + # Metadata inputs + metadata_parser = parser.add_argument_group('metadata') + + metadata_parser.add_argument('--summary', required=False, action='store', + help='summary to be included in the metadata.yaml') + metadata_parser.add_argument('--maintainer', required=False, action='store', + help='maintainer information to be included in the metadata.yaml') + metadata_parser.add_argument('--description', required=False, action='store', + help='description to be included in the metadata.yaml') + + # License header inputs + license_header_group = parser.add_argument_group('license_header') + + license_header_group.add_argument('--company', required=False, action='store', + help='company name to be included in the license headers') + license_header_group.add_argument('--email', required=False, action='store', + help='email to be included in the license headers') + + return parser.parse_args() + + +def process_args(args): + # Metadata information for metadata.yaml + metadata = {} + + if args.summary: + metadata['summary'] = args.summary + if args.maintainer: + metadata['maintainer'] = args.maintainer + if args.description: + metadata['description'] = args.description + + # Information for license headers + license = { + 'year': datetime.now().year + } + + if args.company: + license['company'] = args.company + if args.email: + license['email'] = args.email + + # Options to configure the backends + options = { + 'backend': None + } + + if args.ansible: + options['backend'] = 'ansible' + elif args.http: + options['backend'] = 'http' + elif args.sol002: + options['backend'] = 'sol002' + elif args.scripts: + options['backend'] = 'scripts' + + return metadata, license, options + + +def main(): + # getting the input from the user + args = input_processing() + + # configure the logger + configure_logger(args) + + logger.info('Starting generation process...') + + # verify if the environment is correct and the args are valid + verify_environment(args) + + # process data to input in generator + metadata, license, options = process_args(args) + + logger.debug('Metadata: %s', metadata) + logger.debug('License: %s', license) + logger.debug('Options: %s', options) + + if options['backend'] == 'ansible': + generator = AnsibleGenerator(metadata=metadata, license=license, options=options) + elif options['backend'] == 'http': + logger.error('HTTP backend not yet available. Available backends are: ansible') + sys.exit(-1) + elif options['backend'] == 'sol002': + logger.error('SOL002 backend not yet available. Available backends are: ansible') + sys.exit(-1) + elif options['backend'] == 'scripts': + logger.error('Scripts backend not yet available. Available backends are: ansible') + sys.exit(-1) + else: + logger.error('Undefined backend for generator. Available backends are: ansible') + sys.exit(-1) + + generator.generate() + + logger.info('Generation process complete.') + + +if __name__ == "__main__": + main() diff --git a/descriptor-packages/tools/charm-generator/generators/__init__.py b/descriptor-packages/tools/charm-generator/generators/__init__.py new file mode 100644 index 00000000..e584ee55 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/generators/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## diff --git a/descriptor-packages/tools/charm-generator/generators/actions_generator.py b/descriptor-packages/tools/charm-generator/generators/actions_generator.py new file mode 100644 index 00000000..832fd092 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/generators/actions_generator.py @@ -0,0 +1,130 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## + +import functools +import logging +import os +import stat + +from jinja2 import Environment, PackageLoader + + +class ActionsGenerator: + LOGGER = logging.getLogger() + ENV = Environment(loader=PackageLoader('actions', 'templates')) + + def __init__(self, metadata, actions, license=None, options=None): + """ + Creates the object to generate the actions folder, actions files and actions.yaml. + + Usage should be: + + 1) Create the object. + 2) Run the generate method. + + :param metadata: metadata information about the charm being generated. + :param actions: actions to be included in the charm. + :param license: information license to included in the charm being generated. + :param options: options to override the normal flow. + """ + self.path = os.getcwd() + self.metadata = metadata + self.actions = actions + self.license = license + self.options = options + + def generate(self): + """ + Generates the actions folder, actions files and actions.yaml. + """ + ActionsGenerator.LOGGER.info('Generating the actions...') + + self._create_actions_folder() + + for action in self.actions: + self._generate_action_file(action) + + self._generate_actions_yaml_file() + + ActionsGenerator.LOGGER.info('Generated the actions.') + + def _create_actions_folder(self): + """ + Creates the actions folder, where all the action files are placed. + These files are the entry point for the execution of the actions. + """ + ActionsGenerator.LOGGER.debug('Creating the actions folder...') + + actions_path = self.path + '/actions' + + if not os.path.isdir(actions_path): + os.mkdir(actions_path) + else: + ActionsGenerator.LOGGER.warning('Actions folder already exists.') + return + + ActionsGenerator.LOGGER.debug('Created actions folder.') + + def _generate_action_file(self, action): + """ + Generates the action file to act as entry point for a specific action. + + Note: the action file is made executable during this function. + + :param action: dictionary with information about the action + """ + ActionsGenerator.LOGGER.debug('Creating action file: %s...', action['action_name']) + + playbook_path = self.path + ('/actions/%s' % action['action_name']) + action_file_template = ActionsGenerator.ENV.get_template('action.j2') + + with open(playbook_path, "w") as f: + f.write(action_file_template.render(license=self.license)) + mode = os.fstat(f.fileno()).st_mode + mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + os.fchmod(f.fileno(), stat.S_IMODE(mode)) + + ActionsGenerator.LOGGER.debug('Created action file: %s.', action['action_name']) + + def _generate_actions_yaml_file(self): + """ + Generates the actions.yaml file from a template. + It takes all the playbook information and fills in the templates. + + Note: renames the old actions.yaml file with a .bkp extension, so the history is preserved. + """ + ActionsGenerator.LOGGER.debug('Creating actions.yaml...') + + actions_yaml_path = self.path + '/actions.yaml' + actions_template = ActionsGenerator.ENV.get_template('actions.yaml.j2') + + if os.path.isfile(actions_yaml_path): + ids = [int(f.split('.')[-1]) for f in os.listdir(self.path) if f.startswith('actions.yaml.bkp')] + + id = 0 + if ids: + id = functools.reduce(lambda x, y: x if (x > y) else y, ids) + id += 1 + + backup_actions_yaml_path = self.path + ('/actions.yaml.bkp.%02d' % id) + os.rename(actions_yaml_path, backup_actions_yaml_path) + + with open(actions_yaml_path, 'w') as f: + f.write(actions_template.render(actions=self.actions, license=self.license)) + + ActionsGenerator.LOGGER.debug('Created actions.yaml.') diff --git a/descriptor-packages/tools/charm-generator/generators/ansible_generator.py b/descriptor-packages/tools/charm-generator/generators/ansible_generator.py new file mode 100644 index 00000000..18f5a5a9 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/generators/ansible_generator.py @@ -0,0 +1,182 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## + +import functools +import logging +import os +import sys + +from jinja2 import Environment, PackageLoader + +from generators.actions_generator import ActionsGenerator +from generators.metadata_generator import MetadataGenerator + + +class AnsibleGenerator: + LOGGER = logging.getLogger() + ENV = Environment(loader=PackageLoader('ansible-charm', 'templates')) + + def __init__(self, metadata, license=None, options=None): + """ + Creates the object to generate the ansible charm from templates. + + Usage should be: + + 1) Create the object. + 2) Run the generate method. + + :param metadata: metadata information about the charm being generated. + :param license: information license to included in the charm being generated. + :param options: options to override the normal flow. + """ + self.path = os.getcwd() + self.metadata = metadata + self.license = license + self.options = options + self.playbooks = AnsibleGenerator._fetch_all_playbooks(self.path) + + playbooks_name = [playbook['file'] for playbook in self.playbooks] + AnsibleGenerator.LOGGER.debug('Playbooks found: %s', playbooks_name) + + self.metadata_generator = MetadataGenerator(metadata=self.metadata, license=self.license, options=self.options) + self.actions_generator = ActionsGenerator(metadata=self.metadata, actions=self.playbooks, license=self.license, + options=self.options) + + def generate(self): + """ + Generates the Ansible Charm using templates. + """ + AnsibleGenerator.LOGGER.info('Generating the Ansible Charm...') + + # Generating metadata.yaml + self.metadata_generator.generate() + self.metadata = self.metadata_generator.get_metadata() + + # Generating actions + self.actions_generator.generate() + + self._generate_ansible_lib() + self._generate_layer_yaml_file() + self._generate_reactive_file() + + AnsibleGenerator.LOGGER.info('Generated the Ansible Charm.') + + @staticmethod + def _fetch_all_playbooks(path): + """ + Walks over the playbooks directory, fetches the playbook's name. + It takes the file name and normalizes it to be used in the ansible charm. + + :param path: path of the root of the charm. + :return: a list of dictionaries with the information about the playbooks. + """ + playbooks_path = path + '/playbooks' + + if os.path.isdir(playbooks_path) and len(os.listdir(playbooks_path)) != 0: + filenames = os.listdir(playbooks_path) + + result = [] + for file in filenames: + info = { + 'action_name': file.replace('_', '-').replace('.yaml', ''), + 'function_name': file.replace('-', '_').replace('.yaml', ''), + 'file': file + } + result.append(info) + + return result + else: + AnsibleGenerator.LOGGER.error('Playbooks directory should exist and be populated.') + sys.exit(-1) + + def _generate_ansible_lib(self): + """ + Generates the ansible lib file from a template. + """ + AnsibleGenerator.LOGGER.debug('Creating ansible.py lib...') + + lib_folder_path = self.path + '/lib/charms' + ansible_lib_path = lib_folder_path + '/libansible.py' + + if not os.path.isdir(lib_folder_path): + os.makedirs(lib_folder_path) + + ansible_lib_template = AnsibleGenerator.ENV.get_template('ansible_lib.py.j2') + + with open(ansible_lib_path, 'w') as f: + f.write(ansible_lib_template.render(license=self.license)) + + AnsibleGenerator.LOGGER.debug('Created anisble.py lib.') + + def _generate_layer_yaml_file(self): + """ + Generates the layer.yaml file from a template. + + Note: disables the venv environment. + """ + AnsibleGenerator.LOGGER.debug('Creating layer.yaml...') + + layer_yaml_path = self.path + '/layer.yaml' + + layers = [{ + 'name': 'basic', + 'options': [{ + 'name': 'use_venv', + 'value': 'false' + }]}, { + 'name': 'ansible-base' + }, { + 'name': 'vnfproxy' + }] + + layer_template = AnsibleGenerator.ENV.get_template('layer.yaml.j2') + + with open(layer_yaml_path, 'w') as f: + f.write(layer_template.render(layers=layers, license=self.license)) + + AnsibleGenerator.LOGGER.debug('Created layer.yaml.') + + def _generate_reactive_file(self): + """ + Generates the Ansible reactive file from a template. + It takes all the playbook information and fills in the templates. + + Note: renames the old charm file with a .bkp extension, so the history is preserved. + """ + AnsibleGenerator.LOGGER.debug('Creating ansible charm: %s...', self.metadata['file']) + + reactive_folder_path = self.path + '/reactive' + charm_file_path = reactive_folder_path + ('/%s' % self.metadata['file']) + ansible_charm_template = AnsibleGenerator.ENV.get_template('ansible_charm.py.j2') + + ids = [int(f.split('.')[-1]) for f in os.listdir(reactive_folder_path) + if f.startswith('%s.bkp' % self.metadata['file'])] + + id = 0 + if ids: + id = functools.reduce(lambda x, y: x if (x > y) else y, ids) + id += 1 + + backup_charm_file_path = reactive_folder_path + ('/%s.bkp.%02d' % (self.metadata['file'], id)) + os.rename(charm_file_path, backup_charm_file_path) + + with open(charm_file_path, 'w') as f: + f.write(ansible_charm_template.render(charm_name=self.metadata['name'], playbooks=self.playbooks, + license=self.license)) + + AnsibleGenerator.LOGGER.debug('Created ansible charm: %s.', self.metadata['file']) diff --git a/descriptor-packages/tools/charm-generator/generators/metadata_generator.py b/descriptor-packages/tools/charm-generator/generators/metadata_generator.py new file mode 100644 index 00000000..8b0d9b8d --- /dev/null +++ b/descriptor-packages/tools/charm-generator/generators/metadata_generator.py @@ -0,0 +1,152 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## + +import functools +import logging +import os +import sys + +import yaml + +from jinja2 import Environment, PackageLoader + + +class MetadataGenerator: + LOGGER = logging.getLogger() + ENV = Environment(loader=PackageLoader('metadata', 'templates')) + + def __init__(self, metadata, license=None, options=None): + """ + Creates the object to generate the metadata.yaml file from a template. + + Usage should be: + + 1) Create the object. + 2) Run the generate method. + + :param metadata: metadata information about the charm being generated. + :param license: information license to included in the charm being generated. + :param options: options to override the normal flow. + """ + self.path = os.getcwd() + self.metadata = metadata + self.license = license + self.options = options + + def generate(self): + """ + Generates the metadata.yaml using templates. + """ + MetadataGenerator.LOGGER.info('Generating the metadata.yaml...') + + standard_metadata = self._read_metadata_yaml() + self._update_metadata(standard_metadata) + self._rename_metadata_yaml_as_backup() + self._write_metadata_yaml() + + MetadataGenerator.LOGGER.info('Generated the metadata.yaml.') + + def get_metadata(self): + """ + Gets the enhanced metadata. + + :return: the enhanced metadata dictionary. + """ + return self.metadata + + def _read_metadata_yaml(self): + """ + Reads the values from the old metadata.yaml and does a cleanup on undesired values. + """ + MetadataGenerator.LOGGER.debug('Reading old metadata.yaml...') + + metadata_yaml_path = self.path + '/metadata.yaml' + + if not os.path.isfile(metadata_yaml_path): + MetadataGenerator.LOGGER.error('metadata.yaml must be present. Must be run in the root of the charm') + sys.exit(-1) + + with open(metadata_yaml_path, 'r') as f: + metadata = yaml.load(f) + + if 'tags' in metadata.keys(): + del metadata['tags'] + if 'provides' in metadata.keys(): + del metadata['provides'] + if 'requires' in metadata.keys(): + del metadata['requires'] + if 'peers' in metadata.keys(): + del metadata['peers'] + + MetadataGenerator.LOGGER.debug('Read old metadata.yaml.') + + return metadata + + def _update_metadata(self, metadata): + """ + Update internal metadata before writing the new metadata.yaml. + + :param metadata: metadata values provided by the user. + """ + MetadataGenerator.LOGGER.debug('Generating the Ansible Charm...') + + if 'name' in metadata: + self.metadata['name'] = metadata['name'] + self.metadata['file'] = '%s.py' % metadata['name'].replace('-', '_') + + self.metadata['subordinate'] = False + self.metadata['tags'] = ['misc', 'osm', 'vnf'] + self.metadata['series'] = ['xenial', 'trusty'] + + MetadataGenerator.LOGGER.debug('Generating the Ansible Charm...') + + def _rename_metadata_yaml_as_backup(self): + """ + Renames the metadata.yaml to metadata.yaml.bkp.*. + Preserves the history of the charm for the user. + """ + MetadataGenerator.LOGGER.debug('Renaming the metadata.yaml to .bkp.*...') + + metadata_yaml_path = self.path + '/metadata.yaml' + + ids = [int(f.split('.')[-1]) for f in os.listdir(self.path) + if f.startswith('metadata.yaml.bkp')] + + id = 0 + if ids: + id = functools.reduce(lambda x, y: x if (x > y) else y, ids) + id += 1 + + backup_metadata_yaml_path = self.path + ('/metadata.yaml.bkp.%02d' % id) + os.rename(metadata_yaml_path, backup_metadata_yaml_path) + + MetadataGenerator.LOGGER.debug('Renamed the metadata.yaml to .bkp.%02d.', id) + + def _write_metadata_yaml(self): + """ + Writes the enriched metadata to metadata.yaml. + """ + MetadataGenerator.LOGGER.debug('Generating metadata.yaml...') + + metadata_yaml_path = self.path + '/metadata.yaml' + metadata_yaml_template = MetadataGenerator.ENV.get_template('metadata.yaml.j2') + + with open(metadata_yaml_path, 'w') as f: + f.write(metadata_yaml_template.render(metadata=self.metadata, license=self.license)) + + MetadataGenerator.LOGGER.debug('Generated metadata.yaml.') diff --git a/descriptor-packages/tools/charm-generator/metadata/__init__.py b/descriptor-packages/tools/charm-generator/metadata/__init__.py new file mode 100644 index 00000000..e584ee55 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/metadata/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## diff --git a/descriptor-packages/tools/charm-generator/metadata/templates/metadata.yaml.j2 b/descriptor-packages/tools/charm-generator/metadata/templates/metadata.yaml.j2 new file mode 100644 index 00000000..e1892444 --- /dev/null +++ b/descriptor-packages/tools/charm-generator/metadata/templates/metadata.yaml.j2 @@ -0,0 +1,50 @@ +{#- +# Copyright 2019 Whitestack, LLC +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +-#} +{%- if license is defined -%} +# Copyright {{ license.year }} {{ license.company }} +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: {{ license.email }} +{%- endif %} + +name: {{ metadata.name }} +summary: {{ metadata.summary }} +maintainer: {{ metadata.maintainer }} +description: {{ metadata.description }} +tags: +{% for tag in metadata.tags -%} +- {{ tag }} +{% endfor -%} +series: +{% for serie in metadata.series -%} +- {{ serie }} +{% endfor -%} +subordinate: {{ metadata.subordinate }} diff --git a/descriptor-packages/tools/charm-generator/requirements.txt b/descriptor-packages/tools/charm-generator/requirements.txt new file mode 100644 index 00000000..89ee2b0f --- /dev/null +++ b/descriptor-packages/tools/charm-generator/requirements.txt @@ -0,0 +1 @@ +Jinja2==2.10