From e13e89775f7da2e26f254da18d513704691f5729 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 26 Oct 2015 15:53:33 +0100 Subject: [PATCH 001/509] Add TOSCA support to IM --- IM/InfrastructureManager.py | 17 +- IM/tosca/Tosca.py | 880 +++++++++++++++++ IM/tosca/__init__.py | 16 + IM/tosca/artifacts/apache/apache_install.yml | 26 + .../artifacts/galaxy/galaxy_configure.yml | 25 + IM/tosca/artifacts/galaxy/galaxy_install.yml | 9 + IM/tosca/artifacts/galaxy/galaxy_start.yml | 6 + .../galaxy/galaxy_tools_configure.yml | 34 + IM/tosca/artifacts/mysql/mysql_configure.yml | 4 + .../artifacts/mysql/mysql_db_configure.yml | 5 + IM/tosca/artifacts/mysql/mysql_install.yml | 27 + IM/tosca/toscaparser/__init__.py | 19 + IM/tosca/toscaparser/capabilities.py | 57 ++ IM/tosca/toscaparser/common/__init__.py | 0 IM/tosca/toscaparser/common/exception.py | 100 ++ IM/tosca/toscaparser/dataentity.py | 159 ++++ .../elements/TOSCA_definition_1_0.yaml | 893 ++++++++++++++++++ IM/tosca/toscaparser/elements/__init__.py | 0 IM/tosca/toscaparser/elements/artifacttype.py | 45 + .../elements/attribute_definition.py | 20 + .../toscaparser/elements/capabilitytype.py | 71 ++ IM/tosca/toscaparser/elements/constraints.py | 569 +++++++++++ IM/tosca/toscaparser/elements/datatype.py | 56 ++ IM/tosca/toscaparser/elements/entity_type.py | 113 +++ IM/tosca/toscaparser/elements/interfaces.py | 74 ++ IM/tosca/toscaparser/elements/nodetype.py | 200 ++++ IM/tosca/toscaparser/elements/policytype.py | 45 + .../elements/property_definition.py | 46 + .../toscaparser/elements/relationshiptype.py | 33 + IM/tosca/toscaparser/elements/scalarunit.py | 130 +++ .../elements/statefulentitytype.py | 81 ++ IM/tosca/toscaparser/entity_template.py | 285 ++++++ IM/tosca/toscaparser/functions.py | 410 ++++++++ IM/tosca/toscaparser/groups.py | 27 + IM/tosca/toscaparser/nodetemplate.py | 242 +++++ IM/tosca/toscaparser/parameters.py | 110 +++ IM/tosca/toscaparser/prereq/__init__.py | 0 IM/tosca/toscaparser/prereq/csar.py | 122 +++ IM/tosca/toscaparser/properties.py | 79 ++ IM/tosca/toscaparser/relationship_template.py | 68 ++ IM/tosca/toscaparser/topology_template.py | 213 +++++ IM/tosca/toscaparser/tosca_template.py | 190 ++++ .../toscaparser/tpl_relationship_graph.py | 46 + IM/tosca/toscaparser/utils/__init__.py | 0 IM/tosca/toscaparser/utils/gettextutils.py | 22 + IM/tosca/toscaparser/utils/urlutils.py | 43 + IM/tosca/toscaparser/utils/validateutils.py | 154 +++ IM/tosca/toscaparser/utils/yamlparser.py | 73 ++ examples/galaxy_tosca.yml | 38 + examples/tosca.yml | 118 +++ 50 files changed, 5998 insertions(+), 2 deletions(-) create mode 100644 IM/tosca/Tosca.py create mode 100644 IM/tosca/__init__.py create mode 100755 IM/tosca/artifacts/apache/apache_install.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_configure.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_install.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_start.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml create mode 100755 IM/tosca/artifacts/mysql/mysql_configure.yml create mode 100644 IM/tosca/artifacts/mysql/mysql_db_configure.yml create mode 100755 IM/tosca/artifacts/mysql/mysql_install.yml create mode 100644 IM/tosca/toscaparser/__init__.py create mode 100644 IM/tosca/toscaparser/capabilities.py create mode 100644 IM/tosca/toscaparser/common/__init__.py create mode 100644 IM/tosca/toscaparser/common/exception.py create mode 100644 IM/tosca/toscaparser/dataentity.py create mode 100644 IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml create mode 100644 IM/tosca/toscaparser/elements/__init__.py create mode 100644 IM/tosca/toscaparser/elements/artifacttype.py create mode 100644 IM/tosca/toscaparser/elements/attribute_definition.py create mode 100644 IM/tosca/toscaparser/elements/capabilitytype.py create mode 100644 IM/tosca/toscaparser/elements/constraints.py create mode 100644 IM/tosca/toscaparser/elements/datatype.py create mode 100644 IM/tosca/toscaparser/elements/entity_type.py create mode 100644 IM/tosca/toscaparser/elements/interfaces.py create mode 100644 IM/tosca/toscaparser/elements/nodetype.py create mode 100644 IM/tosca/toscaparser/elements/policytype.py create mode 100644 IM/tosca/toscaparser/elements/property_definition.py create mode 100644 IM/tosca/toscaparser/elements/relationshiptype.py create mode 100644 IM/tosca/toscaparser/elements/scalarunit.py create mode 100644 IM/tosca/toscaparser/elements/statefulentitytype.py create mode 100644 IM/tosca/toscaparser/entity_template.py create mode 100644 IM/tosca/toscaparser/functions.py create mode 100644 IM/tosca/toscaparser/groups.py create mode 100644 IM/tosca/toscaparser/nodetemplate.py create mode 100644 IM/tosca/toscaparser/parameters.py create mode 100644 IM/tosca/toscaparser/prereq/__init__.py create mode 100644 IM/tosca/toscaparser/prereq/csar.py create mode 100644 IM/tosca/toscaparser/properties.py create mode 100644 IM/tosca/toscaparser/relationship_template.py create mode 100644 IM/tosca/toscaparser/topology_template.py create mode 100644 IM/tosca/toscaparser/tosca_template.py create mode 100644 IM/tosca/toscaparser/tpl_relationship_graph.py create mode 100644 IM/tosca/toscaparser/utils/__init__.py create mode 100644 IM/tosca/toscaparser/utils/gettextutils.py create mode 100644 IM/tosca/toscaparser/utils/urlutils.py create mode 100644 IM/tosca/toscaparser/utils/validateutils.py create mode 100644 IM/tosca/toscaparser/utils/yamlparser.py create mode 100644 examples/galaxy_tosca.yml create mode 100644 examples/tosca.yml diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 6a47f5efc..2cf4a9c76 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -34,6 +34,7 @@ from IM.radl.radl import Feature from IM.recipe import Recipe from IM.db import DataBase +from IM.tosca.Tosca import Tosca from config import Config @@ -354,10 +355,22 @@ def AddResource(inf_id, radl_data, auth, context = True, failed_clouds = []): InfrastructureManager.logger.info("Adding resources to inf: " + str(inf_id)) - radl = radl_parse.parse_radl(radl_data) - radl.check() + # TODO: Think about CSAR files using xmlrpclib.Binary o enconding a file using b64 + # see: http://stackoverflow.com/questions/9099174/send-file-from-client-to-server-using-xmlrpc + # We must save the file, unzip it and get the file pointed by: Entry-Definitions: some.yaml + # http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/csd03/TOSCA-Simple-Profile-YAML-v1.0-csd03.html#_Toc419746172 + if Tosca.is_tosca(radl_data): + try: + tosca = Tosca(radl_data) + radl = tosca.to_radl() + except Exception, ex: + InfrastructureManager.logger.exception("Error parsing TOSCA input data.") + raise Exception("Error parsing TOSCA input data: " + str(ex)) + else: + radl = radl_parse.parse_radl(radl_data) InfrastructureManager.logger.debug(radl) + radl.check() sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py new file mode 100644 index 000000000..bfd6df0ab --- /dev/null +++ b/IM/tosca/Tosca.py @@ -0,0 +1,880 @@ +import os +import logging +import yaml +import copy + +from IM.tosca.toscaparser.tosca_template import ToscaTemplate +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.functions import Function, is_function, get_function, GetAttribute +from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from pylint.pyreverse.diagrams import Relationship +from compiler.ast import Node + +class Tosca: + """ + Class to translate a TOSCA document to an RADL object. + + TODO: What about CSAR files? + + """ + + ARTIFACTS_PATH = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/artifacts" + CUSTOM_TYPES_FILE = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/custon_types.yaml" + + logger = logging.getLogger('InfrastructureManager') + + def __init__(self, path): + self.path = path + self.tosca = None + self.tosca = ToscaTemplate(path) + + @staticmethod + def is_tosca(yaml_string): + """ + Check if a string seems to be a tosca document + Check if it is a correct YAML document and has the item 'tosca_definitions_version' + """ + try: + yamlo = yaml.load(yaml_string) + if isinstance(yamlo, dict) and 'tosca_definitions_version' in yamlo.keys(): + return True + else: + return False + except: + return False + + def to_radl(self): + """ + Converts the current ToscaTemplate object in a RADL object + """ + + relationships = [] + for node in self.tosca.nodetemplates: + # Store relationships to check later + for relationship, target in node.relationships.iteritems(): + source = node + relationships.append((source, target, relationship)) + + radl = RADL() + interfaces = {} + cont_intems = [] + + for node in self.tosca.nodetemplates: + root_type = Tosca._get_root_parent_type(node).type + + if root_type == "tosca.nodes.BlockStorage": + # The BlockStorage disks are processed later + pass + elif root_type == "tosca.nodes.network.Port": + pass + elif root_type == "tosca.nodes.network.Network": + # TODO: check IM to support more network properties + # At this moment we only support the network_type with values, private and public + net = Tosca._gen_network(node) + radl.networks.append(net) + else: + if root_type == "tosca.nodes.Compute": + # Add the system RADL element + sys = Tosca._gen_system(node, self.tosca.nodetemplates) + radl.systems.append(sys) + # Add the deploy element for this system + dep = deploy(sys.name, 1) + radl.deploys.append(dep) + compute = node + else: + # Select the host to host this element + compute = Tosca._find_host_compute(node, self.tosca.nodetemplates) + + interfaces = Tosca._get_interfaces(node) + interfaces.update(Tosca._get_relationships_interfaces(relationships, node)) + + conf = self._gen_configure_from_interfaces(radl, node, interfaces, compute) + if conf: + level = Tosca._get_dependency_level(node) + radl.configures.append(conf) + cont_intems.append(contextualize_item(compute.name, conf.name, level)) + + if cont_intems: + radl.contextualize = contextualize(cont_intems) + + return self._complete_radl_networks(radl) + + @staticmethod + def _get_relationship_template(rel, src, trgt): + rel_tpls = src.get_relationship_template() + rel_tpls.extend(trgt.get_relationship_template()) + for rel_tpl in rel_tpls: + if rel.type == rel_tpl.type: + return rel_tpl + + @staticmethod + def _get_relationships_interfaces(relationships, node): + res = {} + for src, trgt, rel in relationships: + rel_tpl = Tosca._get_relationship_template(rel, src, trgt) + if src.name == node.name: + for name in ['pre_configure_source', 'post_configure_source', 'add_source']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface + elif trgt.name == node.name: + for name in ['pre_configure_target', 'post_configure_target', 'add_target','target_changed','remove_target']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface + return res + + def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): + if not interfaces: + return None + + variables = "" + tasks = "" + recipe_list = [] + remote_artifacts_path = "/tmp" + # Take the interfaces in correct order + for name in ['create', 'pre_configure_source','pre_configure_target','configure', 'post_configure_source','post_configure_target', 'start', 'add_target','add_source','target_changed','remove_target']: + interface = interfaces.get(name, None) + if interface: + artifacts = [] + # Get the inputs + env = {} + if interface.inputs: + for param_name, param_value in interface.inputs.iteritems(): + val = None + + if self._is_artifact(param_value): + artifact_uri = self._get_artifact_uri(param_value, node) + val = remote_artifacts_path + "/" + os.path.basename(artifact_uri) + artifacts.append(artifact_uri) + else: + val = self._final_function_result(param_value, node) + + if val: + env[param_name] = val + else: + raise Exception("input value for %s in interface %s of node %s not valid" % (param_name, name, node.name)) + + name = node.name + "_" + interface.name + script_path = os.path.join(Tosca.ARTIFACTS_PATH, interface.implementation) + + # if there are artifacts to download + if artifacts: + for artifact in artifacts: + tasks += " - name: Download artifact " + artifact + "\n" + tasks += " get_url: dest=" + remote_artifacts_path + "/" + os.path.basename(artifact) + " url='" + artifact + "'\n" + + if interface.implementation.endswith(".yaml") or interface.implementation.endswith(".yml"): + if os.path.isfile(script_path): + f = open(script_path) + script_content = f.read() + f.close() + + if env: + for var_name, var_value in env.iteritems(): + variables += " %s: %s " % (var_name, var_value) + "\n" + variables += "\n" + + recipe_list.append(script_content) + else: + raise Exception(script_path + " is not located in the artifacts folder.") + else: + if os.path.isfile(script_path): + f = open(script_path) + script_content = f.read().replace("\n","\\n") + f.close() + + recipe = "- tasks:\n" + recipe += " - name: Copy contents of script of interface " + name + "\n" + recipe += " copy: dest=/tmp/" + os.path.basename(script_path) + " content='" + script_content + "' mode=0755\n" + + recipe += " - name: " + name + "\n" + recipe += " shell: /tmp/" + os.path.basename(script_path) + "\n" + if env: + recipe += " environment:\n" + for var_name, var_value in env.iteritems(): + recipe += " %s: %s\n" % (var_name, var_value) + + recipe_list.append(recipe) + else: + raise Exception(script_path + " is not located in the artifacts folder.") + + if tasks or recipe_list: + name = node.name + "_conf" + if variables: + recipes = "---\n- vars:\n" + variables + "\n" + recipes += " " + else: + recipes = "- " + + if tasks: + recipes += "tasks:\n" + tasks + "\n" + + # Merge the main recipe with the other yaml files + for recipe in recipe_list: + recipes = Tosca._merge_yaml(recipes, recipe) + + return configure(name, recipes) + else: + return None + + @staticmethod + def _is_artifact(function): + """Returns True if the provided function is a Tosca get_artifact function. + + Examples: + + * "{ get_artifact: { SELF, uri } }" + + :param function: Function as string. + :return: True if function is a Tosca get_artifact function, otherwise False. + """ + if isinstance(function, dict) and len(function) == 1: + func_name = list(function.keys())[0] + return func_name == "get_artifact" + return False + + @staticmethod + def _get_artifact_uri(function, node): + if isinstance(function, dict) and len(function) == 1: + name = function["get_artifact"][1] + artifacts = node.entity_tpl.get("artifacts") + if isinstance(artifacts, dict): + for artifact_name, value in artifacts.iteritems(): + if artifact_name == name: + return value['implementation'] + + return None + + @staticmethod + def _complete_radl_networks(radl): + if not radl.networks: + radl.networks.append(network.createNetwork("public", True)) + + public_net = None + for net in radl.networks: + if net.isPublic(): + public_net = net + break + + if not public_net: + for net in radl.networks: + public_net = net + + for sys in radl.systems: + if not sys.hasFeature("net_interface.0.connection"): + sys.setValue("net_interface.0.connection", public_net.id) + + return radl + + @staticmethod + def _is_intrinsic(function): + """Returns True if the provided function is a Tosca get_artifact function. + + Examples: + + * "{ concat: ['str1', 'str2'] }" + * "{ token: [ , , ] }" + + :param function: Function as string. + :return: True if function is a Tosca get_artifact function, otherwise False. + """ + if isinstance(function, dict) and len(function) == 1: + func_name = list(function.keys())[0] + return func_name in ["concat", "token"] + return False + + def _get_intrinsic_value(self, func, node): + if isinstance(func, dict) and len(func) == 1: + func_name = list(func.keys())[0] + if func_name == "concat": + items = func["concat"] + res = "" + for item in items: + if is_function(item): + res += str(self._final_function_result(item, node)) + else: + res += str(item) + return res + elif func_name == "token": + if len(items) == 3: + string_with_tokens = items[0] + string_of_token_chars = items[1] + substring_index = int(items[2]) + + parts = string_with_tokens.split(string_of_token_chars) + if len(parts) >= substring_index: + return parts[substring_index] + else: + Tosca.logger.error("Incorrect substring_index in function token.") + return None + else: + Tosca.logger.warn("Intrinsic function token must receive 3 parameters.") + return None + else: + Tosca.logger.warn("Intrinsic function %s not supported." % func_name) + return None + + def _get_attribute_result(self, func, node): + """Get an attribute value of an entity defined in the service template + + Node template attributes values are set in runtime and therefore its the + responsibility of the Tosca engine to implement the evaluation of + get_attribute functions. + + Arguments: + + * Node template name | HOST. + * Attribute name. + + If the HOST keyword is passed as the node template name argument the + function will search each node template along the HostedOn relationship + chain until a node which contains the attribute is found. + + Examples: + + * { get_attribute: [ server, private_address ] } + * { get_attribute: [ HOST, private_address ] } + * { get_attribute: [ SELF, private_address ] } + """ + node_name = func.args[0] + attribute_name = func.args[1] + + if node_name == "HOST": + node = self._find_host_compute(node, self.tosca.nodetemplates) + else: + for n in self.tosca.nodetemplates: + if n.name == node_name: + node = n + break + + if attribute_name == "tosca_id": + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_VMID }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_VMID'] }}" % node.name + elif attribute_name == "tosca_name": + return node.name + elif attribute_name == "private_address": + # TODO: we suppose that iface 1 is the private one + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_NET_1_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_1_IP'] }}" % node.name + elif attribute_name == "public_address": + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_ANSIBLE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name + elif attribute_name == "ip_address": + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.network.Port": + order = node.get_property_value('order') + return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) + elif root_type == "tosca.capabilities.Endpoint": + # TODO: check this + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_ANSIBLE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name + else: + Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + return None + else: + Tosca.logger.warn("Attribute %s not supported." % attribute_name) + return None + + def _final_function_result(self, func, node): + """ + Take a translator.toscalib.functions.Function and return the final result + (in some cases the result of a function is another function) + """ + if isinstance(func, dict): + if is_function(func): + func = get_function(self.tosca, node, func) + + if isinstance(func, Function): + if isinstance(func, GetAttribute): + func = self._get_attribute_result(func, node) + while isinstance(func, Function): + func = func.result() + + if isinstance(func, dict): + if self._is_intrinsic(func): + func = self._get_intrinsic_value(func, node) + + if func is None: + # TODO: resolve function values related with run-time values as IM or ansible variables + pass + return func + + @staticmethod + def _find_host_compute(node, nodetemplates): + """ + Select the node to host each node, using the node requirements + In most of the cases the are directly specified, otherwise "node_filter" is used + """ + + # check for a HosteOn relation + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.Compute": + return node + + if node.requirements: + for r, n in node.relationships.iteritems(): + if Tosca._is_derived_from(r, r.HOSTEDON) or Tosca._is_derived_from(r, r.BINDSTO): + root_type = Tosca._get_root_parent_type(n).type + if root_type == "tosca.nodes.Compute": + return n + else: + return Tosca._find_host_compute(n, nodetemplates) + + # There are no direct HostedOn node + # check node_filter requirements + if node.requirements: + for requires in node.requirements: + if 'host' in requires: + value = requires.get('host') + if isinstance(value, dict): + if 'node_filter' in value: + node_filter = value.get('node_filter') + return Tosca._get_compute_from_node_filter(node_filter, nodetemplates) + + return None + + @staticmethod + def _node_fulfill_filter(node, node_filter): + """ + Check if a node fulfills the features of a node filter + """ + + # Get node properties + node_props = {} + for cap_type in ['os', 'host']: + if node.get_capability(cap_type): + for prop in node.get_capability(cap_type).get_properties_objects(): + if prop.value: + unit = None + value = prop.value + if prop.name in ['disk_size', 'mem_size']: + value, unit = Tosca._get_size_and_unit(prop.value) + node_props[prop.name] = (value, unit) + + filter_props = {} + # Get node_filter properties + for elem in node_filter: + if isinstance(elem, dict): + for cap_type in ['os', 'host']: + if cap_type in elem: + for p in elem.get(cap_type).get('properties'): + p_name = p.keys()[0] + p_value = p.values()[0] + if isinstance(p_value, dict): + filter_props[p_name] = (p_value.keys()[0], p_value.values()[0]) + else: + filter_props[p_name] = ("equal", p_value) + + operator_map = { + 'equal':'==', + 'greater_than':'>', + 'greater_or_equal':'>=', + 'less_than': '<', + 'less_or_equal': '<=' + } + + # Compare the properties + for name, value in filter_props.iteritems(): + operator, filter_value = value + if name in ['disk_size', 'mem_size']: + filter_value, _ = Tosca._get_size_and_unit(filter_value) + + if name in node_props: + node_value, _ = node_props[name] + + if isinstance(node_value, str) or isinstance(node_value, unicode): + str_node_value = "'" + node_value + "'" + else: + str_node_value = str(node_value) + + conv_operator = operator_map.get(operator, None) + if conv_operator: + if isinstance(filter_value, str) or isinstance(filter_value, unicode): + str_filter_value = "'" + filter_value + "'" + else: + str_filter_value = str(filter_value) + + comparation = str_node_value + conv_operator + str_filter_value + else: + if operator == "in_range": + minv = filter_value[0] + maxv = filter_value[1] + comparation = str_node_value + ">=" +str(minv) + " and " + str_node_value + "<=" + str(maxv) + elif operator == "valid_values": + comparation = str_node_value + " in " + str(filter_value) + else: + Tosca.logger.warn("Logical operator %s not supported." % operator) + + if not eval(comparation): + return False + else: + # if this property is not specified in the node, return False + # TODO: we must think about default values + return False + + return True + + @staticmethod + def _get_compute_from_node_filter(node_filter, nodetemplates): + """ + Select the first node that fulfills the specified "node_filter" + """ + #{'capabilities': [{'host': {'properties': [{'num_cpus': {'in_range': [1, 4]}}, {'mem_size': {'greater_or_equal': '2 GB'}}]}}, {'os': {'properties': [{'architecture': {'equal': 'x86_64'}}, {'type': 'linux'}, {'distribution': 'ubuntu'}]}}]} + + for node in nodetemplates: + root_type = Tosca._get_root_parent_type(node).type + + if root_type == "tosca.nodes.Compute": + if Tosca._node_fulfill_filter(node, node_filter.get('capabilities')): + return node + + return None + + @staticmethod + def _get_dependency_level(node): + """ + Check the relations to get the contextualization level + """ + if node.related_nodes: + maxl = 0 + for node_depend in node.related_nodes: + level = Tosca._get_dependency_level(node_depend) + if level > maxl: + maxl = level + return maxl + 1 + else: + return 1 + + @staticmethod + def _unit_to_bytes(unit): + """Return the value of an unit.""" + if not unit: + return 1 + unit = unit.upper() + + if unit.startswith("KI"): + return 1024 + elif unit.startswith("K"): + return 1000 + elif unit.startswith("MI"): + return 1048576 + elif unit.startswith("M"): + return 1000000 + elif unit.startswith("GI"): + return 1073741824 + elif unit.startswith("G"): + return 1000000000 + elif unit.startswith("TI"): + return 1099511627776 + elif unit.startswith("T"): + return 1000000000000 + else: + return 1 + + @staticmethod + def _get_size_and_unit(str_value): + """ + Normalize the size and units to bytes + """ + parts = str_value.split(" ") + value = float(parts[0]) + unit = 'M' + if len(parts) > 1: + unit = parts[1] + + value = int(value * Tosca._unit_to_bytes(unit)) + + return value, 'B' + + @staticmethod + def _gen_network(node): + """ + Take a node of type "Network" and get the RADL.network to represent it + """ + res = network(node.name) + + nework_type = node.get_property_value("network_type") + network_name = node.get_property_value("network_name") + + # TODO: get more properties -> must be implemented in the RADL + if nework_type == "public": + res.setValue("outbound", "yes") + + if network_name: + res.setValue("provider_id", network_name) + + return res + + + @staticmethod + def _gen_system(node, nodetemplates): + """ + Take a node of type "Compute" and get the RADL.system to represent it + """ + res = system(node.name) + + property_map = { + 'architecture':'cpu.arch', + 'type':'disk.0.os.name', + 'distribution':'disk.0.os.flavour', + 'version': 'disk.0.os.version', + 'num_cpus': 'cpu.count', + 'disk_size': 'disk.0.size', + 'mem_size': 'memory.size', + 'cpu_frequency': 'cpu.performance' + } + + for cap_type in ['os', 'host']: + if node.get_capability(cap_type): + for prop in node.get_capability(cap_type).get_properties_objects(): + name = property_map.get(prop.name, None) + if name and prop.value: + unit = None + value = prop.value + if prop.name in ['disk_size', 'mem_size']: + value, unit = Tosca._get_size_and_unit(prop.value) + + if prop.name == "version": + value= str(value) + + if isinstance(value, float) or isinstance(value, int): + operator = ">=" + else: + operator = "=" + + feature = Feature(name, operator, value, unit) + res.addFeature(feature) + + # Find associated BlockStorages + disks = Tosca._get_attached_disks(node, nodetemplates) + + for size, unit, location, device, num in disks: + res.setValue('disk.%d.size' % num, size, unit) + if device: + res.setValue('disk.%d.device' % num, device) + if location: + res.setValue('disk.%d.mount_path' % num, location) + res.setValue('disk.%d.fstype' % num, "ext4") + + # Find associated Networks + nets = Tosca._get_bind_networks(node, nodetemplates) + for net_name, ip, dns_name, num in nets: + res.setValue('net_interface.%d.connection' % num, net_name) + if dns_name: + res.setValue('net_interface.%d.dns_name' % num, dns_name) + if ip: + res.setValue('net_interface.%d.ip' % num, ip) + + return res + + @staticmethod + def _get_bind_networks(node, nodetemplates): + nets = [] + count = 0 + for requires in node.requirements: + for value in requires.values(): + name = None + ip = None + dns_name = None + if isinstance(value, dict): + if 'relationship' in value: + rel = value.get('relationship') + + rel_type = None + if isinstance(rel, dict) and 'type' in rel: + rel_type = rel.get('type') + else: + rel_type = rel + + if rel_type and rel_type.endswith("BindsTo"): + if isinstance(rel, dict) and 'properties' in rel: + prop = rel.get('properties') + if isinstance(prop, dict): + ip = prop.get('ip', None) + dns_name = prop.get('dns_name', None) + + name = value.values()[0] + nets.append((name, ip, dns_name, count)) + count += 1 + else: + Tosca.logger.error("ERROR: expected dict in requires values.") + + for port in nodetemplates: + root_type = Tosca._get_root_parent_type(port).type + if root_type == "tosca.nodes.network.Port": + binding = None + link = None + for requires in port.requirements: + binding = requires.get('binding', binding) + link = requires.get('link', link) + + if binding == node.name: + ip = port.get_property_value('ip_address') + order = port.get_property_value('order') + dns_name = None + nets.append((link, ip, dns_name, order)) + + return nets + + + @staticmethod + def _get_attached_disks(node, nodetemplates): + """ + Get the disks attached to a node + """ + disks = [] + count = 1 + for requires in node.requirements: + for value in requires.values(): + size = None + location = None + device = None + if isinstance(value, dict): + if 'relationship' in value: + rel = value.get('relationship') + + rel_type = None + if isinstance(rel, dict) and 'type' in rel: + rel_type = rel.get('type') + else: + rel_type = rel + + if rel_type and rel_type.endswith("AttachesTo"): + if isinstance(rel, dict) and 'properties' in rel: + prop = rel.get('properties') + if isinstance(prop, dict): + location = prop.get('location', None) + device = prop.get('device', None) + + # seet a default device + if not device: + device = "hdb" + + for node_name in value.values(): + for n in nodetemplates: + if n.name == node_name: + size, unit = Tosca._get_size_and_unit(n.get_property_value('size')) + break + + disks.append((size, unit, location, device, count)) + count += 1 + else: + Tosca.logger.error("ERROR: expected dict in requires values.") + + return disks + + @staticmethod + def _is_derived_from(rel, parent_type): + """ + Check if a node is a descendant from a specified parent type + """ + while True: + if rel.type == parent_type: + return True + else: + if rel.parent_type: + rel = rel.parent_type + else: + return False + @staticmethod + def _get_root_parent_type(node): + """ + Get the root parent type of a node (just before the tosca.nodes.Root) + """ + node_type = node.type_definition + + while True: + if node_type.parent_type != None: + if node_type.parent_type.type.endswith(".Root"): + return node_type + else: + node_type = node_type.parent_type + else: + return node_type + + @staticmethod + def _get_interfaces(node): + """ + Get a dict of InterfacesDef of the specified node + """ + interfaces = {} + for interface in node.interfaces: + interfaces[interface.name] = interface + + node_type = node.type_definition + + while True: + if node_type.interfaces and 'Standard' in node_type.interfaces: + for name, elems in node_type.interfaces['Standard'].iteritems(): + if name in ['create', 'configure', 'start', 'stop', 'delete']: + if name not in interfaces: + interfaces[name] = InterfacesDef(node_type, 'Standard', name=name, value=elems) + + if node_type.parent_type != None: + node_type = node_type.parent_type + else: + return interfaces + + @staticmethod + def _merge_yaml(yaml1, yaml2): + """ + Merge two ansible yaml docs + + Arguments: + - yaml1(str): string with the first YAML + - yaml1(str): string with the second YAML + Returns: The merged YAML. In case of errors, it concatenates both strings + """ + yamlo1o = {} + try: + yamlo1o = yaml.load(yaml1)[0] + if not isinstance(yamlo1o, dict): + yamlo1o = {} + except Exception: + Tosca.logger.exception("Error parsing YAML: " + yaml1 + "\n Ignore it") + + try: + yamlo2s = yaml.load(yaml2) + if not isinstance(yamlo2s, list) or any([ not isinstance(d, dict) for d in yamlo2s ]): + yamlo2s = {} + except Exception: + Tosca.logger.exception("Error parsing YAML: " + yaml2 + "\n Ignore it") + yamlo2s = {} + + if not yamlo2s and not yamlo1o: + return "" + + result = [] + for yamlo2 in yamlo2s: + yamlo1 = copy.deepcopy(yamlo1o) + all_keys = [] + all_keys.extend(yamlo1.keys()) + all_keys.extend(yamlo2.keys()) + all_keys = set(all_keys) + + for key in all_keys: + if key in yamlo1 and yamlo1[key]: + if key in yamlo2 and yamlo2[key]: + if isinstance(yamlo1[key], dict): + yamlo1[key].update(yamlo2[key]) + elif isinstance(yamlo1[key], list): + yamlo1[key].extend(yamlo2[key]) + else: + # Both use have the same key with merge in a lists + v1 = yamlo1[key] + v2 = yamlo2[key] + yamlo1[key] = [v1, v2] + elif key in yamlo2 and yamlo2[key]: + yamlo1[key] = yamlo2[key] + result.append(yamlo1) + + return yaml.dump(result, default_flow_style=False, explicit_start=True, width=256) \ No newline at end of file diff --git a/IM/tosca/__init__.py b/IM/tosca/__init__.py new file mode 100644 index 000000000..c059187df --- /dev/null +++ b/IM/tosca/__init__.py @@ -0,0 +1,16 @@ +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/IM/tosca/artifacts/apache/apache_install.yml b/IM/tosca/artifacts/apache/apache_install.yml new file mode 100755 index 000000000..db572f85b --- /dev/null +++ b/IM/tosca/artifacts/apache/apache_install.yml @@ -0,0 +1,26 @@ + # Disable IPv6 + - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" + with_items: + - 'net.ipv6.conf.all.disable_ipv6' + - 'net.ipv6.conf.default.disable_ipv6' + - 'net.ipv6.conf.lo.disable_ipv6' + ignore_errors: yes + + - command: sysctl -p + ignore_errors: yes + + - name: Apache | Make sure the Apache packages are installed + apt: pkg=apache2 update_cache=yes + when: ansible_os_family == "Debian" + + - name: Start Apache service + service: name=apache2 state=started + when: ansible_os_family == "Debian" + + - name: Apache | Make sure the Apache packages are installed + apt: yum=httpd + when: ansible_os_family == "RedHat" + + - name: Start Apache service + service: name=httpd state=started + when: ansible_os_family == "RedHat" diff --git a/IM/tosca/artifacts/galaxy/galaxy_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_configure.yml new file mode 100644 index 000000000..164bbaf92 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_configure.yml @@ -0,0 +1,25 @@ +--- +- vars: + GALAXY_USER_ID: 4001 + GALAXY_USER_PASSWORD: $6$Ehg4GHQT5y$6ZCTLffp.epiNEhS1M3ZB.P6Kii1wELySe/DCwUInGt8r7zgdAHfHw66DuPwpS6pfOiZ9PS/KaTiBKjoCn23t0 + + tasks: + # General configuration + - copy: src={{galaxy_install_path}}/config/galaxy.ini.sample dest={{galaxy_install_path}}/config/galaxy.ini force=no + - ini_file: dest={{galaxy_install_path}}/config/galaxy.ini section={{ item.section }} option={{ item.option }} value="{{ item.value }}" + with_items: + - { section: 'server:main', option: 'host', value: '0.0.0.0' } + - { section: 'app:main', option: 'admin_users', value: "{{galaxy_admin}}" } + - { section: 'app:main', option: 'master_api_key', value: "{{galaxy_admin_api_key}}" } + - { section: 'app:main', option: 'tool_dependency_dir', value: "{{galaxy_install_path}}/tool_dependency_dir" } + + # Create galaxy user to launch the daemon + - user: name={{galaxy_user}} password={{GALAXY_USER_PASSWORD}} generate_ssh_key=yes shell=/bin/bash uid={{GALAXY_USER_ID}} + - local_action: command cp /home/{{galaxy_user}}/.ssh/id_rsa.pub /tmp/{{galaxy_user}}_id_rsa.pub creates=/tmp/{{galaxy_user}}_id_rsa.pub + - name: Add the authorized_key to the user {{galaxy_user}} + authorized_key: user={{galaxy_user}} key="{{ lookup('file', '/tmp/' + galaxy_user + '_id_rsa.pub') }}" + + - file: path=/home/{{galaxy_user}} state=directory owner={{galaxy_user}} group={{galaxy_user}} + - file: path={{galaxy_install_path}} state=directory recurse=yes owner={{galaxy_user}} + + - copy: dest="{{galaxy_install_path}}/config/local_env.sh" content="PYTHON_EGG_CACHE={{galaxy_install_path}}/egg\nGALAXY_RUN_ALL=1\nexport PYTHON_EGG_CACHE GALAXY_RUN_ALL" \ No newline at end of file diff --git a/IM/tosca/artifacts/galaxy/galaxy_install.yml b/IM/tosca/artifacts/galaxy/galaxy_install.yml new file mode 100644 index 000000000..cd6689fb0 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_install.yml @@ -0,0 +1,9 @@ +--- +- tasks: + # Install requisites + - apt: name=git update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + - yum: name=git + when: ansible_os_family == "RedHat" + # Download Galaxy + - git: repo=https://github.com/galaxyproject/galaxy/ dest={{galaxy_install_path}} version=master diff --git a/IM/tosca/artifacts/galaxy/galaxy_start.yml b/IM/tosca/artifacts/galaxy/galaxy_start.yml new file mode 100644 index 000000000..0801d10d8 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_start.yml @@ -0,0 +1,6 @@ +--- +- tasks: + # Launch the server + - shell: bash run.sh --daemon chdir={{galaxy_install_path}}/ creates={{galaxy_install_path}}/main.pid + sudo: true + sudo_user: "{{galaxy_user}}" diff --git a/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml new file mode 100644 index 000000000..047fca373 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml @@ -0,0 +1,34 @@ +--- +- vars: + tool_content: | + tools: + - name: '{{galaxy_tool_name}}' + owner: '{{galaxy_tool_owner}}' + tool_panel_section_id: '{{galaxy_tool_panel_section_id}}' + tasks: + # Install galaxy tools + - name: Uninstall old version of python-requests in Ubuntu + shell: dpkg --force-all -r python-requests + when: ansible_os_family == "Debian" + ignore_errors: yes + + - name: Install script dependencies + pip: name={{item}} state=latest + with_items: + - bioblend + - requests + + - name: Place the tool management script + get_url: url=https://raw.githubusercontent.com/galaxyproject/ansible-galaxy-tools/master/files/install_tool_shed_tools.py dest={{galaxy_install_path}}/install_tool_shed_tools.py + + - name: Copy tool list files + copy: + content: "{{tool_content}}" + dest: "{{galaxy_install_path}}/my_tool_list.yml" + + - name: Wait for Galaxy to start + wait_for: port=8080 delay=5 state=started timeout=150 + + - name: Install Tool Shed tools + shell: chdir={{galaxy_install_path}} python install_tool_shed_tools.py -t my_tool_list.yml -a {{galaxy_admin_api_key}} -g 127.0.0.1:8080 + #creates={{galaxy_install_path}}//tool_dependency_dir/bowtie2 diff --git a/IM/tosca/artifacts/mysql/mysql_configure.yml b/IM/tosca/artifacts/mysql/mysql_configure.yml new file mode 100755 index 000000000..446ac80c9 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_configure.yml @@ -0,0 +1,4 @@ +- tasks: + - name: update mysql root password for all root accounts + mysql_user: name=root password={{root_password}} + ignore_errors: yes diff --git a/IM/tosca/artifacts/mysql/mysql_db_configure.yml b/IM/tosca/artifacts/mysql/mysql_db_configure.yml new file mode 100644 index 000000000..1217d59b5 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_db_configure.yml @@ -0,0 +1,5 @@ + - name: Create DB {{name}} + mysql_db: name={{name}} state=present login_user=root login_password={{root_password}} + + - name: Create user {{user}} for the DB {{name}} + mysql_user: name={{user}} password={{password}} login_user=root login_password={{root_password}} priv={{name}}.*:ALL,GRANT state=present diff --git a/IM/tosca/artifacts/mysql/mysql_install.yml b/IM/tosca/artifacts/mysql/mysql_install.yml new file mode 100755 index 000000000..d7e5897c8 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_install.yml @@ -0,0 +1,27 @@ +- tasks: + # Disable IPv6 + - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" + with_items: + - 'net.ipv6.conf.all.disable_ipv6' + - 'net.ipv6.conf.default.disable_ipv6' + - 'net.ipv6.conf.lo.disable_ipv6' + ignore_errors: yes + + - command: sysctl -p + ignore_errors: yes + + - name: MySQL | Make sure the MySQL packages are installed + apt: pkg=mysql-server,python-mysqldb update_cache=yes + when: ansible_os_family == "Debian" + + - name: Start MySQL service + service: name=mysql state=started + when: ansible_os_family == "Debian" + + - name: MySQL | Make sure the MySQL packages are installed + apt: yum=mysql-server,MySQL-python + when: ansible_os_family == "RedHat" + + - name: Start MySQL service + service: name=mysqld state=started + when: ansible_os_family == "RedHat" diff --git a/IM/tosca/toscaparser/__init__.py b/IM/tosca/toscaparser/__init__.py new file mode 100644 index 000000000..f418d00a9 --- /dev/null +++ b/IM/tosca/toscaparser/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# 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. + +import pbr.version + +__version__ = "1.0.0" +#__version__ = pbr.version.VersionInfo( +# 'tosca-parser').version_string() diff --git a/IM/tosca/toscaparser/capabilities.py b/IM/tosca/toscaparser/capabilities.py new file mode 100644 index 000000000..5af77f862 --- /dev/null +++ b/IM/tosca/toscaparser/capabilities.py @@ -0,0 +1,57 @@ +# 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. + +from IM.tosca.toscaparser.properties import Property + + +class Capability(object): + '''TOSCA built-in capabilities type.''' + + def __init__(self, name, properties, definition): + self.name = name + self._properties = properties + self.definition = definition + + def get_properties_objects(self): + '''Return a list of property objects.''' + properties = [] + # Miguel: cambios aqui + props_def = self.definition.get_properties_def() + if props_def: + props_name = props_def.keys() + + for name in props_name: + value = None + if name in self._properties: + value = self._properties[name] + properties.append(Property(name, value, props_def[name].schema)) + +# props = self._properties +# +# if props: +# for name, value in props.items(): +# props_def = self.definition.get_properties_def() +# if props_def and name in props_def: +# properties.append(Property(name, value, +# props_def[name].schema)) + return properties + + def get_properties(self): + '''Return a dictionary of property name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_objects()} + + def get_property_value(self, name): + '''Return the value of a given property name.''' + props = self.get_properties() + if props and name in props: + return props[name].value diff --git a/IM/tosca/toscaparser/common/__init__.py b/IM/tosca/toscaparser/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/common/exception.py b/IM/tosca/toscaparser/common/exception.py new file mode 100644 index 000000000..fb2eea9e6 --- /dev/null +++ b/IM/tosca/toscaparser/common/exception.py @@ -0,0 +1,100 @@ +# 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. + +''' +TOSCA exception classes +''' +import logging +import sys + +from IM.tosca.toscaparser.utils.gettextutils import _ + + +log = logging.getLogger(__name__) + + +class TOSCAException(Exception): + '''Base exception class for TOSCA + + To correctly use this class, inherit from it and define + a 'msg_fmt' property. + + ''' + + _FATAL_EXCEPTION_FORMAT_ERRORS = False + + message = _('An unknown exception occurred.') + + def __init__(self, **kwargs): + try: + self.message = self.msg_fmt % kwargs + except KeyError: + exc_info = sys.exc_info() + log.exception(_('Exception in string format operation: %s') + % exc_info[1]) + + if TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS: + raise exc_info[0] + + def __str__(self): + return self.message + + @staticmethod + def set_fatal_format_exception(flag): + if isinstance(flag, bool): + TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS = flag + + +class MissingRequiredFieldError(TOSCAException): + msg_fmt = _('%(what)s is missing required field: "%(required)s".') + + +class UnknownFieldError(TOSCAException): + msg_fmt = _('%(what)s contain(s) unknown field: "%(field)s", ' + 'refer to the definition to verify valid values.') + + +class TypeMismatchError(TOSCAException): + msg_fmt = _('%(what)s must be of type: "%(type)s".') + + +class InvalidNodeTypeError(TOSCAException): + msg_fmt = _('Node type "%(what)s" is not a valid type.') + + +class InvalidTypeError(TOSCAException): + msg_fmt = _('Type "%(what)s" is not a valid type.') + + +class InvalidSchemaError(TOSCAException): + msg_fmt = _("%(message)s") + + +class ValidationError(TOSCAException): + msg_fmt = _("%(message)s") + + +class UnknownInputError(TOSCAException): + msg_fmt = _('Unknown input: %(input_name)s') + + +class InvalidPropertyValueError(TOSCAException): + msg_fmt = _('Value of property "%(what)s" is invalid.') + + +class InvalidTemplateVersion(TOSCAException): + msg_fmt = _('The template version "%(what)s" is invalid. ' + 'The valid versions are: "%(valid_versions)s"') + + +class InvalidTOSCAVersionPropertyException(TOSCAException): + msg_fmt = _('Value of TOSCA version property "%(what)s" is invalid.') diff --git a/IM/tosca/toscaparser/dataentity.py b/IM/tosca/toscaparser/dataentity.py new file mode 100644 index 000000000..eb67e63a4 --- /dev/null +++ b/IM/tosca/toscaparser/dataentity.py @@ -0,0 +1,159 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import TypeMismatchError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.elements.constraints import Schema +from IM.tosca.toscaparser.elements.datatype import DataType +from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Frequency +from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Size +from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Time + +from IM.tosca.toscaparser.utils.gettextutils import _ +from IM.tosca.toscaparser.utils import validateutils + + +class DataEntity(object): + '''A complex data value entity.''' + + def __init__(self, datatypename, value_dict, custom_def=None): + self.custom_def = custom_def + self.datatype = DataType(datatypename, custom_def) + self.schema = self.datatype.get_all_properties() + self.value = value_dict + + def validate(self): + '''Validate the value by the definition of the datatype.''' + + # A datatype can not have both 'type' and 'properties' definitions. + # If the datatype has 'type' definition + if self.datatype.value_type: + self.value = DataEntity.validate_datatype(self.datatype.value_type, + self.value, + None, + self.custom_def) + schema = Schema(None, self.datatype.defs) + for constraint in schema.constraints: + constraint.validate(self.value) + # If the datatype has 'properties' definition + else: + if not isinstance(self.value, dict): + raise TypeMismatchError(what=self.value, + type=self.datatype.type) + allowed_props = [] + required_props = [] + default_props = {} + if self.schema: + allowed_props = self.schema.keys() + for name, prop_def in self.schema.items(): + if prop_def.required: + required_props.append(name) + if prop_def.default: + default_props[name] = prop_def.default + + # check allowed field + for value_key in list(self.value.keys()): + if value_key not in allowed_props: + raise UnknownFieldError(what=_('Data value of type %s') + % self.datatype.type, + field=value_key) + + # check default field + for def_key, def_value in list(default_props.items()): + if def_key not in list(self.value.keys()): + self.value[def_key] = def_value + + # check missing field + missingprop = [] + for req_key in required_props: + if req_key not in list(self.value.keys()): + missingprop.append(req_key) + if missingprop: + raise MissingRequiredFieldError(what=_('Data value of type %s') + % self.datatype.type, + required=missingprop) + + # check every field + for name, value in list(self.value.items()): + prop_schema = Schema(name, self._find_schema(name)) + # check if field value meets type defined + DataEntity.validate_datatype(prop_schema.type, value, + prop_schema.entry_schema, + self.custom_def) + # check if field value meets constraints defined + if prop_schema.constraints: + for constraint in prop_schema.constraints: + constraint.validate(value) + + return self.value + + def _find_schema(self, name): + if self.schema and name in self.schema.keys(): + return self.schema[name].schema + + @staticmethod + def validate_datatype(type, value, entry_schema=None, custom_def=None): + '''Validate value with given type. + + If type is list or map, validate its entry by entry_schema(if defined) + If type is a user-defined complex datatype, custom_def is required. + ''' + if type == Schema.STRING: + return validateutils.validate_string(value) + elif type == Schema.INTEGER: + return validateutils.validate_integer(value) + elif type == Schema.FLOAT: + return validateutils.validate_float(value) + elif type == Schema.NUMBER: + return validateutils.validate_number(value) + elif type == Schema.BOOLEAN: + return validateutils.validate_boolean(value) + elif type == Schema.TIMESTAMP: + validateutils.validate_timestamp(value) + return value + elif type == Schema.LIST: + validateutils.validate_list(value) + if entry_schema: + DataEntity.validate_entry(value, entry_schema, custom_def) + return value + elif type == Schema.SCALAR_UNIT_SIZE: + return ScalarUnit_Size(value).validate_scalar_unit() + elif type == Schema.SCALAR_UNIT_FREQUENCY: + return ScalarUnit_Frequency(value).validate_scalar_unit() + elif type == Schema.SCALAR_UNIT_TIME: + return ScalarUnit_Time(value).validate_scalar_unit() + elif type == Schema.VERSION: + return validateutils.TOSCAVersionProperty(value).get_version() + elif type == Schema.MAP: + validateutils.validate_map(value) + if entry_schema: + DataEntity.validate_entry(value, entry_schema, custom_def) + return value + else: + data = DataEntity(type, value, custom_def) + return data.validate() + + @staticmethod + def validate_entry(value, entry_schema, custom_def=None): + '''Validate entries for map and list.''' + schema = Schema(None, entry_schema) + valuelist = value + if isinstance(value, dict): + valuelist = list(value.values()) + for v in valuelist: + DataEntity.validate_datatype(schema.type, v, schema.entry_schema, + custom_def) + if schema.constraints: + for constraint in schema.constraints: + constraint.validate(v) + return value diff --git a/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml b/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml new file mode 100644 index 000000000..b819c02b4 --- /dev/null +++ b/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml @@ -0,0 +1,893 @@ +# 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. + +########################################################################## +# The content of this file reflects TOSCA Simple Profile in YAML version +# 1.0.0. It describes the definition for TOSCA types including Node Type, +# Relationship Type, Capability Type and Interfaces. +########################################################################## +tosca_definitions_version: tosca_simple_yaml_1_0 + +########################################################################## +# Node Type. +# A Node Type is a reusable entity that defines the type of one or more +# Node Templates. +########################################################################## +tosca.nodes.Root: + description: > + The TOSCA root node all other TOSCA base node types derive from. + attributes: + tosca_id: + type: string + tosca_name: + type: string + state: + type: string + capabilities: + feature: + type: tosca.capabilities.Node + requirements: + - dependency: + capability: tosca.capabilities.Node + node: tosca.nodes.Root + relationship: tosca.relationships.DependsOn + occurrences: [ 0, UNBOUNDED ] + interfaces: + Standard: + type: tosca.interfaces.node.lifecycle.Standard + +tosca.nodes.Compute: + derived_from: tosca.nodes.Root + attributes: + private_address: + type: string + public_address: + type: string + capabilities: + host: + type: tosca.capabilities.Container + binding: + type: tosca.capabilities.network.Bindable + os: + type: tosca.capabilities.OperatingSystem + scalable: + type: tosca.capabilities.Scalable + requirements: + - local_storage: + capability: tosca.capabilities.Attachment + node: tosca.nodes.BlockStorage + relationship: tosca.relationships.AttachesTo + occurrences: [0, UNBOUNDED] + +tosca.nodes.SoftwareComponent: + derived_from: tosca.nodes.Root + properties: + # domain-specific software component version + component_version: + type: version + required: false + description: > + Software component version. + admin_credential: + type: tosca.datatypes.Credential + required: false + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.Compute + relationship: tosca.relationships.HostedOn + +tosca.nodes.DBMS: + derived_from: tosca.nodes.SoftwareComponent + properties: + port: + required: no + type: integer + description: > + The port the DBMS service will listen to for data and requests. + root_password: + required: no + type: string + description: > + The root password for the DBMS service. + capabilities: + host: + type: tosca.capabilities.Container + valid_source_types: [tosca.nodes.Database] + +tosca.nodes.Database: + derived_from: tosca.nodes.Root + properties: + user: + required: no + type: string + description: > + User account name for DB administration + name: + required: no + type: string + description: > + The name of the database. + password: + required: no + type: string + description: > + The password for the DB user account + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.DBMS + relationship: tosca.relationships.HostedOn + capabilities: + database_endpoint: + type: tosca.capabilities.Endpoint.Database + +tosca.nodes.WebServer: + derived_from: tosca.nodes.SoftwareComponent + capabilities: + data_endpoint: + type: tosca.capabilities.Endpoint + admin_endpoint: + type: tosca.capabilities.Endpoint.Admin + host: + type: tosca.capabilities.Container + valid_source_types: [tosca.nodes.WebApplication] + +tosca.nodes.WebApplication: + derived_from: tosca.nodes.Root + properties: + context_root: + type: string + required: false + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.WebServer + relationship: tosca.relationships.HostedOn + capabilities: + app_endpoint: + type: tosca.capabilities.Endpoint + +tosca.nodes.BlockStorage: + derived_from: tosca.nodes.Root + properties: + size: + type: scalar-unit.size + constraints: + - greater_or_equal: 1 MB + volume_id: + type: string + required: false + snapshot_id: + type: string + required: false + attributes: + volume_id: + type: string + capabilities: + attachment: + type: tosca.capabilities.Attachment + +tosca.nodes.network.Network: + derived_from: tosca.nodes.Root + description: > + The TOSCA Network node represents a simple, logical network service. + properties: + ip_version: + type: integer + required: no + default: 4 + constraints: + - valid_values: [ 4, 6 ] + description: > + The IP version of the requested network. Valid values are 4 for ipv4 + or 6 for ipv6. + cidr: + type: string + required: no + description: > + The cidr block of the requested network. + start_ip: + type: string + required: no + description: > + The IP address to be used as the start of a pool of addresses within + the full IP range derived from the cidr block. + end_ip: + type: string + required: no + description: > + The IP address to be used as the end of a pool of addresses within + the full IP range derived from the cidr block. + gateway_ip: + type: string + required: no + description: > + The gateway IP address. + network_name: + type: string + required: no + description: > + An identifier that represents an existing Network instance in the + underlying cloud infrastructure or can be used as the name of the + newly created network. If network_name is provided and no other + properties are provided (with exception of network_id), then an + existing network instance will be used. If network_name is provided + alongside with more properties then a new network with this name will + be created. + network_id: + type: string + required: no + description: > + An identifier that represents an existing Network instance in the + underlying cloud infrastructure. This property is mutually exclusive + with all other properties except network_name. This can be used alone + or together with network_name to identify an existing network. + network_type: + type: string + required: no + description: > + It specifies the nature of the physical network in the underlying + cloud infrastructure. Examples are flat, vlan, gre or vxlan. F + segmentation_id: + type: string + required: no + description: > + A segmentation identifier in the underlying cloud infrastructure. + E.g. VLAN ID, GRE tunnel ID, etc.. + dhcp_enabled: + type: boolean + required: no + default: true + description: > + Indicates should DHCP service be enabled on the network or not. + capabilities: + link: + type: tosca.capabilities.network.Linkable + +tosca.nodes.network.Port: + derived_from: tosca.nodes.Root + description: > + The TOSCA Port node represents a logical entity that associates between + Compute and Network normative types. The Port node type effectively + represents a single virtual NIC on the Compute node instance. + properties: + ip_address: + type: string + required: no + description: > + Allow the user to set a static IP. + order: + type: integer + required: no + default: 0 + constraints: + - greater_or_equal: 0 + description: > + The order of the NIC on the compute instance (e.g. eth2). + is_default: + type: boolean + required: no + default: false + description: > + If is_default=true this port will be used for the default gateway + route. Only one port that is associated to single compute node can + set as is_default=true. + ip_range_start: + type: string + required: no + description: > + Defines the starting IP of a range to be allocated for the compute + instances that are associated with this Port. + ip_range_end: + type: string + required: no + description: > + Defines the ending IP of a range to be allocated for the compute + instances that are associated with this Port. + attributes: + ip_address: + type: string + requirements: + - binding: + description: > + Binding requirement expresses the relationship between Port and + Compute nodes. Effectevely it indicates that the Port will be + attached to specific Compute node instance + capability: tosca.capabilities.network.Bindable + relationship: tosca.relationships.network.BindsTo + - link: + description: > + Link requirement expresses the relationship between Port and Network + nodes. It indicates which network this port will connect to. + capability: tosca.capabilities.network.Linkable + relationship: tosca.relationships.network.LinksTo + +tosca.nodes.ObjectStorage: + derived_from: tosca.nodes.Root + description: > + The TOSCA ObjectStorage node represents storage that provides the ability + to store data as objects (or BLOBs of data) without consideration for the + underlying filesystem or devices + properties: + name: + type: string + required: yes + description: > + The logical name of the object store (or container). + size: + type: scalar-unit.size + required: no + constraints: + - greater_or_equal: 0 GB + description: > + The requested initial storage size. + maxsize: + type: scalar-unit.size + required: no + constraints: + - greater_or_equal: 0 GB + description: > + The requested maximum storage size. + capabilities: + storage_endpoint: + type: tosca.capabilities.Endpoint + +########################################################################## +# Relationship Type. +# A Relationship Type is a reusable entity that defines the type of one +# or more relationships between Node Types or Node Templates. +########################################################################## +tosca.relationships.Root: + description: > + The TOSCA root Relationship Type all other TOSCA base Relationship Types + derive from. + attributes: + tosca_id: + type: string + tosca_name: + type: string + interfaces: + Configure: + type: tosca.interfaces.relationship.Configure + +tosca.relationships.DependsOn: + derived_from: tosca.relationships.Root + +tosca.relationships.HostedOn: + derived_from: tosca.relationships.Root + valid_target_types: [ tosca.capabilities.Container ] + +tosca.relationships.ConnectsTo: + derived_from: tosca.relationships.Root + valid_target_types: [ tosca.capabilities.Endpoint ] + credential: + type: tosca.datatypes.Credential + required: false + +tosca.relationships.AttachesTo: + derived_from: tosca.relationships.Root + valid_target_types: [ tosca.capabilities.Attachment ] + properties: + location: + required: true + type: string + constraints: + - min_length: 1 + device: + required: false + type: string + +tosca.relationships.network.LinksTo: + derived_from: tosca.relationships.DependsOn + valid_target_types: [ tosca.capabilities.network.Linkable ] + +tosca.relationships.network.BindsTo: + derived_from: tosca.relationships.DependsOn + valid_target_types: [ tosca.capabilities.network.Bindable ] + +########################################################################## +# Capability Type. +# A Capability Type is a reusable entity that describes a kind of +# capability that a Node Type can declare to expose. +########################################################################## +tosca.capabilities.Root: + description: > + The TOSCA root Capability Type all other TOSCA base Capability Types + derive from. + +tosca.capabilities.Node: + derived_from: tosca.capabilities.Root + +tosca.capabilities.Container: + derived_from: tosca.capabilities.Root + properties: + num_cpus: + required: no + type: integer + constraints: + - greater_or_equal: 1 + cpu_frequency: + required: no + type: scalar-unit.frequency + constraints: + - greater_or_equal: 0.1 GHz + disk_size: + required: no + type: scalar-unit.size + constraints: + - greater_or_equal: 0 MB + mem_size: + required: no + type: scalar-unit.size + constraints: + - greater_or_equal: 0 MB + +tosca.capabilities.Endpoint: + derived_from: tosca.capabilities.Root + properties: + protocol: + type: string + default: tcp + port: + type: tosca.datatypes.network.PortDef + required: false + secure: + type: boolean + default: false + url_path: + type: string + required: false + port_name: + type: string + required: false + network_name: + type: string + required: false + initiator: + type: string + default: source + constraints: + - valid_values: [source, target, peer] + ports: + type: map + required: false + constraints: + - min_length: 1 + entry_schema: + type: tosca.datatypes.network.PortDef + attributes: + ip_address: + type: string + +tosca.capabilities.Endpoint.Admin: + derived_from: tosca.capabilities.Endpoint + properties: + secure: true + +tosca.capabilities.Scalable: + derived_from: tosca.capabilities.Root + properties: + min_instances: + type: integer + required: yes + default: 1 + description: > + This property is used to indicate the minimum number of instances + that should be created for the associated TOSCA Node Template by + a TOSCA orchestrator. + max_instances: + type: integer + required: yes + default: 1 + description: > + This property is used to indicate the maximum number of instances + that should be created for the associated TOSCA Node Template by + a TOSCA orchestrator. + default_instances: + type: integer + required: no + description: > + An optional property that indicates the requested default number + of instances that should be the starting number of instances a + TOSCA orchestrator should attempt to allocate. + The value for this property MUST be in the range between the values + set for min_instances and max_instances properties. + +tosca.capabilities.Endpoint.Database: + derived_from: tosca.capabilities.Endpoint + +tosca.capabilities.Attachment: + derived_from: tosca.capabilities.Root + +tosca.capabilities.network.Linkable: + derived_from: tosca.capabilities.Root + description: > + A node type that includes the Linkable capability indicates that it can + be pointed by tosca.relationships.network.LinksTo relationship type, which + represents an association relationship between Port and Network node types. + +tosca.capabilities.network.Bindable: + derived_from: tosca.capabilities.Root + description: > + A node type that includes the Bindable capability indicates that it can + be pointed by tosca.relationships.network.BindsTo relationship type, which + represents a network association relationship between Port and Compute node + types. + +tosca.capabilities.OperatingSystem: + derived_from: tosca.capabilities.Root + properties: + architecture: + required: false + type: string + description: > + The host Operating System (OS) architecture. + type: + required: false + type: string + description: > + The host Operating System (OS) type. + distribution: + required: false + type: string + description: > + The host Operating System (OS) distribution. Examples of valid values + for an “type” of “Linux” would include: + debian, fedora, rhel and ubuntu. + version: + required: false + type: version + description: > + The host Operating System version. + +########################################################################## + # Interfaces Type. + # The Interfaces element describes a list of one or more interface + # definitions for a modelable entity (e.g., a Node or Relationship Type) + # as defined within the TOSCA Simple Profile specification. +########################################################################## +tosca.interfaces.node.lifecycle.Standard: + create: + description: Standard lifecycle create operation. + configure: + description: Standard lifecycle configure operation. + start: + description: Standard lifecycle start operation. + stop: + description: Standard lifecycle stop operation. + delete: + description: Standard lifecycle delete operation. + +tosca.interfaces.relationship.Configure: + pre_configure_source: + description: Operation to pre-configure the source endpoint. + pre_configure_target: + description: Operation to pre-configure the target endpoint. + post_configure_source: + description: Operation to post-configure the source endpoint. + post_configure_target: + description: Operation to post-configure the target endpoint. + add_target: + description: Operation to add a target node. + remove_target: + description: Operation to remove a target node. + add_source: > + description: Operation to notify the target node of a source node which + is now available via a relationship. + description: + target_changed: > + description: Operation to notify source some property or attribute of the + target changed + +########################################################################## + # Data Type. + # A Datatype is a complex data type declaration which contains other + # complex or simple data types. +########################################################################## +tosca.datatypes.network.NetworkInfo: + properties: + network_name: + type: string + network_id: + type: string + addresses: + type: list + entry_schema: + type: string + +tosca.datatypes.network.PortInfo: + properties: + port_name: + type: string + port_id: + type: string + network_id: + type: string + mac_address: + type: string + addresses: + type: list + entry_schema: + type: string + +tosca.datatypes.network.PortDef: + type: integer + constraints: + - in_range: [ 1, 65535 ] + +tosca.datatypes.network.PortSpec: + properties: + protocol: + type: string + required: true + default: tcp + constraints: + - valid_values: [ udp, tcp, igmp ] + target: + type: list + entry_schema: + type: PortDef + target_range: + type: range + constraints: + - in_range: [ 1, 65535 ] + source: + type: list + entry_schema: + type: PortDef + source_range: + type: range + constraints: + - in_range: [ 1, 65535 ] + +tosca.datatypes.Credential: + properties: + protocol: + type: string + token_type: + type: string + token: + type: string + keys: + type: map + entry_schema: + type: string + user: + type: string + required: false + +########################################################################## + # Artifact Type. + # An Artifact Type is a reusable entity that defines the type of one or more + # files which Node Types or Node Templates can have dependent relationships + # and used during operations such as during installation or deployment. +########################################################################## +tosca.artifacts.Root: + description: > + The TOSCA Artifact Type all other TOSCA Artifact Types derive from + properties: + version: version + +tosca.artifacts.File: + derived_from: tosca.artifacts.Root + +tosca.artifacts.Deployment: + derived_from: tosca.artifacts.Root + description: TOSCA base type for deployment artifacts + +tosca.artifacts.Deployment.Image: + derived_from: tosca.artifacts.Deployment + +tosca.artifacts.Deployment.Image.VM: + derived_from: tosca.artifacts.Deployment.Image + +tosca.artifacts.Implementation: + derived_from: tosca.artifacts.Root + description: TOSCA base type for implementation artifacts + +tosca.artifacts.Implementation.Bash: + derived_from: tosca.artifacts.Implementation + description: Script artifact for the Unix Bash shell + mime_type: application/x-sh + file_ext: [ sh ] + +tosca.artifacts.Implementation.Python: + derived_from: tosca.artifacts.Implementation + description: Artifact for the interpreted Python language + mime_type: application/x-python + file_ext: [ py ] + +tosca.artifacts.Deployment.Image.Container.Docker: + derived_from: tosca.artifacts.Deployment.Image + description: Docker container image + +tosca.artifacts.Deployment.Image.VM.ISO: + derived_from: tosca.artifacts.Deployment.Image + description: Virtual Machine (VM) image in ISO disk format + mime_type: application/octet-stream + file_ext: [ iso ] + +tosca.artifacts.Deployment.Image.VM.QCOW2: + derived_from: tosca.artifacts.Deployment.Image + description: Virtual Machine (VM) image in QCOW v2 standard disk format + mime_type: application/octet-stream + file_ext: [ qcow2 ] + +########################################################################## + # Policy Type. + # TOSCA Policy Types represent logical grouping of TOSCA nodes that have + # an implied relationship and need to be orchestrated or managed together + # to achieve some result. +########################################################################## +tosca.policies.Root: + description: The TOSCA Policy Type all other TOSCA Policy Types derive from. + +tosca.policies.Placement: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to govern + placement of TOSCA nodes or groups of nodes. + +tosca.policies.Scaling: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to govern + scaling of TOSCA nodes or groups of nodes. + +tosca.policies.Update: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to govern + update of TOSCA nodes or groups of nodes. + +tosca.policies.Performance: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to declare + performance requirements for TOSCA nodes or groups of nodes. + +# Miguel: new types + +tosca.nodes.Database.MySQL: + derived_from: tosca.nodes.Database + properties: + password: + type: string + required: true + name: + type: string + required: true + user: + type: string + required: true + root_password: + type: string + required: true + requirements: + - host: + capability: tosca.capabilities.Container + relationship: tosca.relationships.HostedOn + node: tosca.nodes.DBMS.MySQL + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_configure.yml + inputs: + password: { get_property: [ SELF, password ] } + name: { get_property: [ SELF, name ] } + user: { get_property: [ SELF, user ] } + root_password: { get_property: [ SELF, root_password ] } + + +tosca.nodes.DBMS.MySQL: + derived_from: tosca.nodes.DBMS + properties: + port: + type: integer + description: reflect the default MySQL server port + default: 3306 + root_password: + type: string + # MySQL requires a root_password for configuration + required: true + capabilities: + # Further constrain the ‘host’ capability to only allow MySQL databases + host: + type: tosca.capabilities.Container + valid_source_types: [ tosca.nodes.Database.MySQL ] + interfaces: + Standard: + create: mysql/mysql_install.yml + configure: + implementation: mysql/mysql_configure.yml + inputs: + root_password: { get_property: [ SELF, root_password ] } + port: { get_property: [ SELF, port ] } + +tosca.nodes.WebServer.Apache: + derived_from: tosca.nodes.WebServer + interfaces: + Standard: + create: apache/apache_install.yml + +# INDIGO non normative types + +tosca.nodes.indigo.GalaxyPortal: + derived_from: tosca.nodes.WebServer + properties: + admin: + type: string + description: email of the admin user + default: admin@admin.com + required: false + admin_api_key: + type: string + description: key to access the API with admin role + default: not_very_secret_api_key + required: false + user: + type: string + description: username to launch the galaxy daemon + default: galaxy + required: false + install_path: + type: string + description: path to install the galaxy tool + default: /home/galaxy/galaxy + required: false + interfaces: + Standard: + create: + implementation: galaxy/galaxy_install.yml + inputs: + galaxy_install_path: { get_property: [ SELF, install_path ] } + configure: + implementation: galaxy/galaxy_configure.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + galaxy_admin: { get_property: [ SELF, admin ] } + galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } + start: + implementation: galaxy/galaxy_start.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + + +tosca.nodes.indigo.GalaxyTool: + derived_from: tosca.nodes.WebApplication + properties: + name: + type: string + description: name of the tool + required: true + owner: + type: string + description: developer of the tool + required: true + tool_panel_section_id: + type: string + description: panel section to install the tool + required: true + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.indigo.GalaxyPortal + relationship: tosca.relationships.HostedOn + interfaces: + Standard: + create: + implementation: galaxy/galaxy_tools_configure.yml + inputs: + galaxy_install_path: { get_property: [ HOST, install_path ] } + galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } + galaxy_tool_name: { get_property: [ SELF, name ] } + galaxy_tool_owner: { get_property: [ SELF, owner ] } + galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } diff --git a/IM/tosca/toscaparser/elements/__init__.py b/IM/tosca/toscaparser/elements/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/elements/artifacttype.py b/IM/tosca/toscaparser/elements/artifacttype.py new file mode 100644 index 000000000..e0897b3d7 --- /dev/null +++ b/IM/tosca/toscaparser/elements/artifacttype.py @@ -0,0 +1,45 @@ +# 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. + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class ArtifactTypeDef(StatefulEntityType): + '''TOSCA built-in artifacts type.''' + + def __init__(self, atype, custom_def=None): + super(ArtifactTypeDef, self).__init__(atype, self.ARTIFACT_PREFIX, + custom_def) + self.type = atype + self.properties = None + if self.PROPERTIES in self.defs: + self.properties = self.defs[self.PROPERTIES] + self.parent_artifacts = self._get_parent_artifacts() + + def _get_parent_artifacts(self): + artifacts = {} + parent_artif = self.parent_type + if parent_artif: + while parent_artif != 'tosca.artifacts.Root': + artifacts[parent_artif] = self.TOSCA_DEF[parent_artif] + parent_artif = artifacts[parent_artif]['derived_from'] + return artifacts + + @property + def parent_type(self): + '''Return an artifact this artifact is derived from.''' + return self.derived_from(self.defs) + + def get_artifact(self, name): + '''Return the definition of an artifact field by name.''' + if name in self.defs: + return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/attribute_definition.py b/IM/tosca/toscaparser/elements/attribute_definition.py new file mode 100644 index 000000000..35ba27f22 --- /dev/null +++ b/IM/tosca/toscaparser/elements/attribute_definition.py @@ -0,0 +1,20 @@ +# 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. + + +class AttributeDef(object): + '''TOSCA built-in Attribute type.''' + + def __init__(self, name, value=None, schema=None): + self.name = name + self.value = value + self.schema = schema diff --git a/IM/tosca/toscaparser/elements/capabilitytype.py b/IM/tosca/toscaparser/elements/capabilitytype.py new file mode 100644 index 000000000..b1bd7d767 --- /dev/null +++ b/IM/tosca/toscaparser/elements/capabilitytype.py @@ -0,0 +1,71 @@ +# 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. + +from IM.tosca.toscaparser.elements.property_definition import PropertyDef +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class CapabilityTypeDef(StatefulEntityType): + '''TOSCA built-in capabilities type.''' + + def __init__(self, name, ctype, ntype, custom_def=None): + self.name = name + super(CapabilityTypeDef, self).__init__(ctype, self.CAPABILITY_PREFIX, + custom_def) + self.nodetype = ntype + self.properties = None + if self.PROPERTIES in self.defs: + self.properties = self.defs[self.PROPERTIES] + self.parent_capabilities = self._get_parent_capabilities() + + def get_properties_def_objects(self): + '''Return a list of property definition objects.''' + properties = [] + parent_properties = {} + if self.parent_capabilities: + for type, value in self.parent_capabilities.items(): + parent_properties[type] = value.get('properties') + if self.properties: + for prop, schema in self.properties.items(): + # Miguel: Cambios aqui + if isinstance(schema, dict): + properties.append(PropertyDef(prop, None, schema)) + if parent_properties: + for parent, props in parent_properties.items(): + for prop, schema in props.items(): + properties.append(PropertyDef(prop, None, schema)) + return properties + + def get_properties_def(self): + '''Return a dictionary of property definition name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_def_objects()} + + def get_property_def_value(self, name): + '''Return the definition of a given property name.''' + props_def = self.get_properties_def() + if props_def and name in props_def: + return props_def[name].value + + def _get_parent_capabilities(self): + capabilities = {} + parent_cap = self.parent_type + if parent_cap: + while parent_cap != 'tosca.capabilities.Root': + capabilities[parent_cap] = self.TOSCA_DEF[parent_cap] + parent_cap = capabilities[parent_cap]['derived_from'] + return capabilities + + @property + def parent_type(self): + '''Return a capability this capability is derived from.''' + return self.derived_from(self.defs) diff --git a/IM/tosca/toscaparser/elements/constraints.py b/IM/tosca/toscaparser/elements/constraints.py new file mode 100644 index 000000000..2f38eeffd --- /dev/null +++ b/IM/tosca/toscaparser/elements/constraints.py @@ -0,0 +1,569 @@ +# 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. + +import collections +import datetime +import re + +from IM.tosca.toscaparser.common.exception import InvalidSchemaError +from IM.tosca.toscaparser.common.exception import ValidationError +from IM.tosca.toscaparser.elements import scalarunit +from IM.tosca.toscaparser.functions import is_function +from IM.tosca.toscaparser.utils.gettextutils import _ + + +class Schema(collections.Mapping): + + KEYS = ( + TYPE, REQUIRED, DESCRIPTION, + DEFAULT, CONSTRAINTS, ENTRYSCHEMA + ) = ( + 'type', 'required', 'description', + 'default', 'constraints', 'entry_schema' + ) + + PROPERTY_TYPES = ( + INTEGER, STRING, BOOLEAN, FLOAT, + NUMBER, TIMESTAMP, LIST, MAP, + SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME, + PORTDEF, VERSION + ) = ( + 'integer', 'string', 'boolean', 'float', + 'number', 'timestamp', 'list', 'map', + 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time', + 'PortDef', 'version' + ) + + SCALAR_UNIT_SIZE_DEFAULT = 'B' + SCALAR_UNIT_SIZE_DICT = {'B': 1, 'KB': 1000, 'KIB': 1024, 'MB': 1000000, + 'MIB': 1048576, 'GB': 1000000000, + 'GIB': 1073741824, 'TB': 1000000000000, + 'TIB': 1099511627776} + + def __init__(self, name, schema_dict): + self.name = name + if not isinstance(schema_dict, collections.Mapping): + msg = _("Schema %(pname)s must be a dict.") % dict(pname=name) + raise InvalidSchemaError(message=msg) + + try: + schema_dict['type'] + except KeyError: + msg = _("Schema %(pname)s must have type.") % dict(pname=name) + raise InvalidSchemaError(message=msg) + + self.schema = schema_dict + self._len = None + self.constraints_list = [] + + @property + def type(self): + return self.schema[self.TYPE] + + @property + def required(self): + return self.schema.get(self.REQUIRED, True) + + @property + def description(self): + return self.schema.get(self.DESCRIPTION, '') + + @property + def default(self): + return self.schema.get(self.DEFAULT) + + @property + def constraints(self): + if not self.constraints_list: + constraint_schemata = self.schema.get(self.CONSTRAINTS) + if constraint_schemata: + self.constraints_list = [Constraint(self.name, + self.type, + cschema) + for cschema in constraint_schemata] + return self.constraints_list + + @property + def entry_schema(self): + return self.schema.get(self.ENTRYSCHEMA) + + def __getitem__(self, key): + return self.schema[key] + + def __iter__(self): + for k in self.KEYS: + try: + self.schema[k] + except KeyError: + pass + else: + yield k + + def __len__(self): + if self._len is None: + self._len = len(list(iter(self))) + return self._len + + +class Constraint(object): + '''Parent class for constraints for a Property or Input.''' + + CONSTRAINTS = (EQUAL, GREATER_THAN, + GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_RANGE, + VALID_VALUES, LENGTH, MIN_LENGTH, MAX_LENGTH, PATTERN) = \ + ('equal', 'greater_than', 'greater_or_equal', 'less_than', + 'less_or_equal', 'in_range', 'valid_values', 'length', + 'min_length', 'max_length', 'pattern') + + def __new__(cls, property_name, property_type, constraint): + if cls is not Constraint: + return super(Constraint, cls).__new__(cls) + + if(not isinstance(constraint, collections.Mapping) or + len(constraint) != 1): + raise InvalidSchemaError(message=_('Invalid constraint schema.')) + + for type in constraint.keys(): + ConstraintClass = get_constraint_class(type) + if not ConstraintClass: + msg = _('Invalid constraint type "%s".') % type + raise InvalidSchemaError(message=msg) + + return ConstraintClass(property_name, property_type, constraint) + + def __init__(self, property_name, property_type, constraint): + self.property_name = property_name + self.property_type = property_type + self.constraint_value = constraint[self.constraint_key] + self.constraint_value_msg = self.constraint_value + if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: + self.constraint_value = self._get_scalarunit_constraint_value() + # check if constraint is valid for property type + if property_type not in self.valid_prop_types: + msg = _('Constraint type "%(ctype)s" is not valid ' + 'for data type "%(dtype)s".') % dict( + ctype=self.constraint_key, + dtype=property_type) + raise InvalidSchemaError(message=msg) + + def _get_scalarunit_constraint_value(self): + if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: + ScalarUnit_Class = (scalarunit. + get_scalarunit_class(self.property_type)) + if isinstance(self.constraint_value, list): + return [ScalarUnit_Class(v).get_num_from_scalar_unit() + for v in self.constraint_value] + else: + return (ScalarUnit_Class(self.constraint_value). + get_num_from_scalar_unit()) + + def _err_msg(self, value): + return _('Property %s could not be validated.') % self.property_name + + def validate(self, value): + self.value_msg = value + if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: + value = scalarunit.get_scalarunit_value(self.property_type, value) + if not self._is_valid(value): + err_msg = self._err_msg(value) + raise ValidationError(message=err_msg) + + +class Equal(Constraint): + """Constraint class for "equal" + + Constrains a property or parameter to a value equal to ('=') + the value declared. + """ + + constraint_key = Constraint.EQUAL + + valid_prop_types = Schema.PROPERTY_TYPES + + def _is_valid(self, value): + if value == self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s is not equal to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class GreaterThan(Constraint): + """Constraint class for "greater_than" + + Constrains a property or parameter to a value greater than ('>') + the value declared. + """ + + constraint_key = Constraint.GREATER_THAN + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(GreaterThan, self).__init__(property_name, property_type, + constraint) + if not isinstance(constraint[self.GREATER_THAN], self.valid_types): + raise InvalidSchemaError(message=_('greater_than must ' + 'be comparable.')) + + def _is_valid(self, value): + if value > self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be greater than "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class GreaterOrEqual(Constraint): + """Constraint class for "greater_or_equal" + + Constrains a property or parameter to a value greater than or equal + to ('>=') the value declared. + """ + + constraint_key = Constraint.GREATER_OR_EQUAL + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(GreaterOrEqual, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('greater_or_equal must ' + 'be comparable.')) + + def _is_valid(self, value): + if is_function(value) or value >= self.constraint_value: + return True + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be greater or equal ' + 'to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class LessThan(Constraint): + """Constraint class for "less_than" + + Constrains a property or parameter to a value less than ('<') + the value declared. + """ + + constraint_key = Constraint.LESS_THAN + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(LessThan, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('less_than must ' + 'be comparable.')) + + def _is_valid(self, value): + if value < self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be less than "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class LessOrEqual(Constraint): + """Constraint class for "less_or_equal" + + Constrains a property or parameter to a value less than or equal + to ('<=') the value declared. + """ + + constraint_key = Constraint.LESS_OR_EQUAL + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(LessOrEqual, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('less_or_equal must ' + 'be comparable.')) + + def _is_valid(self, value): + if value <= self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be less or ' + 'equal to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class InRange(Constraint): + """Constraint class for "in_range" + + Constrains a property or parameter to a value in range of (inclusive) + the two values declared. + """ + + constraint_key = Constraint.IN_RANGE + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(InRange, self).__init__(property_name, property_type, constraint) + if(not isinstance(self.constraint_value, collections.Sequence) or + (len(constraint[self.IN_RANGE]) != 2)): + raise InvalidSchemaError(message=_('in_range must be a list.')) + + for value in self.constraint_value: + if not isinstance(value, self.valid_types): + raise InvalidSchemaError(_('in_range value must ' + 'be comparable.')) + + self.min = self.constraint_value[0] + self.max = self.constraint_value[1] + + def _is_valid(self, value): + if value < self.min: + return False + if value > self.max: + return False + + return True + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s is out of range ' + '(min:%(vmin)s, max:%(vmax)s).') % + dict(pname=self.property_name, + pvalue=self.value_msg, + vmin=self.constraint_value_msg[0], + vmax=self.constraint_value_msg[1])) + + +class ValidValues(Constraint): + """Constraint class for "valid_values" + + Constrains a property or parameter to a value that is in the list of + declared values. + """ + constraint_key = Constraint.VALID_VALUES + + valid_prop_types = Schema.PROPERTY_TYPES + + def __init__(self, property_name, property_type, constraint): + super(ValidValues, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, collections.Sequence): + raise InvalidSchemaError(message=_('valid_values must be a list.')) + + def _is_valid(self, value): + if isinstance(value, list): + return all(v in self.constraint_value for v in value) + return value in self.constraint_value + + def _err_msg(self, value): + allowed = '[%s]' % ', '.join(str(a) for a in self.constraint_value) + return (_('%(pname)s: %(pvalue)s is not an valid ' + 'value "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=allowed)) + + +class Length(Constraint): + """Constraint class for "length" + + Constrains the property or parameter to a value of a given length. + """ + + constraint_key = Constraint.LENGTH + + valid_types = (int, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(Length, self).__init__(property_name, property_type, constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('length must be integer.')) + + def _is_valid(self, value): + if isinstance(value, str) and len(value) == self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('length of %(pname)s: %(pvalue)s must be equal ' + 'to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +class MinLength(Constraint): + """Constraint class for "min_length" + + Constrains the property or parameter to a value to a minimum length. + """ + + constraint_key = Constraint.MIN_LENGTH + + valid_types = (int, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(MinLength, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('min_length must be integer.')) + + def _is_valid(self, value): + if isinstance(value, str) and len(value) >= self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('length of %(pname)s: %(pvalue)s must be ' + 'at least "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +class MaxLength(Constraint): + """Constraint class for "max_length" + + Constrains the property or parameter to a value to a maximum length. + """ + + constraint_key = Constraint.MAX_LENGTH + + valid_types = (int, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(MaxLength, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('max_length must be integer.')) + + def _is_valid(self, value): + if isinstance(value, str) and len(value) <= self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('length of %(pname)s: %(pvalue)s must be no greater ' + 'than "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +class Pattern(Constraint): + """Constraint class for "pattern" + + Constrains the property or parameter to a value that is allowed by + the provided regular expression. + """ + + constraint_key = Constraint.PATTERN + + valid_types = (str, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(Pattern, self).__init__(property_name, property_type, constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('pattern must be string.')) + self.match = re.compile(self.constraint_value).match + + def _is_valid(self, value): + match = self.match(value) + return match is not None and match.end() == len(value) + + def _err_msg(self, value): + return (_('%(pname)s: "%(pvalue)s" does not match ' + 'pattern "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +constraint_mapping = { + Constraint.EQUAL: Equal, + Constraint.GREATER_THAN: GreaterThan, + Constraint.GREATER_OR_EQUAL: GreaterOrEqual, + Constraint.LESS_THAN: LessThan, + Constraint.LESS_OR_EQUAL: LessOrEqual, + Constraint.IN_RANGE: InRange, + Constraint.VALID_VALUES: ValidValues, + Constraint.LENGTH: Length, + Constraint.MIN_LENGTH: MinLength, + Constraint.MAX_LENGTH: MaxLength, + Constraint.PATTERN: Pattern + } + + +def get_constraint_class(type): + return constraint_mapping.get(type) diff --git a/IM/tosca/toscaparser/elements/datatype.py b/IM/tosca/toscaparser/elements/datatype.py new file mode 100644 index 000000000..e66c3b79c --- /dev/null +++ b/IM/tosca/toscaparser/elements/datatype.py @@ -0,0 +1,56 @@ +# 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. + + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class DataType(StatefulEntityType): + '''TOSCA built-in and user defined complex data type.''' + + def __init__(self, datatypename, custom_def=None): + super(DataType, self).__init__(datatypename, self.DATATYPE_PREFIX, + custom_def) + self.custom_def = custom_def + + @property + def parent_type(self): + '''Return a datatype this datatype is derived from.''' + ptype = self.derived_from(self.defs) + if ptype: + return DataType(ptype, self.custom_def) + return None + + @property + def value_type(self): + '''Return 'type' section in the datatype schema.''' + return self.entity_value(self.defs, 'type') + + def get_all_properties_objects(self): + '''Return all properties objects defined in type and parent type.''' + props_def = self.get_properties_def_objects() + ptype = self.parent_type + while ptype: + props_def.extend(ptype.get_properties_def_objects()) + ptype = ptype.parent_type + return props_def + + def get_all_properties(self): + '''Return a dictionary of all property definition name-object pairs.''' + return {prop.name: prop + for prop in self.get_all_properties_objects()} + + def get_all_property_value(self, name): + '''Return the value of a given property name.''' + props_def = self.get_all_properties() + if props_def and name in props_def.key(): + return props_def[name].value diff --git a/IM/tosca/toscaparser/elements/entity_type.py b/IM/tosca/toscaparser/elements/entity_type.py new file mode 100644 index 000000000..241556093 --- /dev/null +++ b/IM/tosca/toscaparser/elements/entity_type.py @@ -0,0 +1,113 @@ +# 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. + +import logging +import os +import IM.tosca.toscaparser.utils.yamlparser + +log = logging.getLogger('tosca') + + +class EntityType(object): + '''Base class for TOSCA elements.''' + + SECTIONS = (DERIVED_FROM, PROPERTIES, ATTRIBUTES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE, ARTIFACTS) = \ + ('derived_from', 'properties', 'attributes', 'requirements', + 'interfaces', 'capabilities', 'type', 'artifacts') + + '''TOSCA definition file.''' + TOSCA_DEF_FILE = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "TOSCA_definition_1_0.yaml") + + loader = IM.tosca.toscaparser.utils.yamlparser.load_yaml + + TOSCA_DEF = loader(TOSCA_DEF_FILE) + + RELATIONSHIP_TYPE = (DEPENDSON, HOSTEDON, CONNECTSTO, ATTACHESTO, + LINKSTO, BINDSTO) = \ + ('tosca.relationships.DependsOn', + 'tosca.relationships.HostedOn', + 'tosca.relationships.ConnectsTo', + 'tosca.relationships.AttachesTo', + 'tosca.relationships.network.LinksTo', + 'tosca.relationships.network.BindsTo') + + NODE_PREFIX = 'tosca.nodes.' + RELATIONSHIP_PREFIX = 'tosca.relationships.' + CAPABILITY_PREFIX = 'tosca.capabilities.' + INTERFACE_PREFIX = 'tosca.interfaces.' + ARTIFACT_PREFIX = 'tosca.artifacts.' + POLICY_PREFIX = 'tosca.policies.' + # currently the data types are defined only for network + # but may have changes in the future. + DATATYPE_PREFIX = 'tosca.datatypes.network.' + TOSCA = 'tosca' + + def derived_from(self, defs): + '''Return a type this type is derived from.''' + return self.entity_value(defs, 'derived_from') + + def is_derived_from(self, type_str): + '''Check if object inherits from the given type. + + Returns true if this object is derived from 'type_str'. + False otherwise. + ''' + if not self.type: + return False + elif self.type == type_str: + return True + elif self.parent_type: + return self.parent_type.is_derived_from(type_str) + else: + return False + + def entity_value(self, defs, key): + if key in defs: + return defs[key] + + def get_value(self, ndtype, defs=None, parent=None): + value = None + if defs is None: + defs = self.defs + if ndtype in defs: + value = defs[ndtype] + if parent and not value: + p = self.parent_type + while value is None: + # check parent node + if not p: + break + if p and p.type == 'tosca.nodes.Root': + break + value = p.get_value(ndtype) + p = p.parent_type + return value + + def get_definition(self, ndtype): + value = None + defs = self.defs + if ndtype in defs: + value = defs[ndtype] + p = self.parent_type + if p: + inherited = p.get_definition(ndtype) + if inherited: + inherited = dict(inherited) + if not value: + value = inherited + else: + inherited.update(value) + value.update(inherited) + return value diff --git a/IM/tosca/toscaparser/elements/interfaces.py b/IM/tosca/toscaparser/elements/interfaces.py new file mode 100644 index 000000000..b763294f8 --- /dev/null +++ b/IM/tosca/toscaparser/elements/interfaces.py @@ -0,0 +1,74 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + +SECTIONS = (LIFECYCLE, CONFIGURE, LIFECYCLE_SHORTNAME, + CONFIGURE_SHORTNAME) = \ + ('tosca.interfaces.node.lifecycle.Standard', + 'tosca.interfaces.relationship.Configure', + 'Standard', 'Configure') + +INTERFACEVALUE = (IMPLEMENTATION, INPUTS) = ('implementation', 'inputs') + + +class InterfacesDef(StatefulEntityType): + '''TOSCA built-in interfaces type.''' + + def __init__(self, node_type, interfacetype, + node_template=None, name=None, value=None): + self.ntype = node_type + self.node_template = node_template + self.type = interfacetype + self.name = name + self.value = value + self.implementation = None + self.inputs = None + self.defs = {} + if interfacetype == LIFECYCLE_SHORTNAME: + interfacetype = LIFECYCLE + if interfacetype == CONFIGURE_SHORTNAME: + interfacetype = CONFIGURE + if node_type: + self.defs = self.TOSCA_DEF[interfacetype] + if value: + if isinstance(self.value, dict): + for i, j in self.value.items(): + if i == IMPLEMENTATION: + self.implementation = j + elif i == INPUTS: + self.inputs = j + else: + what = ('Interfaces of template %s' % + self.node_template.name) + raise UnknownFieldError(what=what, field=i) + else: + self.implementation = value + + @property + def lifecycle_ops(self): + if self.defs: + if self.type == LIFECYCLE: + return self._ops() + + @property + def configure_ops(self): + if self.defs: + if self.type == CONFIGURE: + return self._ops() + + def _ops(self): + ops = [] + for name in list(self.defs.keys()): + ops.append(name) + return ops diff --git a/IM/tosca/toscaparser/elements/nodetype.py b/IM/tosca/toscaparser/elements/nodetype.py new file mode 100644 index 000000000..f0ee53453 --- /dev/null +++ b/IM/tosca/toscaparser/elements/nodetype.py @@ -0,0 +1,200 @@ +# 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. + +from IM.tosca.toscaparser.elements.capabilitytype import CapabilityTypeDef +import IM.tosca.toscaparser.elements.interfaces as ifaces +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class NodeType(StatefulEntityType): + '''TOSCA built-in node type.''' + + def __init__(self, ntype, custom_def=None): + super(NodeType, self).__init__(ntype, self.NODE_PREFIX, custom_def) + self.custom_def = custom_def + + @property + def parent_type(self): + '''Return a node this node is derived from.''' + pnode = self.derived_from(self.defs) + if pnode: + return NodeType(pnode) + + @property + def relationship(self): + '''Return a dictionary of relationships to other node types. + + This method returns a dictionary of named relationships that nodes + of the current node type (self) can have to other nodes (of specific + types) in a TOSCA template. + + ''' + relationship = {} + requires = self.get_all_requirements() + if requires: + # NOTE(sdmonov): Check if requires is a dict. + # If it is a dict convert it to a list of dicts. + # This is needed because currently the code below supports only + # lists as requirements definition. The following check will + # make sure if a map (dict) was provided it will be converted to + # a list before proceeding to the parsing. + if isinstance(requires, dict): + requires = [{key: value} for key, value in requires.items()] + + keyword = None + node_type = None + for require in requires: + for key, req in require.items(): + if 'relationship' in req: + relation = req.get('relationship') + if 'type' in relation: + relation = relation.get('type') + node_type = req.get('node') + value = req + if node_type: + keyword = 'node' + else: + # If value is a dict and has a type key + # we need to lookup the node type using + # the capability type + value = req + if isinstance(value, dict): + captype = value['capability'] + value = (self. + _get_node_type_by_cap(key, captype)) + relation = self._get_relation(key, value) + keyword = key + node_type = value + rtype = RelationshipType(relation, keyword, req) + relatednode = NodeType(node_type, self.custom_def) + relationship[rtype] = relatednode + return relationship + + def _get_node_type_by_cap(self, key, cap): + '''Find the node type that has the provided capability + + This method will lookup all node types if they have the + provided capability. + ''' + + # Filter the node types + node_types = [node_type for node_type in self.TOSCA_DEF.keys() + if node_type.startswith(self.NODE_PREFIX) and + node_type != 'tosca.nodes.Root'] + + for node_type in node_types: + node_def = self.TOSCA_DEF[node_type] + if isinstance(node_def, dict) and 'capabilities' in node_def: + node_caps = node_def['capabilities'] + for value in node_caps.values(): + if isinstance(value, dict) and \ + 'type' in value and value['type'] == cap: + return node_type + + def _get_relation(self, key, ndtype): + relation = None + ntype = NodeType(ndtype) + caps = ntype.get_capabilities() + if caps and key in caps.keys(): + c = caps[key] + for r in self.RELATIONSHIP_TYPE: + rtypedef = ntype.TOSCA_DEF[r] + for properties in rtypedef.values(): + if c.type in properties: + relation = r + break + if relation: + break + else: + for properties in rtypedef.values(): + if c.parent_type in properties: + relation = r + break + return relation + + def get_capabilities_objects(self): + '''Return a list of capability objects.''' + typecapabilities = [] + caps = self.get_value(self.CAPABILITIES) + if caps is None: + caps = self.get_value(self.CAPABILITIES, None, True) + if caps: + for name, value in caps.items(): + ctype = value.get('type') + cap = CapabilityTypeDef(name, ctype, self.type, + self.custom_def) + typecapabilities.append(cap) + return typecapabilities + + def get_capabilities(self): + '''Return a dictionary of capability name-objects pairs.''' + return {cap.name: cap + for cap in self.get_capabilities_objects()} + + @property + def requirements(self): + return self.get_value(self.REQUIREMENTS) + + def get_all_requirements(self): + requires = self.requirements + parent_node = self.parent_type + if requires is None: + requires = self.get_value(self.REQUIREMENTS, None, True) + parent_node = parent_node.parent_type + if parent_node: + while parent_node.type != 'tosca.nodes.Root': + req = parent_node.get_value(self.REQUIREMENTS, None, True) + for r in req: + if r not in requires: + requires.append(r) + parent_node = parent_node.parent_type + return requires + + @property + def interfaces(self): + return self.get_value(self.INTERFACES) + + @property + def lifecycle_inputs(self): + '''Return inputs to life cycle operations if found.''' + inputs = [] + interfaces = self.interfaces + if interfaces: + for name, value in interfaces.items(): + if name == ifaces.LIFECYCLE: + for x, y in value.items(): + if x == 'inputs': + for i in y.iterkeys(): + inputs.append(i) + return inputs + + @property + def lifecycle_operations(self): + '''Return available life cycle operations if found.''' + ops = None + interfaces = self.interfaces + if interfaces: + i = InterfacesDef(self.type, ifaces.LIFECYCLE) + ops = i.lifecycle_ops + return ops + + def get_capability(self, name): + caps = self.get_capabilities() + if caps and name in caps.keys(): + return caps[name].value + + def get_capability_type(self, name): + captype = self.get_capability(name) + if captype and name in captype.keys(): + return captype[name].value diff --git a/IM/tosca/toscaparser/elements/policytype.py b/IM/tosca/toscaparser/elements/policytype.py new file mode 100644 index 000000000..573e04509 --- /dev/null +++ b/IM/tosca/toscaparser/elements/policytype.py @@ -0,0 +1,45 @@ +# 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. + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class PolicyType(StatefulEntityType): + '''TOSCA built-in policies type.''' + + def __init__(self, ptype, custom_def=None): + super(PolicyType, self).__init__(ptype, self.POLICY_PREFIX, + custom_def) + self.type = ptype + self.properties = None + if self.PROPERTIES in self.defs: + self.properties = self.defs[self.PROPERTIES] + self.parent_policies = self._get_parent_policies() + + def _get_parent_policies(self): + policies = {} + parent_policy = self.parent_type + if parent_policy: + while parent_policy != 'tosca.policies.Root': + policies[parent_policy] = self.TOSCA_DEF[parent_policy] + parent_policy = policies[parent_policy]['derived_from'] + return policies + + @property + def parent_type(self): + '''Return a policy this policy is derived from.''' + return self.derived_from(self.defs) + + def get_policy(self, name): + '''Return the definition of a policy field by name.''' + if name in self.defs: + return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/property_definition.py b/IM/tosca/toscaparser/elements/property_definition.py new file mode 100644 index 000000000..c2c7f0089 --- /dev/null +++ b/IM/tosca/toscaparser/elements/property_definition.py @@ -0,0 +1,46 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import InvalidSchemaError +# Miguel: add import +from IM.tosca.toscaparser.utils.gettextutils import _ + +class PropertyDef(object): + '''TOSCA built-in Property type.''' + + def __init__(self, name, value=None, schema=None): + self.name = name + self.value = value + self.schema = schema + + try: + self.schema['type'] + except KeyError: + msg = (_("Property definition of %(pname)s must have type.") % + dict(pname=self.name)) + raise InvalidSchemaError(message=msg) + + @property + def required(self): + if self.schema: + for prop_key, prop_value in self.schema.items(): + if prop_key == 'required' and prop_value: + return True + return False + + @property + def default(self): + if self.schema: + for prop_key, prop_value in self.schema.items(): + if prop_key == 'default': + return prop_value + return None diff --git a/IM/tosca/toscaparser/elements/relationshiptype.py b/IM/tosca/toscaparser/elements/relationshiptype.py new file mode 100644 index 000000000..e45ee3d93 --- /dev/null +++ b/IM/tosca/toscaparser/elements/relationshiptype.py @@ -0,0 +1,33 @@ +# 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. + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class RelationshipType(StatefulEntityType): + '''TOSCA built-in relationship type.''' + def __init__(self, type, capability_name=None, custom_def=None): + super(RelationshipType, self).__init__(type, self.RELATIONSHIP_PREFIX, + custom_def) + self.capability_name = capability_name + self.custom_def = custom_def + + @property + def parent_type(self): + '''Return a relationship this reletionship is derived from.''' + prel = self.derived_from(self.defs) + if prel: + return RelationshipType(prel) + + @property + def valid_target_types(self): + return self.entity_value(self.defs, 'valid_target_types') diff --git a/IM/tosca/toscaparser/elements/scalarunit.py b/IM/tosca/toscaparser/elements/scalarunit.py new file mode 100644 index 000000000..836427085 --- /dev/null +++ b/IM/tosca/toscaparser/elements/scalarunit.py @@ -0,0 +1,130 @@ +# 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. + +import logging +import re + +from IM.tosca.toscaparser.utils.gettextutils import _ +from IM.tosca.toscaparser.utils import validateutils + +log = logging.getLogger('tosca') + + +class ScalarUnit(object): + '''Parent class for scalar-unit type.''' + + SCALAR_UNIT_TYPES = ( + SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME + ) = ( + 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time' + ) + + def __init__(self, value): + self.value = value + + def _check_unit_in_scalar_standard_units(self, input_unit): + """Check whether the input unit is following specified standard + + If unit is not following specified standard, convert it to standard + unit after displaying a warning message. + """ + if input_unit in self.SCALAR_UNIT_DICT.keys(): + return input_unit + else: + for key in self.SCALAR_UNIT_DICT.keys(): + if key.upper() == input_unit.upper(): + log.warning(_('Given unit %(unit)s does not follow scalar ' + 'unit standards; using %(key)s instead.') % { + 'unit': input_unit, 'key': key}) + return key + msg = (_('Provided unit "%(unit)s" is not valid. The valid units' + ' are %(valid_units)s') % {'unit': input_unit, + 'valid_units': sorted(self.SCALAR_UNIT_DICT.keys())}) + raise ValueError(msg) + + def validate_scalar_unit(self): + # Miguel: Cambios aqui + if self.value is None: + return None + regex = re.compile('([0-9.]+)\s*(\w+)') + try: + result = regex.match(str(self.value)).groups() + validateutils.str_to_num(result[0]) + scalar_unit = self._check_unit_in_scalar_standard_units(result[1]) + self.value = ' '.join([result[0], scalar_unit]) + return self.value + + except Exception: + raise ValueError(_('"%s" is not a valid scalar-unit') + % self.value) + + def get_num_from_scalar_unit(self, unit=None): + #Miguel: Cambios aqui + if self.value is None: + return None + if unit: + unit = self._check_unit_in_scalar_standard_units(unit) + else: + unit = self.SCALAR_UNIT_DEFAULT + self.validate_scalar_unit() + + regex = re.compile('([0-9.]+)\s*(\w+)') + result = regex.match(str(self.value)).groups() + converted = (float(validateutils.str_to_num(result[0])) + * self.SCALAR_UNIT_DICT[result[1]] + / self.SCALAR_UNIT_DICT[unit]) + if converted - int(converted) < 0.0000000000001: + converted = int(converted) + return converted + + +class ScalarUnit_Size(ScalarUnit): + + SCALAR_UNIT_DEFAULT = 'B' + SCALAR_UNIT_DICT = {'B': 1, 'kB': 1000, 'KiB': 1024, 'MB': 1000000, + 'MiB': 1048576, 'GB': 1000000000, + 'GiB': 1073741824, 'TB': 1000000000000, + 'TiB': 1099511627776} + + +class ScalarUnit_Time(ScalarUnit): + + SCALAR_UNIT_DEFAULT = 'ms' + SCALAR_UNIT_DICT = {'d': 86400, 'h': 3600, 'm': 60, 's': 1, + 'ms': 0.001, 'us': 0.000001, 'ns': 0.000000001} + + +class ScalarUnit_Frequency(ScalarUnit): + + SCALAR_UNIT_DEFAULT = 'GHz' + SCALAR_UNIT_DICT = {'Hz': 1, 'kHz': 1000, + 'MHz': 1000000, 'GHz': 1000000000} + + +scalarunit_mapping = { + ScalarUnit.SCALAR_UNIT_FREQUENCY: ScalarUnit_Frequency, + ScalarUnit.SCALAR_UNIT_SIZE: ScalarUnit_Size, + ScalarUnit.SCALAR_UNIT_TIME: ScalarUnit_Time, + } + + +def get_scalarunit_class(type): + return scalarunit_mapping.get(type) + + +def get_scalarunit_value(type, value, unit=None): + if type in ScalarUnit.SCALAR_UNIT_TYPES: + ScalarUnit_Class = get_scalarunit_class(type) + return (ScalarUnit_Class(value). + get_num_from_scalar_unit(unit)) + else: + raise TypeError(_('"%s" is not a valid scalar-unit type') % type) diff --git a/IM/tosca/toscaparser/elements/statefulentitytype.py b/IM/tosca/toscaparser/elements/statefulentitytype.py new file mode 100644 index 000000000..af820bd69 --- /dev/null +++ b/IM/tosca/toscaparser/elements/statefulentitytype.py @@ -0,0 +1,81 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import InvalidTypeError +from IM.tosca.toscaparser.elements.attribute_definition import AttributeDef +from IM.tosca.toscaparser.elements.entity_type import EntityType +from IM.tosca.toscaparser.elements.property_definition import PropertyDef + + +class StatefulEntityType(EntityType): + '''Class representing TOSCA states.''' + + interfaces_node_lifecycle_operations = ['create', + 'configure', 'start', + 'stop', 'delete'] + + interfaces_relationship_confiure_operations = ['post_configure_source', + 'post_configure_target', + 'add_target', + 'remove_target'] + + def __init__(self, entitytype, prefix, custom_def=None): + entire_entitytype = entitytype + if not entitytype.startswith(self.TOSCA): + entire_entitytype = prefix + entitytype + if entire_entitytype in list(self.TOSCA_DEF.keys()): + self.defs = self.TOSCA_DEF[entire_entitytype] + entitytype = entire_entitytype + elif custom_def and entitytype in list(custom_def.keys()): + self.defs = custom_def[entitytype] + else: + raise InvalidTypeError(what=entitytype) + self.type = entitytype + + def get_properties_def_objects(self): + '''Return a list of property definition objects.''' + properties = [] + props = self.get_definition(self.PROPERTIES) + if props: + for prop, schema in props.items(): + properties.append(PropertyDef(prop, None, schema)) + return properties + + def get_properties_def(self): + '''Return a dictionary of property definition name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_def_objects()} + + def get_property_def_value(self, name): + '''Return the property definition associated with a given name.''' + props_def = self.get_properties_def() + if props_def and name in props_def.keys(): + return props_def[name].value + + def get_attributes_def_objects(self): + '''Return a list of attribute definition objects.''' + attrs = self.get_value(self.ATTRIBUTES) + if attrs: + return [AttributeDef(attr, None, schema) + for attr, schema in attrs.items()] + return [] + + def get_attributes_def(self): + '''Return a dictionary of attribute definition name-object pairs.''' + return {attr.name: attr + for attr in self.get_attributes_def_objects()} + + def get_attribute_def_value(self, name): + '''Return the attribute definition associated with a given name.''' + attrs_def = self.get_attributes_def() + if attrs_def and name in attrs_def.keys(): + return attrs_def[name].value diff --git a/IM/tosca/toscaparser/entity_template.py b/IM/tosca/toscaparser/entity_template.py new file mode 100644 index 000000000..f8f3ced25 --- /dev/null +++ b/IM/tosca/toscaparser/entity_template.py @@ -0,0 +1,285 @@ +# 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. + +from IM.tosca.toscaparser.capabilities import Capability +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.common.exception import ValidationError +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.elements.nodetype import NodeType +from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType +from IM.tosca.toscaparser.properties import Property + + +class EntityTemplate(object): + '''Base class for TOSCA templates.''' + + SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE, DESCRIPTION, DIRECTIVES, + ATTRIBUTES, ARTIFACTS, NODE_FILTER, COPY) = \ + ('derived_from', 'properties', 'requirements', 'interfaces', + 'capabilities', 'type', 'description', 'directives', + 'attributes', 'artifacts', 'node_filter', 'copy') + # Miguel: Add NODE_FILTER to the REQUIREMENTS_SECTION + REQUIREMENTS_SECTION = (NODE, CAPABILITY, RELATIONSHIP, OCCURRENCES, NODE_FILTER) = \ + ('node', 'capability', 'relationship', + 'occurrences','node_filter') + + def __init__(self, name, template, entity_name, custom_def=None): + self.name = name + self.entity_tpl = template + self.custom_def = custom_def + self._validate_field(self.entity_tpl) + if entity_name == 'node_type': + self.type_definition = NodeType(self.entity_tpl['type'], + custom_def) + if entity_name == 'relationship_type': + relationship = template.get('relationship') + type = None + if relationship and isinstance(relationship, dict): + type = relationship.get('type') + elif isinstance(relationship, str): + type = self.entity_tpl['relationship'] + else: + type = self.entity_tpl['type'] + self.type_definition = RelationshipType(type, + None, custom_def) + self._properties = None + self._interfaces = None + self._requirements = None + self._capabilities = None + + @property + def type(self): + return self.type_definition.type + + @property + def requirements(self): + if self._requirements is None: + self._requirements = self.type_definition.get_value( + self.REQUIREMENTS, + self.entity_tpl) or [] + return self._requirements + + def get_properties_objects(self): + '''Return properties objects for this template.''' + if self._properties is None: + self._properties = self._create_properties() + return self._properties + + def get_properties(self): + '''Return a dictionary of property name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_objects()} + + def get_property_value(self, name): + '''Return the value of a given property name.''' + props = self.get_properties() + if props and name in props.keys(): + return props[name].value + + @property + def interfaces(self): + #if self._interfaces is None: + if not self._interfaces: + self._interfaces = self._create_interfaces() + return self._interfaces + + def get_capabilities_objects(self): + '''Return capabilities objects for this template.''' + if not self._capabilities: + self._capabilities = self._create_capabilities() + return self._capabilities + + def get_capabilities(self): + '''Return a dictionary of capability name-object pairs.''' + return {cap.name: cap + for cap in self.get_capabilities_objects()} + + def is_derived_from(self, type_str): + '''Check if object inherits from the given type. + + Returns true if this object is derived from 'type_str'. + False otherwise. + ''' + if not self.type: + return False + elif self.type == type_str: + return True + elif self.parent_type: + return self.parent_type.is_derived_from(type_str) + else: + return False + + def _create_capabilities(self): + capability = [] + # Miguel: cambios aqui + caps = self.type_definition.get_value(self.CAPABILITIES, + self.entity_tpl, + self.type_definition) + if caps: + for name, props in caps.items(): + capabilities = self.type_definition.get_capabilities() + if name in capabilities.keys(): + c = capabilities[name] + if 'properties' in props: + cap = Capability(name, props['properties'], c) + else: + cap = Capability(name, [], c) + capability.append(cap) + return capability + + def _validate_properties(self, template, entitytype): + properties = entitytype.get_value(self.PROPERTIES, template) + self._common_validate_properties(entitytype, properties) + + def _validate_capabilities(self): + type_capabilities = self.type_definition.get_capabilities() + allowed_caps = \ + type_capabilities.keys() if type_capabilities else [] + capabilities = self.type_definition.get_value(self.CAPABILITIES, + self.entity_tpl) + if capabilities: + self._common_validate_field(capabilities, allowed_caps, + 'Capabilities') + self._validate_capabilities_properties(capabilities) + + def _validate_capabilities_properties(self, capabilities): + for cap, props in capabilities.items(): + capabilitydef = self.get_capability(cap).definition + self._common_validate_properties(capabilitydef, + props[self.PROPERTIES]) + + # validating capability properties values + for prop in self.get_capability(cap).get_properties_objects(): + prop.validate() + + # TODO(srinivas_tadepalli): temporary work around to validate + # default_instances until standardized in specification + if cap == "scalable" and prop.name == "default_instances": + prop_dict = props[self.PROPERTIES] + min_instances = prop_dict.get("min_instances") + max_instances = prop_dict.get("max_instances") + default_instances = prop_dict.get("default_instances") + if not (min_instances <= default_instances + <= max_instances): + err_msg = ("Properties of template %s : " + "default_instances value is not" + " between min_instances and " + "max_instances" % self.name) + raise ValidationError(message=err_msg) + + def _common_validate_properties(self, entitytype, properties): + allowed_props = [] + required_props = [] + for p in entitytype.get_properties_def_objects(): + allowed_props.append(p.name) + if p.required: + required_props.append(p.name) + if properties: + self._common_validate_field(properties, allowed_props, + 'Properties') + # make sure it's not missing any property required by a tosca type + missingprop = [] + for r in required_props: + if r not in properties.keys(): + missingprop.append(r) + if missingprop: + raise MissingRequiredFieldError( + what='Properties of template %s' % self.name, + required=missingprop) + else: + if required_props: + raise MissingRequiredFieldError( + what='Properties of template %s' % self.name, + required=missingprop) + + def _validate_field(self, template): + if not isinstance(template, dict): + raise MissingRequiredFieldError( + what='Template %s' % self.name, required=self.TYPE) + try: + relationship = template.get('relationship') + if relationship and not isinstance(relationship, str): + relationship[self.TYPE] + elif isinstance(relationship, str): + template['relationship'] + else: + template[self.TYPE] + except KeyError: + raise MissingRequiredFieldError( + what='Template %s' % self.name, required=self.TYPE) + + def _common_validate_field(self, schema, allowedlist, section): + for name in schema: + if name not in allowedlist: + raise UnknownFieldError( + what='%(section)s of template %(nodename)s' + % {'section': section, 'nodename': self.name}, + field=name) + + def _create_properties(self): + props = [] + properties = self.type_definition.get_value(self.PROPERTIES, + self.entity_tpl) or {} + for name, value in properties.items(): + props_def = self.type_definition.get_properties_def() + if props_def and name in props_def: + prop = Property(name, value, + props_def[name].schema, self.custom_def) + props.append(prop) + for p in self.type_definition.get_properties_def_objects(): + if p.default is not None and p.name not in properties.keys(): + prop = Property(p.name, p.default, p.schema, self.custom_def) + props.append(prop) + return props + + def _create_interfaces(self): + interfaces = [] + type_interfaces = None + if isinstance(self.type_definition, RelationshipType): + if isinstance(self.entity_tpl, dict): + # Miguel: cambios aqui + for key, value in self.entity_tpl.items(): + if key == 'interfaces': + type_interfaces = value + elif key != 'type': + rel = None + if isinstance(value, dict): + rel = value.get('relationship') + if rel: + if self.INTERFACES in rel: + type_interfaces = rel[self.INTERFACES] + break + else: + type_interfaces = self.type_definition.get_value(self.INTERFACES, + self.entity_tpl) + if type_interfaces: + for interface_type, value in type_interfaces.items(): + for op, op_def in value.items(): + iface = InterfacesDef(self.type_definition, + interfacetype=interface_type, + node_template=self, + name=op, + value=op_def) + interfaces.append(iface) + return interfaces + + def get_capability(self, name): + """Provide named capability + + :param name: name of capability + :return: capability object if found, None otherwise + """ + caps = self.get_capabilities() + if caps and name in caps.keys(): + return caps[name] diff --git a/IM/tosca/toscaparser/functions.py b/IM/tosca/toscaparser/functions.py new file mode 100644 index 000000000..5ecb905c9 --- /dev/null +++ b/IM/tosca/toscaparser/functions.py @@ -0,0 +1,410 @@ +# +# 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. + + +import abc +import six + +from IM.tosca.toscaparser.common.exception import UnknownInputError +from IM.tosca.toscaparser.utils.gettextutils import _ + + +GET_PROPERTY = 'get_property' +GET_ATTRIBUTE = 'get_attribute' +GET_INPUT = 'get_input' + +SELF = 'SELF' +HOST = 'HOST' + +HOSTED_ON = 'tosca.relationships.HostedOn' + + +@six.add_metaclass(abc.ABCMeta) +class Function(object): + """An abstract type for representing a Tosca template function.""" + + def __init__(self, tosca_tpl, context, name, args): + self.tosca_tpl = tosca_tpl + self.context = context + self.name = name + self.args = args + self.validate() + + @abc.abstractmethod + def result(self): + """Invokes the function and returns its result + + Some methods invocation may only be relevant on runtime (for example, + getting runtime properties) and therefore its the responsibility of + the orchestrator/translator to take care of such functions invocation. + + :return: Function invocation result. + """ + return {self.name: self.args} + + @abc.abstractmethod + def validate(self): + """Validates function arguments.""" + pass + + +class GetInput(Function): + """Get a property value declared within the input of the service template. + + Arguments: + + * Input name. + + Example: + + * get_input: port + """ + + def validate(self): + if len(self.args) != 1: + raise ValueError(_( + 'Expected one argument for get_input function but received: ' + '{0}.').format(self.args)) + inputs = [input.name for input in self.tosca_tpl.inputs] + if self.args[0] not in inputs: + raise UnknownInputError(input_name=self.args[0]) + + def result(self): + found_input = [input_def for input_def in self.tosca_tpl.inputs + if self.input_name == input_def.name][0] + return found_input.default + + @property + def input_name(self): + return self.args[0] + + +class GetAttribute(Function): + """Get an attribute value of an entity defined in the service template + + Node template attributes values are set in runtime and therefore its the + responsibility of the Tosca engine to implement the evaluation of + get_attribute functions. + + Arguments: + + * Node template name | HOST. + * Attribute name. + + If the HOST keyword is passed as the node template name argument the + function will search each node template along the HostedOn relationship + chain until a node which contains the attribute is found. + + Examples: + + * { get_attribute: [ server, private_address ] } + * { get_attribute: [ HOST, private_address ] } + """ + + def validate(self): + # Miguel: this is not true: + # { get_attribute: [ HOST, networks, private, addresses, 0 ] } + if len(self.args) != 2: + raise ValueError(_( + 'Illegal arguments for {0} function. Expected arguments: ' + 'node-template-name, attribute-name').format(GET_ATTRIBUTE)) + self._find_node_template_containing_attribute() + + def result(self): + return self.args + + def get_referenced_node_template(self): + """Gets the NodeTemplate instance the get_attribute function refers to. + + If HOST keyword was used as the node template argument, the node + template which contains the attribute along the HostedOn relationship + chain will be returned. + """ + return self._find_node_template_containing_attribute() + + def _find_node_template_containing_attribute(self): + if self.node_template_name == HOST: + # Currently this is the only way to tell whether the function + # is used within the outputs section of the TOSCA template. + if isinstance(self.context, list): + raise ValueError(_( + "get_attribute HOST keyword is not allowed within the " + "outputs section of the TOSCA template")) + node_tpl = self._find_host_containing_attribute() + if not node_tpl: + raise ValueError(_( + "get_attribute HOST keyword is used in '{0}' node " + "template but {1} was not found " + "in relationship chain").format(self.context.name, + HOSTED_ON)) + else: + node_tpl = self._find_node_template(self.args[0]) + if not self._attribute_exists_in_type(node_tpl.type_definition): + raise KeyError(_( + "Attribute '{0}' not found in node template: {1}.").format( + self.attribute_name, node_tpl.name)) + return node_tpl + + def _attribute_exists_in_type(self, type_definition): + attrs_def = type_definition.get_attributes_def() + found = [attrs_def[self.attribute_name]] \ + if self.attribute_name in attrs_def else [] + return len(found) == 1 + + def _find_host_containing_attribute(self, node_template_name=SELF): + node_template = self._find_node_template(node_template_name) + from IM.tosca.toscaparser.elements.entity_type import EntityType + hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] + for r in node_template.requirements: + for requirement, target_name in r.items(): + target_node = self._find_node_template(target_name) + target_type = target_node.type_definition + for capability in target_type.get_capabilities_objects(): + if capability.type in hosted_on_rel['valid_target_types']: + if self._attribute_exists_in_type(target_type): + return target_node + return self._find_host_containing_attribute( + target_name) + return None + + def _find_node_template(self, node_template_name): + name = self.context.name if node_template_name == SELF else \ + node_template_name + for node_template in self.tosca_tpl.nodetemplates: + if node_template.name == name: + return node_template + raise KeyError(_( + 'No such node template: {0}.').format(node_template_name)) + + @property + def node_template_name(self): + return self.args[0] + + @property + def attribute_name(self): + return self.args[1] + + +class GetProperty(Function): + """Get a property value of an entity defined in the same service template. + + Arguments: + + * Node template name. + * Requirement or capability name (optional). + * Property name. + + If requirement or capability name is specified, the behavior is as follows: + The req or cap name is first looked up in the specified node template's + requirements. + If found, it would search for a matching capability + of an other node template and get its property as specified in function + arguments. + Otherwise, the req or cap name would be looked up in the specified + node template's capabilities and if found, it would return the property of + the capability as specified in function arguments. + + Examples: + + * { get_property: [ mysql_server, port ] } + * { get_property: [ SELF, db_port ] } + * { get_property: [ SELF, database_endpoint, port ] } + """ + + def validate(self): + if len(self.args) < 2 or len(self.args) > 3: + raise ValueError(_( + 'Expected arguments: [node-template-name, req-or-cap ' + '(optional), property name.')) + if len(self.args) == 2: + prop = self._find_property(self.args[1]).value + if not isinstance(prop, Function): + get_function(self.tosca_tpl, self.context, prop) + elif len(self.args) == 3: + get_function(self.tosca_tpl, + self.context, + self._find_req_or_cap_property(self.args[1], + self.args[2])) + else: + raise NotImplementedError(_( + 'Nested properties are not supported.')) + + def _find_req_or_cap_property(self, req_or_cap, property_name): + node_tpl = self._find_node_template(self.args[0]) + # Find property in node template's requirements + for r in node_tpl.requirements: + for req, node_name in r.items(): + if req == req_or_cap: + node_template = self._find_node_template(node_name) + return self._get_capability_property( + node_template, + req, + property_name) + # If requirement was not found, look in node template's capabilities + return self._get_capability_property(node_tpl, + req_or_cap, + property_name) + + def _get_capability_property(self, + node_template, + capability_name, + property_name): + """Gets a node template capability property.""" + caps = node_template.get_capabilities() + if caps and capability_name in caps.keys(): + cap = caps[capability_name] + # Miguel: Cambios aqui + property = None + props = cap.get_properties() + if props and property_name in props.keys(): + property = props[property_name] + if not property: + raise KeyError(_( + "Property '{0}' not found in capability '{1}' of node" + " template '{2}' referenced from node template" + " '{3}'.").format(property_name, + capability_name, + node_template.name, + self.context.name)) + if property.value: + return property.value + else: + return property.default + msg = _("Requirement/Capability '{0}' referenced from '{1}' node " + "template not found in '{2}' node template.").format( + capability_name, + self.context.name, + node_template.name) + raise KeyError(msg) + + def _find_property(self, property_name): + node_tpl = self._find_node_template(self.args[0]) + props = node_tpl.get_properties() + found = [props[property_name]] if property_name in props else [] + if len(found) == 0: + raise KeyError(_( + "Property: '{0}' not found in node template: {1}.").format( + property_name, node_tpl.name)) + return found[0] + + def _find_node_template(self, node_template_name): + if node_template_name == SELF: + return self.context + # Miguel: cambios aqui + elif node_template_name == HOST: + return self._find_host_containing_property() + for node_template in self.tosca_tpl.nodetemplates: + if node_template.name == node_template_name: + return node_template + raise KeyError(_( + 'No such node template: {0}.').format(node_template_name)) + + # Miguel: anyado esto + def _find_host_containing_property(self, node_template_name=SELF): + node_template = self._find_node_template(node_template_name) + from IM.tosca.toscaparser.elements.entity_type import EntityType + hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] + for r in node_template.requirements: + for requirement, target_name in r.items(): + target_node = self._find_node_template(target_name) + target_type = target_node.type_definition + for capability in target_type.get_capabilities_objects(): + if capability.type in hosted_on_rel['valid_target_types']: + if self._property_exists_in_type(target_type): + return target_node + return self._find_host_containing_attribute( + target_name) + return None + + def _property_exists_in_type(self, type_definition): + props_def = type_definition.get_properties_def() + found = [props_def[self.args[1]]] \ + if self.args[1] in props_def else [] + return len(found) == 1 + + def result(self): + if len(self.args) == 3: + property_value = self._find_req_or_cap_property(self.args[1], + self.args[2]) + else: + property_value = self._find_property(self.args[1]).value + if isinstance(property_value, Function): + return property_value + return get_function(self.tosca_tpl, + self.context, + property_value) + + @property + def node_template_name(self): + return self.args[0] + + @property + def property_name(self): + if len(self.args) > 2: + return self.args[2] + return self.args[1] + + @property + def req_or_cap(self): + if len(self.args) > 2: + return self.args[1] + return None + + +function_mappings = { + GET_PROPERTY: GetProperty, + GET_INPUT: GetInput, + GET_ATTRIBUTE: GetAttribute +} + + +def is_function(function): + """Returns True if the provided function is a Tosca intrinsic function. + + Examples: + + * "{ get_property: { SELF, port } }" + * "{ get_input: db_name }" + * Function instance + + :param function: Function as string or a Function instance. + :return: True if function is a Tosca intrinsic function, otherwise False. + """ + if isinstance(function, dict) and len(function) == 1: + func_name = list(function.keys())[0] + return func_name in function_mappings + return isinstance(function, Function) + + +def get_function(tosca_tpl, node_template, raw_function): + """Gets a Function instance representing the provided template function. + + If the format provided raw_function format is not relevant for template + functions or if the function name doesn't exist in function mapping the + method returns the provided raw_function. + + :param tosca_tpl: The tosca template. + :param node_template: The node template the function is specified for. + :param raw_function: The raw function as dict. + :return: Template function as Function instance or the raw_function if + parsing was unsuccessful. + """ + if is_function(raw_function): + func_name = list(raw_function.keys())[0] + if func_name in function_mappings: + func = function_mappings[func_name] + func_args = list(raw_function.values())[0] + if not isinstance(func_args, list): + func_args = [func_args] + return func(tosca_tpl, node_template, func_name, func_args) + return raw_function diff --git a/IM/tosca/toscaparser/groups.py b/IM/tosca/toscaparser/groups.py new file mode 100644 index 000000000..40ebcf548 --- /dev/null +++ b/IM/tosca/toscaparser/groups.py @@ -0,0 +1,27 @@ +# 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. + + +class NodeGroup(object): + + def __init__(self, name, group_templates, member_nodes): + self.name = name + self.tpl = group_templates + self.members = member_nodes + + @property + def member_names(self): + return self.tpl.get('members') + + @property + def policies(self): + return self.tpl.get('policies') diff --git a/IM/tosca/toscaparser/nodetemplate.py b/IM/tosca/toscaparser/nodetemplate.py new file mode 100644 index 000000000..9288571b1 --- /dev/null +++ b/IM/tosca/toscaparser/nodetemplate.py @@ -0,0 +1,242 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.common.exception import InvalidPropertyValueError +from IM.tosca.toscaparser.common.exception import TypeMismatchError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.dataentity import DataEntity +from IM.tosca.toscaparser.elements.interfaces import CONFIGURE +from IM.tosca.toscaparser.elements.interfaces import CONFIGURE_SHORTNAME +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE +from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE_SHORTNAME +from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType +from IM.tosca.toscaparser.entity_template import EntityTemplate +from IM.tosca.toscaparser.relationship_template import RelationshipTemplate +from IM.tosca.toscaparser.utils.gettextutils import _ + +log = logging.getLogger('tosca') + + +class NodeTemplate(EntityTemplate): + '''Node template from a Tosca profile.''' + def __init__(self, name, node_templates, custom_def=None, + available_rel_tpls=None, available_rel_types=None): + super(NodeTemplate, self).__init__(name, node_templates[name], + 'node_type', + custom_def) + self.templates = node_templates + self._validate_fields(node_templates[name]) + self.custom_def = custom_def + self.related = {} + self.relationship_tpl = [] + self.available_rel_tpls = available_rel_tpls + self.available_rel_types = available_rel_types + self._relationships = {} + + @property + def relationships(self): + if not self._relationships: + requires = self.requirements + if requires: + for r in requires: + for _, value in r.items(): + explicit = self._get_explicit_relationship(r, value) + if explicit: + for key, value in explicit.items(): + self._relationships[key] = value + return self._relationships + + def _get_explicit_relationship(self, req, value): + """Handle explicit relationship + + For example, + - req: + node: DBMS + relationship: tosca.relationships.HostedOn + """ + explicit_relation = {} + node = value.get('node') if isinstance(value, dict) else value + + if node: + # TODO(spzala) implement look up once Glance meta data is available + # to find a matching TOSCA node using the TOSCA types + msg = _('Lookup by TOSCA types are not supported. ' + 'Requirement for %s can not be full-filled.') % self.name + if (node in list(self.type_definition.TOSCA_DEF.keys()) + or node in self.custom_def): + raise NotImplementedError(msg) + related_tpl = NodeTemplate(node, self.templates, self.custom_def) + relationship = value.get('relationship') \ + if isinstance(value, dict) else None + # check if it's type has relationship defined + if not relationship: + parent_reqs = self.type_definition.get_all_requirements() + for key in req.keys(): + for req_dict in parent_reqs: + if key in req_dict.keys(): + relationship = (req_dict.get(key). + get('relationship')) + break + if relationship: + found_relationship_tpl = False + # apply available relationship templates if found + # Miguel: add this if + if self.available_rel_tpls: + for tpl in self.available_rel_tpls: + if tpl.name == relationship: + rtype = RelationshipType(tpl.type, None, + self.custom_def) + explicit_relation[rtype] = related_tpl + self.relationship_tpl.append(tpl) + found_relationship_tpl = True + + # create relationship template object. + rel_prfx = self.type_definition.RELATIONSHIP_PREFIX + if not found_relationship_tpl: + if isinstance(relationship, dict): + relationship = relationship.get('type') + if self.available_rel_types and \ + relationship in self.available_rel_types.keys(): + pass + elif not relationship.startswith(rel_prfx): + relationship = rel_prfx + relationship + for rtype in self.type_definition.relationship.keys(): + if rtype.type == relationship: + explicit_relation[rtype] = related_tpl + related_tpl._add_relationship_template(req, + rtype.type) + elif self.available_rel_types: + if relationship in self.available_rel_types.keys(): + rel_type_def = self.available_rel_types.\ + get(relationship) + if 'derived_from' in rel_type_def: + super_type = \ + rel_type_def.get('derived_from') + if not super_type.startswith(rel_prfx): + super_type = rel_prfx + super_type + if rtype.type == super_type: + explicit_relation[rtype] = related_tpl + related_tpl.\ + _add_relationship_template( + req, rtype.type) + return explicit_relation + + def _add_relationship_template(self, requirement, rtype): + req = requirement.copy() + req['type'] = rtype + tpl = RelationshipTemplate(req, rtype, None) + self.relationship_tpl.append(tpl) + + def get_relationship_template(self): + return self.relationship_tpl + + def _add_next(self, nodetpl, relationship): + self.related[nodetpl] = relationship + + @property + def related_nodes(self): + if not self.related: + for relation, node in self.type_definition.relationship.items(): + for tpl in self.templates: + if tpl == node.type: + self.related[NodeTemplate(tpl)] = relation + return self.related.keys() + + def validate(self, tosca_tpl=None): + self._validate_capabilities() + self._validate_requirements() + self._validate_properties(self.entity_tpl, self.type_definition) + self._validate_interfaces() + for prop in self.get_properties_objects(): + prop.validate() + + def _validate_requirements(self): + type_requires = self.type_definition.get_all_requirements() + allowed_reqs = ["template"] + if type_requires: + for treq in type_requires: + for key, value in treq.items(): + allowed_reqs.append(key) + if isinstance(value, dict): + for key in value: + allowed_reqs.append(key) + + requires = self.type_definition.get_value(self.REQUIREMENTS, + self.entity_tpl) + if requires: + if not isinstance(requires, list): + raise TypeMismatchError( + what='Requirements of template %s' % self.name, + type='list') + for req in requires: + for r1, value in req.items(): + if isinstance(value, dict): + self._validate_requirements_keys(value) + self._validate_requirements_properties(value) + allowed_reqs.append(r1) + self._common_validate_field(req, allowed_reqs, 'Requirements') + + def _validate_requirements_properties(self, requirements): + # TODO(anyone): Only occurences property of the requirements is + # validated here. Validation of other requirement properties are being + # validated in different files. Better to keep all the requirements + # properties validation here. + for key, value in requirements.items(): + if key == 'occurrences': + self._validate_occurrences(value) + break + + def _validate_occurrences(self, occurrences): + DataEntity.validate_datatype('list', occurrences) + for value in occurrences: + DataEntity.validate_datatype('integer', value) + if len(occurrences) != 2 or not (0 <= occurrences[0] <= occurrences[1]) \ + or occurrences[1] == 0: + raise InvalidPropertyValueError(what=(occurrences)) + + def _validate_requirements_keys(self, requirement): + for key in requirement.keys(): + if key not in self.REQUIREMENTS_SECTION: + raise UnknownFieldError( + what='Requirements of template %s' % self.name, + field=key) + + def _validate_interfaces(self): + ifaces = self.type_definition.get_value(self.INTERFACES, + self.entity_tpl) + if ifaces: + for i in ifaces: + for name, value in ifaces.items(): + if name in (LIFECYCLE, LIFECYCLE_SHORTNAME): + self._common_validate_field( + value, InterfacesDef. + interfaces_node_lifecycle_operations, + 'Interfaces') + elif name in (CONFIGURE, CONFIGURE_SHORTNAME): + self._common_validate_field( + value, InterfacesDef. + interfaces_relationship_confiure_operations, + 'Interfaces') + else: + raise UnknownFieldError( + what='Interfaces of template %s' % self.name, + field=name) + + def _validate_fields(self, nodetemplate): + for name in nodetemplate.keys(): + if name not in self.SECTIONS: + raise UnknownFieldError(what='Node template %s' + % self.name, field=name) diff --git a/IM/tosca/toscaparser/parameters.py b/IM/tosca/toscaparser/parameters.py new file mode 100644 index 000000000..a8a3f76e4 --- /dev/null +++ b/IM/tosca/toscaparser/parameters.py @@ -0,0 +1,110 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.dataentity import DataEntity +from IM.tosca.toscaparser.elements.constraints import Schema +from IM.tosca.toscaparser.elements.entity_type import EntityType +from IM.tosca.toscaparser.utils.gettextutils import _ + + +log = logging.getLogger('tosca') + + +class Input(object): + + INPUTFIELD = (TYPE, DESCRIPTION, DEFAULT, CONSTRAINTS) = \ + ('type', 'description', 'default', 'constraints') + + def __init__(self, name, schema_dict): + self.name = name + self.schema = Schema(name, schema_dict) + + @property + def type(self): + return self.schema.type + + @property + def description(self): + return self.schema.description + + @property + def default(self): + return self.schema.default + + @property + def constraints(self): + return self.schema.constraints + + def validate(self, value=None): + self._validate_field() + self.validate_type(self.type) + if value: + self._validate_value(value) + + def _validate_field(self): + for name in self.schema: + if name not in self.INPUTFIELD: + raise UnknownFieldError(what='Input %s' % self.name, + field=name) + + def validate_type(self, input_type): + if input_type not in Schema.PROPERTY_TYPES: + raise ValueError(_('Invalid type %s') % type) + + def _validate_value(self, value): + tosca = EntityType.TOSCA_DEF + datatype = None + if self.type in tosca: + datatype = tosca[self.type] + elif EntityType.DATATYPE_PREFIX + self.type in tosca: + datatype = tosca[EntityType.DATATYPE_PREFIX + self.type] + + DataEntity.validate_datatype(self.type, value, None, datatype) + + +class Output(object): + + OUTPUTFIELD = (DESCRIPTION, VALUE) = ('description', 'value') + + def __init__(self, name, attrs): + self.name = name + self.attrs = attrs + + @property + def description(self): + return self.attrs[self.DESCRIPTION] + + @property + def value(self): + return self.attrs[self.VALUE] + + def validate(self): + self._validate_field() + + def _validate_field(self): + if not isinstance(self.attrs, dict): + raise MissingRequiredFieldError(what='Output %s' % self.name, + required=self.VALUE) + try: + self.value + except KeyError: + raise MissingRequiredFieldError(what='Output %s' % self.name, + required=self.VALUE) + for name in self.attrs: + if name not in self.OUTPUTFIELD: + raise UnknownFieldError(what='Output %s' % self.name, + field=name) diff --git a/IM/tosca/toscaparser/prereq/__init__.py b/IM/tosca/toscaparser/prereq/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/prereq/csar.py b/IM/tosca/toscaparser/prereq/csar.py new file mode 100644 index 000000000..9f17b902c --- /dev/null +++ b/IM/tosca/toscaparser/prereq/csar.py @@ -0,0 +1,122 @@ +# 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. + +import os.path +import yaml +import zipfile + +from IM.tosca.toscaparser.common.exception import ValidationError +from IM.tosca.toscaparser.utils.gettextutils import _ + + +class CSAR(object): + + def __init__(self, csar_file): + self.csar_file = csar_file + self.is_validated = False + + def validate(self): + """Validate the provided CSAR file.""" + + self.is_validated = True + + # validate that the file exists + if not os.path.isfile(self.csar_file): + err_msg = (_('The file %s does not exist.') % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that it is a valid zip file + if not zipfile.is_zipfile(self.csar_file): + err_msg = (_('The file %s is not a valid zip file.') + % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that it contains the metadata file in the correct location + self.zfile = zipfile.ZipFile(self.csar_file, 'r') + filelist = self.zfile.namelist() + if 'TOSCA-Metadata/TOSCA.meta' not in filelist: + err_msg = (_('The file %s is not a valid CSAR as it does not ' + 'contain the required file "TOSCA.meta" in the ' + 'folder "TOSCA-Metadata".') % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that 'Entry-Definitions' property exists in TOSCA.meta + data = self.zfile.read('TOSCA-Metadata/TOSCA.meta') + invalid_yaml_err_msg = (_('The file "TOSCA-Metadata/TOSCA.meta" in %s ' + 'does not contain valid YAML content.') % + self.csar_file) + try: + meta = yaml.load(data) + if type(meta) is not dict: + raise ValidationError(message=invalid_yaml_err_msg) + self.metadata = meta + except yaml.YAMLError: + raise ValidationError(message=invalid_yaml_err_msg) + + if 'Entry-Definitions' not in self.metadata: + err_msg = (_('The CSAR file "%s" is missing the required metadata ' + '"Entry-Definitions" in "TOSCA-Metadata/TOSCA.meta".') + % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that 'Entry-Definitions' metadata value points to an + # existing file in the CSAR + entry = self.metadata['Entry-Definitions'] + if entry not in filelist: + err_msg = (_('The "Entry-Definitions" file defined in the CSAR ' + '"%s" does not exist.') % self.csar_file) + raise ValidationError(message=err_msg) + + def get_metadata(self): + """Return the metadata dictionary.""" + + # validate the csar if not already validated + if not self.is_validated: + self.validate() + + # return a copy to avoid changes overwrite the original + return dict(self.metadata) if self.metadata else None + + def _get_metadata(self, key): + if not self.is_validated: + self.validate() + return self.metadata[key] if key in self.metadata else None + + def get_author(self): + return self._get_metadata('Created-By') + + def get_version(self): + return self._get_metadata('CSAR-Version') + + def get_main_template(self): + return self._get_metadata('Entry-Definitions') + + def get_description(self): + desc = self._get_metadata('Description') + if desc is not None: + return desc + + main_template = self.get_main_template() + # extract the description from the main template + data = self.zfile.read(main_template) + invalid_tosca_yaml_err_msg = ( + _('The file %(template)s in %(csar)s does not contain valid TOSCA ' + 'YAML content.') % {'template': main_template, + 'csar': self.csar_file}) + try: + tosca_yaml = yaml.load(data) + if type(tosca_yaml) is not dict: + raise ValidationError(message=invalid_tosca_yaml_err_msg) + self.metadata['Description'] = tosca_yaml['description'] + except Exception: + raise ValidationError(message=invalid_tosca_yaml_err_msg) + return self.metadata['Description'] diff --git a/IM/tosca/toscaparser/properties.py b/IM/tosca/toscaparser/properties.py new file mode 100644 index 000000000..f35c4394a --- /dev/null +++ b/IM/tosca/toscaparser/properties.py @@ -0,0 +1,79 @@ +# 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. + +from IM.tosca.toscaparser.dataentity import DataEntity +from IM.tosca.toscaparser.elements.constraints import Schema +from IM.tosca.toscaparser.functions import is_function + + +class Property(object): + '''TOSCA built-in Property type.''' + + PROPERTY_KEYS = ( + TYPE, REQUIRED, DESCRIPTION, DEFAULT, CONSTRAINTS + ) = ( + 'type', 'required', 'description', 'default', 'constraints' + ) + + ENTRY_SCHEMA_KEYS = ( + ENTRYTYPE, ENTRYPROPERTIES + ) = ( + 'type', 'properties' + ) + + def __init__(self, property_name, value, schema_dict, custom_def=None): + self.name = property_name + self.value = value + self.custom_def = custom_def + self.schema = Schema(property_name, schema_dict) + + @property + def type(self): + return self.schema.type + + @property + def required(self): + return self.schema.required + + @property + def description(self): + return self.schema.description + + @property + def default(self): + return self.schema.default + + @property + def constraints(self): + return self.schema.constraints + + @property + def entry_schema(self): + return self.schema.entry_schema + + def validate(self): + '''Validate if not a reference property.''' + # Miguel: Cambios aqui + if not is_function(self.value): + if self.value is not None: + if self.type == Schema.STRING: + self.value = str(self.value) + self.value = DataEntity.validate_datatype(self.type, self.value, + self.entry_schema, + self.custom_def) + self._validate_constraints() + + def _validate_constraints(self): + # Miguel: Cambios aqui + if self.value and self.constraints: + for constraint in self.constraints: + constraint.validate(self.value) diff --git a/IM/tosca/toscaparser/relationship_template.py b/IM/tosca/toscaparser/relationship_template.py new file mode 100644 index 000000000..a213595ca --- /dev/null +++ b/IM/tosca/toscaparser/relationship_template.py @@ -0,0 +1,68 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.entity_template import EntityTemplate +from IM.tosca.toscaparser.properties import Property + +SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE) = \ + ('derived_from', 'properties', 'requirements', 'interfaces', + 'capabilities', 'type') + +log = logging.getLogger('tosca') + + +class RelationshipTemplate(EntityTemplate): + '''Relationship template.''' + def __init__(self, relationship_template, name, custom_def=None): + super(RelationshipTemplate, self).__init__(name, + relationship_template, + 'relationship_type', + custom_def) + self.name = name.lower() + + def get_properties_objects(self): + '''Return properties objects for this template.''' + if self._properties is None: + self._properties = self._create_relationship_properties() + return self._properties + + def _create_relationship_properties(self): + props = [] + properties = {} + relationship = self.entity_tpl.get('relationship') + if relationship: + properties = self.type_definition.get_value(self.PROPERTIES, + relationship) or {} + if not properties: + properties = self.entity_tpl.get(self.PROPERTIES) or {} + + if properties: + for name, value in properties.items(): + props_def = self.type_definition.get_properties_def() + if props_def and name in props_def: + if name in properties.keys(): + value = properties.get(name) + prop = Property(name, value, + props_def[name].schema, self.custom_def) + props.append(prop) + for p in self.type_definition.get_properties_def_objects(): + if p.default is not None and p.name not in properties.keys(): + prop = Property(p.name, p.default, p.schema, self.custom_def) + props.append(prop) + return props + + def validate(self): + self._validate_properties(self.entity_tpl, self.type_definition) diff --git a/IM/tosca/toscaparser/topology_template.py b/IM/tosca/toscaparser/topology_template.py new file mode 100644 index 000000000..6822189fa --- /dev/null +++ b/IM/tosca/toscaparser/topology_template.py @@ -0,0 +1,213 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.common import exception +from IM.tosca.toscaparser import functions +from IM.tosca.toscaparser.groups import NodeGroup +from IM.tosca.toscaparser.nodetemplate import NodeTemplate +from IM.tosca.toscaparser.parameters import Input +from IM.tosca.toscaparser.parameters import Output +from IM.tosca.toscaparser.relationship_template import RelationshipTemplate +from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph + + +# Topology template key names +SECTIONS = (DESCRIPTION, INPUTS, NODE_TEMPLATES, + RELATIONSHIP_TEMPLATES, OUTPUTS, GROUPS, + SUBSTITUION_MAPPINGS) = \ + ('description', 'inputs', 'node_templates', + 'relationship_templates', 'outputs', 'groups', + 'substitution_mappings') + +log = logging.getLogger("tosca.model") + + +class TopologyTemplate(object): + + '''Load the template data.''' + def __init__(self, template, custom_defs, + rel_types=None, parsed_params=None): + self.tpl = template + self.custom_defs = custom_defs + self.rel_types = rel_types + self.parsed_params = parsed_params + self._validate_field() + self.description = self._tpl_description() + self.inputs = self._inputs() + self.relationship_templates = self._relationship_templates() + self.nodetemplates = self._nodetemplates() + self.outputs = self._outputs() + self.graph = ToscaGraph(self.nodetemplates) + self.groups = self._groups() + self._process_intrinsic_functions() + + def _inputs(self): + inputs = [] + for name, attrs in self._tpl_inputs().items(): + input = Input(name, attrs) + if self.parsed_params and name in self.parsed_params: + input.validate(self.parsed_params[name]) + inputs.append(input) + return inputs + + def _nodetemplates(self): + nodetemplates = [] + tpls = self._tpl_nodetemplates() + for name in tpls: + tpl = NodeTemplate(name, tpls, self.custom_defs, + self.relationship_templates, + self.rel_types) + tpl.validate(self) + nodetemplates.append(tpl) + return nodetemplates + + def _relationship_templates(self): + rel_templates = [] + tpls = self._tpl_relationship_templates() + for name in tpls: + tpl = RelationshipTemplate(tpls[name], name, self.custom_defs) + rel_templates.append(tpl) + return rel_templates + + def _outputs(self): + outputs = [] + for name, attrs in self._tpl_outputs().items(): + output = Output(name, attrs) + output.validate() + outputs.append(output) + return outputs + + def _substitution_mappings(self): + pass + + def _groups(self): + groups = [] + for group_name, group_tpl in self._tpl_groups().items(): + member_names = group_tpl.get('members') + if member_names and len(member_names) > 1: + group = NodeGroup(group_name, group_tpl, + self._get_group_memerbs(member_names)) + groups.append(group) + else: + raise ValueError + return groups + + def _get_group_memerbs(self, member_names): + member_nodes = [] + for member in member_names: + for node in self.nodetemplates: + if node.name == member: + member_nodes.append(node) + return member_nodes + + # topology template can act like node template + # it is exposed by substitution_mappings. + def nodetype(self): + pass + + def capabilities(self): + pass + + def requirements(self): + pass + + def _tpl_description(self): + description = self.tpl.get(DESCRIPTION) + if description: + description = description.rstrip() + return description + + def _tpl_inputs(self): + return self.tpl.get(INPUTS) or {} + + def _tpl_nodetemplates(self): + return self.tpl[NODE_TEMPLATES] + + def _tpl_relationship_templates(self): + return self.tpl.get(RELATIONSHIP_TEMPLATES) or {} + + def _tpl_outputs(self): + return self.tpl.get(OUTPUTS) or {} + + def _tpl_substitution_mappings(self): + return self.tpl.get(SUBSTITUION_MAPPINGS) or {} + + def _tpl_groups(self): + return self.tpl.get(GROUPS) or {} + + def _validate_field(self): + for name in self.tpl: + if name not in SECTIONS: + raise exception.UnknownFieldError(what='Template', field=name) + + def _process_intrinsic_functions(self): + """Process intrinsic functions + + Current implementation processes functions within node template + properties, requirements, interfaces inputs and template outputs. + """ + for node_template in self.nodetemplates: + for prop in node_template.get_properties_objects(): + prop.value = functions.get_function(self, + node_template, + prop.value) + for interface in node_template.interfaces: + if interface.inputs: + for name, value in interface.inputs.items(): + interface.inputs[name] = functions.get_function( + self, + node_template, + value) + if node_template.requirements: + for req in node_template.requirements: + rel = req + for req_name, req_item in req.items(): + if isinstance(req_item, dict): + rel = req_item.get('relationship') + break + if rel and 'properties' in rel: + for key, value in rel['properties'].items(): + rel['properties'][key] = functions.get_function( + self, + req, + value) + if node_template.get_capabilities_objects(): + for cap in node_template.get_capabilities_objects(): + if cap.get_properties_objects(): + for prop in cap.get_properties_objects(): + propvalue = functions.get_function( + self, + node_template, + prop.value) + if isinstance(propvalue, functions.GetInput): + propvalue = propvalue.result() + for p, v in cap._properties.items(): + if p == prop.name: + cap._properties[p] = propvalue + for rel, node in node_template.relationships.items(): + rel_tpls = node.relationship_tpl + if rel_tpls: + for rel_tpl in rel_tpls: + for interface in rel_tpl.interfaces: + if interface.inputs: + for name, value in interface.inputs.items(): + interface.inputs[name] = \ + functions.get_function(self, + rel_tpl, + value) + for output in self.outputs: + func = functions.get_function(self, self.outputs, output.value) + if isinstance(func, functions.GetAttribute): + output.attrs[output.VALUE] = func diff --git a/IM/tosca/toscaparser/tosca_template.py b/IM/tosca/toscaparser/tosca_template.py new file mode 100644 index 000000000..0c7589fec --- /dev/null +++ b/IM/tosca/toscaparser/tosca_template.py @@ -0,0 +1,190 @@ +# 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. + + +import logging +import os + +from IM.tosca.toscaparser.common.exception import InvalidTemplateVersion +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.topology_template import TopologyTemplate +from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph +from IM.tosca.toscaparser.utils.gettextutils import _ +import IM.tosca.toscaparser.utils.urlutils +import IM.tosca.toscaparser.utils.yamlparser + + +# TOSCA template key names +SECTIONS = (DEFINITION_VERSION, DEFAULT_NAMESPACE, TEMPLATE_NAME, + TOPOLOGY_TEMPLATE, TEMPLATE_AUTHOR, TEMPLATE_VERSION, + DESCRIPTION, IMPORTS, DSL_DEFINITIONS, NODE_TYPES, + RELATIONSHIP_TYPES, RELATIONSHIP_TEMPLATES, + CAPABILITY_TYPES, ARTIFACT_TYPES, DATATYPE_DEFINITIONS) = \ + ('tosca_definitions_version', 'tosca_default_namespace', + 'template_name', 'topology_template', 'template_author', + 'template_version', 'description', 'imports', 'dsl_definitions', + 'node_types', 'relationship_types', 'relationship_templates', + 'capability_types', 'artifact_types', 'datatype_definitions') + +log = logging.getLogger("tosca.model") + +YAML_LOADER = IM.tosca.toscaparser.utils.yamlparser.load_yaml + + +class ToscaTemplate(object): + + VALID_TEMPLATE_VERSIONS = ['tosca_simple_yaml_1_0'] + + '''Load the template data.''' + def __init__(self, path, a_file=True, parsed_params=None): + self.tpl = YAML_LOADER(path, a_file) + self.path = path + self.a_file = a_file + self.parsed_params = parsed_params + self._validate_field() + self.version = self._tpl_version() + self.relationship_types = self._tpl_relationship_types() + self.description = self._tpl_description() + self.topology_template = self._topology_template() + self.inputs = self._inputs() + self.relationship_templates = self._relationship_templates() + self.nodetemplates = self._nodetemplates() + self.outputs = self._outputs() + self.graph = ToscaGraph(self.nodetemplates) + + def _topology_template(self): + return TopologyTemplate(self._tpl_topology_template(), + self._get_all_custom_defs(), + self.relationship_types, + self.parsed_params) + + def _inputs(self): + return self.topology_template.inputs + + def _nodetemplates(self): + return self.topology_template.nodetemplates + + def _relationship_templates(self): + return self.topology_template.relationship_templates + + def _outputs(self): + return self.topology_template.outputs + + def _tpl_version(self): + return self.tpl[DEFINITION_VERSION] + + def _tpl_description(self): + return self.tpl[DESCRIPTION].rstrip() + + def _tpl_imports(self): + if IMPORTS in self.tpl: + return self.tpl[IMPORTS] + + def _tpl_relationship_types(self): + return self._get_custom_types(RELATIONSHIP_TYPES) + + def _tpl_relationship_templates(self): + topology_template = self._tpl_topology_template() + if RELATIONSHIP_TEMPLATES in topology_template.keys(): + return topology_template[RELATIONSHIP_TEMPLATES] + else: + return None + + def _tpl_topology_template(self): + return self.tpl.get(TOPOLOGY_TEMPLATE) + + def _get_all_custom_defs(self): + types = [NODE_TYPES, CAPABILITY_TYPES, RELATIONSHIP_TYPES, + DATATYPE_DEFINITIONS] + custom_defs = {} + for type in types: + custom_def = self._get_custom_types(type) + if custom_def: + custom_defs.update(custom_def) + return custom_defs + + def _get_custom_types(self, type_definition): + """Handle custom types defined in imported template files + + This method loads the custom type definitions referenced in "imports" + section of the TOSCA YAML template by determining whether each import + is specified via a file reference (by relative or absolute path) or a + URL reference. It then assigns the correct value to "def_file" variable + so the YAML content of those imports can be loaded. + + Possibilities: + +----------+--------+------------------------------+ + | template | import | comment | + +----------+--------+------------------------------+ + | file | file | OK | + | file | URL | OK | + | URL | file | file must be a relative path | + | URL | URL | OK | + +----------+--------+------------------------------+ + """ + + custom_defs = {} + imports = self._tpl_imports() + if imports: + main_a_file = os.path.isfile(self.path) + for definition in imports: + def_file = definition + a_file = False + if main_a_file: + if os.path.isfile(definition): + a_file = True + else: + full_path = os.path.join( + os.path.dirname(os.path.abspath(self.path)), + definition) + if os.path.isfile(full_path): + a_file = True + def_file = full_path + else: # main_a_url + a_url = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ + validate_url(definition) + if not a_url: + if os.path.isabs(definition): + raise ImportError(_("Absolute file name cannot be " + "used for a URL-based input " + "template.")) + def_file = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ + join_url(self.path, definition) + + custom_type = YAML_LOADER(def_file, a_file) + outer_custom_types = custom_type.get(type_definition) + if outer_custom_types: + custom_defs.update(outer_custom_types) + + # Handle custom types defined in current template file + inner_custom_types = self.tpl.get(type_definition) or {} + if inner_custom_types: + custom_defs.update(inner_custom_types) + return custom_defs + + def _validate_field(self): + try: + version = self._tpl_version() + self._validate_version(version) + except KeyError: + raise MissingRequiredFieldError(what='Template', + required=DEFINITION_VERSION) + for name in self.tpl: + if name not in SECTIONS: + raise UnknownFieldError(what='Template', field=name) + + def _validate_version(self, version): + if version not in self.VALID_TEMPLATE_VERSIONS: + raise InvalidTemplateVersion( + what=version, + valid_versions=', '. join(self.VALID_TEMPLATE_VERSIONS)) diff --git a/IM/tosca/toscaparser/tpl_relationship_graph.py b/IM/tosca/toscaparser/tpl_relationship_graph.py new file mode 100644 index 000000000..1a5ea7b66 --- /dev/null +++ b/IM/tosca/toscaparser/tpl_relationship_graph.py @@ -0,0 +1,46 @@ +# 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. + + +class ToscaGraph(object): + '''Graph of Tosca Node Templates.''' + def __init__(self, nodetemplates): + self.nodetemplates = nodetemplates + self.vertices = {} + self._create() + + def _create_vertex(self, node): + if node not in self.vertices: + self.vertices[node.name] = node + + def _create_edge(self, node1, node2, relationship): + if node1 not in self.vertices: + self._create_vertex(node1) + self.vertices[node1.name]._add_next(node2, + relationship) + + def vertex(self, node): + if node in self.vertices: + return self.vertices[node] + + def __iter__(self): + return iter(self.vertices.values()) + + def _create(self): + for node in self.nodetemplates: + relation = node.relationships + if relation: + for rel, nodetpls in relation.items(): + for tpl in self.nodetemplates: + if tpl.name == nodetpls.name: + self._create_edge(node, tpl, rel) + self._create_vertex(node) diff --git a/IM/tosca/toscaparser/utils/__init__.py b/IM/tosca/toscaparser/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/utils/gettextutils.py b/IM/tosca/toscaparser/utils/gettextutils.py new file mode 100644 index 000000000..f5562e2d7 --- /dev/null +++ b/IM/tosca/toscaparser/utils/gettextutils.py @@ -0,0 +1,22 @@ +# 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. + +import gettext +import os + +_localedir = os.environ.get('tosca-parser'.upper() + '_LOCALEDIR') +_t = gettext.translation('tosca-parser', localedir=_localedir, + fallback=True) + + +def _(msg): + return _t.gettext(msg) diff --git a/IM/tosca/toscaparser/utils/urlutils.py b/IM/tosca/toscaparser/utils/urlutils.py new file mode 100644 index 000000000..628314cdf --- /dev/null +++ b/IM/tosca/toscaparser/utils/urlutils.py @@ -0,0 +1,43 @@ +# 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. + + +from six.moves.urllib.parse import urljoin +from six.moves.urllib.parse import urlparse +from IM.tosca.toscaparser.utils.gettextutils import _ + + +class UrlUtils(object): + + @staticmethod + def validate_url(path): + """Validates whether the given path is a URL or not. + + If the given path includes a scheme (http, https, ftp, ...) and a net + location (a domain name such as www.github.com) it is validated as a + URL. + """ + parsed = urlparse(path) + return bool(parsed.scheme) and bool(parsed.netloc) + + @staticmethod + def join_url(url, relative_path): + """Builds a new URL from the given URL and the relative path. + + Example: + url: http://www.githib.com/openstack/heat + relative_path: heat-translator + - joined: http://www.githib.com/openstack/heat-translator + """ + if not UrlUtils.validate_url(url): + raise ValueError(_("Provided URL is invalid.")) + return urljoin(url, relative_path) diff --git a/IM/tosca/toscaparser/utils/validateutils.py b/IM/tosca/toscaparser/utils/validateutils.py new file mode 100644 index 000000000..42bfc4664 --- /dev/null +++ b/IM/tosca/toscaparser/utils/validateutils.py @@ -0,0 +1,154 @@ +# 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. + +import collections +import dateutil.parser +import logging +import numbers +import re +import six + +from IM.tosca.toscaparser.common.exception import ( + InvalidTOSCAVersionPropertyException) +from IM.tosca.toscaparser.utils.gettextutils import _ +log = logging.getLogger('tosca') + + +def str_to_num(value): + '''Convert a string representation of a number into a numeric type.''' + if isinstance(value, numbers.Number): + return value + try: + return int(value) + except ValueError: + return float(value) + + +def validate_number(value): + return str_to_num(value) + + +def validate_integer(value): + if not isinstance(value, int): + try: + value = int(value) + except Exception: + raise ValueError(_('"%s" is not an integer') % value) + return value + + +def validate_float(value): + if not isinstance(value, float): + raise ValueError(_('"%s" is not a float') % value) + return validate_number(value) + + +def validate_string(value): + if not isinstance(value, six.string_types): + raise ValueError(_('"%s" is not a string') % value) + return value + + +def validate_list(value): + if not isinstance(value, list): + raise ValueError(_('"%s" is not a list') % value) + return value + + +def validate_map(value): + if not isinstance(value, collections.Mapping): + raise ValueError(_('"%s" is not a map') % value) + return value + + +def validate_boolean(value): + if isinstance(value, bool): + return value + + if isinstance(value, str): + normalised = value.lower() + if normalised in ['true', 'false']: + return normalised == 'true' + raise ValueError(_('"%s" is not a boolean') % value) + + +def validate_timestamp(value): + return dateutil.parser.parse(value) + + +class TOSCAVersionProperty(object): + + VERSION_RE = re.compile('^(?P([0-9][0-9]*))' + '(\.(?P([0-9][0-9]*)))?' + '(\.(?P([0-9][0-9]*)))?' + '(\.(?P([0-9A-Za-z]+)))?' + '(\-(?P[0-9])*)?$') + + def __init__(self, version): + self.version = str(version) + match = self.VERSION_RE.match(self.version) + if not match: + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + ver = match.groupdict() + if self.version in ['0', '0.0', '0.0.0']: + log.warning(_('Version assumed as not provided')) + self.version = None + self.minor_version = ver['minor_version'] + self.major_version = ver['major_version'] + self.fix_version = ver['fix_version'] + self.qualifier = self._validate_qualifier(ver['qualifier']) + self.build_version = self._validate_build(ver['build_version']) + self._validate_major_version(self.major_version) + + def _validate_major_version(self, value): + """Validate major version + + Checks if only major version is provided and assumes + minor version as 0. + Eg: If version = 18, then it returns version = '18.0' + """ + + if self.minor_version is None and self.build_version is None and \ + value != '0': + log.warning(_('Minor version assumed "0"')) + self.version = '.'.join([value, '0']) + return value + + def _validate_qualifier(self, value): + """Validate qualifier + + TOSCA version is invalid if a qualifier is present without the + fix version or with all of major, minor and fix version 0s. + + For example, the following versions are invalid + 18.0.abc + 0.0.0.abc + """ + if (self.fix_version is None and value) or \ + (self.minor_version == self.major_version == + self.fix_version == '0' and value): + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + return value + + def _validate_build(self, value): + """Validate build version + + TOSCA version is invalid if build version is present without the + qualifier. + Eg: version = 18.0.0-1 is invalid. + """ + if not self.qualifier and value: + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + return value + + def get_version(self): + return self.version diff --git a/IM/tosca/toscaparser/utils/yamlparser.py b/IM/tosca/toscaparser/utils/yamlparser.py new file mode 100644 index 000000000..cd6c5b224 --- /dev/null +++ b/IM/tosca/toscaparser/utils/yamlparser.py @@ -0,0 +1,73 @@ +# 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. + +import codecs +from collections import OrderedDict +import yaml + +try: + # Python 3.x + import urllib.request as urllib2 +except ImportError: + # Python 2.x + import urllib2 + +if hasattr(yaml, 'CSafeLoader'): + yaml_loader = yaml.CSafeLoader +else: + yaml_loader = yaml.SafeLoader + + +def load_yaml(path, a_file=True): + # Miguel: enable to load also a TOSCA string + if path.find("\n") == -1: + f = codecs.open(path, encoding='utf-8', errors='strict') if a_file \ + else urllib2.urlopen(path) + return yaml.load(f.read(), Loader=yaml_loader) + else: + return yaml.load(path, Loader=yaml_loader) + + +def simple_parse(tmpl_str): + try: + tpl = yaml.load(tmpl_str, Loader=yaml_loader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + return tpl + + +def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict): + class OrderedLoader(Loader): + pass + + def construct_mapping(loader, node): + loader.flatten_mapping(node) + return object_pairs_hook(loader.construct_pairs(node)) + + OrderedLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + construct_mapping) + return yaml.load(stream, OrderedLoader) + + +def simple_ordered_parse(tmpl_str): + try: + tpl = ordered_load(tmpl_str) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + return tpl diff --git a/examples/galaxy_tosca.yml b/examples/galaxy_tosca.yml new file mode 100644 index 000000000..dc8e7a7e8 --- /dev/null +++ b/examples/galaxy_tosca.yml @@ -0,0 +1,38 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA Galaxy test for the IM + +topology_template: + + node_templates: + + bowtie2_galaxy_tool: + type: tosca.nodes.indigo.GalaxyTool + properties: + name: bowtie2 + owner: devteam + tool_panel_section_id: ngs_mapping + requirements: + - host: galaxy + + galaxy: + type: tosca.nodes.indigo.GalaxyPortal + requirements: + - host: galaxy_server + + galaxy_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + #distribution: scientific + #version: 6.6 + diff --git a/examples/tosca.yml b/examples/tosca.yml new file mode 100644 index 000000000..ea0f9689a --- /dev/null +++ b/examples/tosca.yml @@ -0,0 +1,118 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: dbname + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + relationship_templates: + my_custom_connection: + type: HostedOn + interfaces: + Configure: + pre_configure_source: scripts/wp_db_configure.sh + + node_templates: + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: scientific + version: 6.6 + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: mysql + relationship: my_custom_connection + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node_filter: + capabilities: + # Constraints for selecting “host” (Container Capability) + - host: + properties: + - num_cpus: { in_range: [1,4] } + - mem_size: { greater_or_equal: 1 GB } + # Constraints for selecting “os” (OperatingSystem Capability) + - os: + properties: + - architecture: { equal: x86_64 } + - type: linux + - distribution: ubuntu + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + requirements: + # contextually this can only be a relationship type + - local_storage: + # capability is provided by Compute Node Type + node: my_block_storage + relationship: + type: AttachesTo + properties: + location: /mnt/disk + # This maps the local requirement name ‘local_storage’ to the + # target node’s capability name ‘attachment’ + device: hdb + interfaces: + Configure: + pre_configure_source: scripts/wp_db_configure.sh + + my_block_storage: + type: BlockStorage + properties: + size: 1 GB + + + From 46460198d282f1af89a576cbca3e93cc0c2e65be Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 26 Oct 2015 15:53:33 +0100 Subject: [PATCH 002/509] Add TOSCA support to IM --- IM/InfrastructureManager.py | 17 +- IM/tosca/Tosca.py | 880 +++++++++++++++++ IM/tosca/__init__.py | 16 + IM/tosca/artifacts/apache/apache_install.yml | 26 + .../artifacts/galaxy/galaxy_configure.yml | 25 + IM/tosca/artifacts/galaxy/galaxy_install.yml | 9 + IM/tosca/artifacts/galaxy/galaxy_start.yml | 6 + .../galaxy/galaxy_tools_configure.yml | 34 + IM/tosca/artifacts/mysql/mysql_configure.yml | 4 + .../artifacts/mysql/mysql_db_configure.yml | 5 + IM/tosca/artifacts/mysql/mysql_install.yml | 27 + IM/tosca/toscaparser/__init__.py | 19 + IM/tosca/toscaparser/capabilities.py | 57 ++ IM/tosca/toscaparser/common/__init__.py | 0 IM/tosca/toscaparser/common/exception.py | 100 ++ IM/tosca/toscaparser/dataentity.py | 159 ++++ .../elements/TOSCA_definition_1_0.yaml | 893 ++++++++++++++++++ IM/tosca/toscaparser/elements/__init__.py | 0 IM/tosca/toscaparser/elements/artifacttype.py | 45 + .../elements/attribute_definition.py | 20 + .../toscaparser/elements/capabilitytype.py | 71 ++ IM/tosca/toscaparser/elements/constraints.py | 569 +++++++++++ IM/tosca/toscaparser/elements/datatype.py | 56 ++ IM/tosca/toscaparser/elements/entity_type.py | 113 +++ IM/tosca/toscaparser/elements/interfaces.py | 74 ++ IM/tosca/toscaparser/elements/nodetype.py | 200 ++++ IM/tosca/toscaparser/elements/policytype.py | 45 + .../elements/property_definition.py | 46 + .../toscaparser/elements/relationshiptype.py | 33 + IM/tosca/toscaparser/elements/scalarunit.py | 130 +++ .../elements/statefulentitytype.py | 81 ++ IM/tosca/toscaparser/entity_template.py | 285 ++++++ IM/tosca/toscaparser/functions.py | 410 ++++++++ IM/tosca/toscaparser/groups.py | 27 + IM/tosca/toscaparser/nodetemplate.py | 242 +++++ IM/tosca/toscaparser/parameters.py | 110 +++ IM/tosca/toscaparser/prereq/__init__.py | 0 IM/tosca/toscaparser/prereq/csar.py | 122 +++ IM/tosca/toscaparser/properties.py | 79 ++ IM/tosca/toscaparser/relationship_template.py | 68 ++ IM/tosca/toscaparser/topology_template.py | 213 +++++ IM/tosca/toscaparser/tosca_template.py | 190 ++++ .../toscaparser/tpl_relationship_graph.py | 46 + IM/tosca/toscaparser/utils/__init__.py | 0 IM/tosca/toscaparser/utils/gettextutils.py | 22 + IM/tosca/toscaparser/utils/urlutils.py | 43 + IM/tosca/toscaparser/utils/validateutils.py | 154 +++ IM/tosca/toscaparser/utils/yamlparser.py | 73 ++ examples/galaxy_tosca.yml | 38 + examples/tosca.yml | 118 +++ 50 files changed, 5998 insertions(+), 2 deletions(-) create mode 100644 IM/tosca/Tosca.py create mode 100644 IM/tosca/__init__.py create mode 100755 IM/tosca/artifacts/apache/apache_install.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_configure.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_install.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_start.yml create mode 100644 IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml create mode 100755 IM/tosca/artifacts/mysql/mysql_configure.yml create mode 100644 IM/tosca/artifacts/mysql/mysql_db_configure.yml create mode 100755 IM/tosca/artifacts/mysql/mysql_install.yml create mode 100644 IM/tosca/toscaparser/__init__.py create mode 100644 IM/tosca/toscaparser/capabilities.py create mode 100644 IM/tosca/toscaparser/common/__init__.py create mode 100644 IM/tosca/toscaparser/common/exception.py create mode 100644 IM/tosca/toscaparser/dataentity.py create mode 100644 IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml create mode 100644 IM/tosca/toscaparser/elements/__init__.py create mode 100644 IM/tosca/toscaparser/elements/artifacttype.py create mode 100644 IM/tosca/toscaparser/elements/attribute_definition.py create mode 100644 IM/tosca/toscaparser/elements/capabilitytype.py create mode 100644 IM/tosca/toscaparser/elements/constraints.py create mode 100644 IM/tosca/toscaparser/elements/datatype.py create mode 100644 IM/tosca/toscaparser/elements/entity_type.py create mode 100644 IM/tosca/toscaparser/elements/interfaces.py create mode 100644 IM/tosca/toscaparser/elements/nodetype.py create mode 100644 IM/tosca/toscaparser/elements/policytype.py create mode 100644 IM/tosca/toscaparser/elements/property_definition.py create mode 100644 IM/tosca/toscaparser/elements/relationshiptype.py create mode 100644 IM/tosca/toscaparser/elements/scalarunit.py create mode 100644 IM/tosca/toscaparser/elements/statefulentitytype.py create mode 100644 IM/tosca/toscaparser/entity_template.py create mode 100644 IM/tosca/toscaparser/functions.py create mode 100644 IM/tosca/toscaparser/groups.py create mode 100644 IM/tosca/toscaparser/nodetemplate.py create mode 100644 IM/tosca/toscaparser/parameters.py create mode 100644 IM/tosca/toscaparser/prereq/__init__.py create mode 100644 IM/tosca/toscaparser/prereq/csar.py create mode 100644 IM/tosca/toscaparser/properties.py create mode 100644 IM/tosca/toscaparser/relationship_template.py create mode 100644 IM/tosca/toscaparser/topology_template.py create mode 100644 IM/tosca/toscaparser/tosca_template.py create mode 100644 IM/tosca/toscaparser/tpl_relationship_graph.py create mode 100644 IM/tosca/toscaparser/utils/__init__.py create mode 100644 IM/tosca/toscaparser/utils/gettextutils.py create mode 100644 IM/tosca/toscaparser/utils/urlutils.py create mode 100644 IM/tosca/toscaparser/utils/validateutils.py create mode 100644 IM/tosca/toscaparser/utils/yamlparser.py create mode 100644 examples/galaxy_tosca.yml create mode 100644 examples/tosca.yml diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 6a47f5efc..2cf4a9c76 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -34,6 +34,7 @@ from IM.radl.radl import Feature from IM.recipe import Recipe from IM.db import DataBase +from IM.tosca.Tosca import Tosca from config import Config @@ -354,10 +355,22 @@ def AddResource(inf_id, radl_data, auth, context = True, failed_clouds = []): InfrastructureManager.logger.info("Adding resources to inf: " + str(inf_id)) - radl = radl_parse.parse_radl(radl_data) - radl.check() + # TODO: Think about CSAR files using xmlrpclib.Binary o enconding a file using b64 + # see: http://stackoverflow.com/questions/9099174/send-file-from-client-to-server-using-xmlrpc + # We must save the file, unzip it and get the file pointed by: Entry-Definitions: some.yaml + # http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/csd03/TOSCA-Simple-Profile-YAML-v1.0-csd03.html#_Toc419746172 + if Tosca.is_tosca(radl_data): + try: + tosca = Tosca(radl_data) + radl = tosca.to_radl() + except Exception, ex: + InfrastructureManager.logger.exception("Error parsing TOSCA input data.") + raise Exception("Error parsing TOSCA input data: " + str(ex)) + else: + radl = radl_parse.parse_radl(radl_data) InfrastructureManager.logger.debug(radl) + radl.check() sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py new file mode 100644 index 000000000..bfd6df0ab --- /dev/null +++ b/IM/tosca/Tosca.py @@ -0,0 +1,880 @@ +import os +import logging +import yaml +import copy + +from IM.tosca.toscaparser.tosca_template import ToscaTemplate +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.functions import Function, is_function, get_function, GetAttribute +from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from pylint.pyreverse.diagrams import Relationship +from compiler.ast import Node + +class Tosca: + """ + Class to translate a TOSCA document to an RADL object. + + TODO: What about CSAR files? + + """ + + ARTIFACTS_PATH = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/artifacts" + CUSTOM_TYPES_FILE = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/custon_types.yaml" + + logger = logging.getLogger('InfrastructureManager') + + def __init__(self, path): + self.path = path + self.tosca = None + self.tosca = ToscaTemplate(path) + + @staticmethod + def is_tosca(yaml_string): + """ + Check if a string seems to be a tosca document + Check if it is a correct YAML document and has the item 'tosca_definitions_version' + """ + try: + yamlo = yaml.load(yaml_string) + if isinstance(yamlo, dict) and 'tosca_definitions_version' in yamlo.keys(): + return True + else: + return False + except: + return False + + def to_radl(self): + """ + Converts the current ToscaTemplate object in a RADL object + """ + + relationships = [] + for node in self.tosca.nodetemplates: + # Store relationships to check later + for relationship, target in node.relationships.iteritems(): + source = node + relationships.append((source, target, relationship)) + + radl = RADL() + interfaces = {} + cont_intems = [] + + for node in self.tosca.nodetemplates: + root_type = Tosca._get_root_parent_type(node).type + + if root_type == "tosca.nodes.BlockStorage": + # The BlockStorage disks are processed later + pass + elif root_type == "tosca.nodes.network.Port": + pass + elif root_type == "tosca.nodes.network.Network": + # TODO: check IM to support more network properties + # At this moment we only support the network_type with values, private and public + net = Tosca._gen_network(node) + radl.networks.append(net) + else: + if root_type == "tosca.nodes.Compute": + # Add the system RADL element + sys = Tosca._gen_system(node, self.tosca.nodetemplates) + radl.systems.append(sys) + # Add the deploy element for this system + dep = deploy(sys.name, 1) + radl.deploys.append(dep) + compute = node + else: + # Select the host to host this element + compute = Tosca._find_host_compute(node, self.tosca.nodetemplates) + + interfaces = Tosca._get_interfaces(node) + interfaces.update(Tosca._get_relationships_interfaces(relationships, node)) + + conf = self._gen_configure_from_interfaces(radl, node, interfaces, compute) + if conf: + level = Tosca._get_dependency_level(node) + radl.configures.append(conf) + cont_intems.append(contextualize_item(compute.name, conf.name, level)) + + if cont_intems: + radl.contextualize = contextualize(cont_intems) + + return self._complete_radl_networks(radl) + + @staticmethod + def _get_relationship_template(rel, src, trgt): + rel_tpls = src.get_relationship_template() + rel_tpls.extend(trgt.get_relationship_template()) + for rel_tpl in rel_tpls: + if rel.type == rel_tpl.type: + return rel_tpl + + @staticmethod + def _get_relationships_interfaces(relationships, node): + res = {} + for src, trgt, rel in relationships: + rel_tpl = Tosca._get_relationship_template(rel, src, trgt) + if src.name == node.name: + for name in ['pre_configure_source', 'post_configure_source', 'add_source']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface + elif trgt.name == node.name: + for name in ['pre_configure_target', 'post_configure_target', 'add_target','target_changed','remove_target']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface + return res + + def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): + if not interfaces: + return None + + variables = "" + tasks = "" + recipe_list = [] + remote_artifacts_path = "/tmp" + # Take the interfaces in correct order + for name in ['create', 'pre_configure_source','pre_configure_target','configure', 'post_configure_source','post_configure_target', 'start', 'add_target','add_source','target_changed','remove_target']: + interface = interfaces.get(name, None) + if interface: + artifacts = [] + # Get the inputs + env = {} + if interface.inputs: + for param_name, param_value in interface.inputs.iteritems(): + val = None + + if self._is_artifact(param_value): + artifact_uri = self._get_artifact_uri(param_value, node) + val = remote_artifacts_path + "/" + os.path.basename(artifact_uri) + artifacts.append(artifact_uri) + else: + val = self._final_function_result(param_value, node) + + if val: + env[param_name] = val + else: + raise Exception("input value for %s in interface %s of node %s not valid" % (param_name, name, node.name)) + + name = node.name + "_" + interface.name + script_path = os.path.join(Tosca.ARTIFACTS_PATH, interface.implementation) + + # if there are artifacts to download + if artifacts: + for artifact in artifacts: + tasks += " - name: Download artifact " + artifact + "\n" + tasks += " get_url: dest=" + remote_artifacts_path + "/" + os.path.basename(artifact) + " url='" + artifact + "'\n" + + if interface.implementation.endswith(".yaml") or interface.implementation.endswith(".yml"): + if os.path.isfile(script_path): + f = open(script_path) + script_content = f.read() + f.close() + + if env: + for var_name, var_value in env.iteritems(): + variables += " %s: %s " % (var_name, var_value) + "\n" + variables += "\n" + + recipe_list.append(script_content) + else: + raise Exception(script_path + " is not located in the artifacts folder.") + else: + if os.path.isfile(script_path): + f = open(script_path) + script_content = f.read().replace("\n","\\n") + f.close() + + recipe = "- tasks:\n" + recipe += " - name: Copy contents of script of interface " + name + "\n" + recipe += " copy: dest=/tmp/" + os.path.basename(script_path) + " content='" + script_content + "' mode=0755\n" + + recipe += " - name: " + name + "\n" + recipe += " shell: /tmp/" + os.path.basename(script_path) + "\n" + if env: + recipe += " environment:\n" + for var_name, var_value in env.iteritems(): + recipe += " %s: %s\n" % (var_name, var_value) + + recipe_list.append(recipe) + else: + raise Exception(script_path + " is not located in the artifacts folder.") + + if tasks or recipe_list: + name = node.name + "_conf" + if variables: + recipes = "---\n- vars:\n" + variables + "\n" + recipes += " " + else: + recipes = "- " + + if tasks: + recipes += "tasks:\n" + tasks + "\n" + + # Merge the main recipe with the other yaml files + for recipe in recipe_list: + recipes = Tosca._merge_yaml(recipes, recipe) + + return configure(name, recipes) + else: + return None + + @staticmethod + def _is_artifact(function): + """Returns True if the provided function is a Tosca get_artifact function. + + Examples: + + * "{ get_artifact: { SELF, uri } }" + + :param function: Function as string. + :return: True if function is a Tosca get_artifact function, otherwise False. + """ + if isinstance(function, dict) and len(function) == 1: + func_name = list(function.keys())[0] + return func_name == "get_artifact" + return False + + @staticmethod + def _get_artifact_uri(function, node): + if isinstance(function, dict) and len(function) == 1: + name = function["get_artifact"][1] + artifacts = node.entity_tpl.get("artifacts") + if isinstance(artifacts, dict): + for artifact_name, value in artifacts.iteritems(): + if artifact_name == name: + return value['implementation'] + + return None + + @staticmethod + def _complete_radl_networks(radl): + if not radl.networks: + radl.networks.append(network.createNetwork("public", True)) + + public_net = None + for net in radl.networks: + if net.isPublic(): + public_net = net + break + + if not public_net: + for net in radl.networks: + public_net = net + + for sys in radl.systems: + if not sys.hasFeature("net_interface.0.connection"): + sys.setValue("net_interface.0.connection", public_net.id) + + return radl + + @staticmethod + def _is_intrinsic(function): + """Returns True if the provided function is a Tosca get_artifact function. + + Examples: + + * "{ concat: ['str1', 'str2'] }" + * "{ token: [ , , ] }" + + :param function: Function as string. + :return: True if function is a Tosca get_artifact function, otherwise False. + """ + if isinstance(function, dict) and len(function) == 1: + func_name = list(function.keys())[0] + return func_name in ["concat", "token"] + return False + + def _get_intrinsic_value(self, func, node): + if isinstance(func, dict) and len(func) == 1: + func_name = list(func.keys())[0] + if func_name == "concat": + items = func["concat"] + res = "" + for item in items: + if is_function(item): + res += str(self._final_function_result(item, node)) + else: + res += str(item) + return res + elif func_name == "token": + if len(items) == 3: + string_with_tokens = items[0] + string_of_token_chars = items[1] + substring_index = int(items[2]) + + parts = string_with_tokens.split(string_of_token_chars) + if len(parts) >= substring_index: + return parts[substring_index] + else: + Tosca.logger.error("Incorrect substring_index in function token.") + return None + else: + Tosca.logger.warn("Intrinsic function token must receive 3 parameters.") + return None + else: + Tosca.logger.warn("Intrinsic function %s not supported." % func_name) + return None + + def _get_attribute_result(self, func, node): + """Get an attribute value of an entity defined in the service template + + Node template attributes values are set in runtime and therefore its the + responsibility of the Tosca engine to implement the evaluation of + get_attribute functions. + + Arguments: + + * Node template name | HOST. + * Attribute name. + + If the HOST keyword is passed as the node template name argument the + function will search each node template along the HostedOn relationship + chain until a node which contains the attribute is found. + + Examples: + + * { get_attribute: [ server, private_address ] } + * { get_attribute: [ HOST, private_address ] } + * { get_attribute: [ SELF, private_address ] } + """ + node_name = func.args[0] + attribute_name = func.args[1] + + if node_name == "HOST": + node = self._find_host_compute(node, self.tosca.nodetemplates) + else: + for n in self.tosca.nodetemplates: + if n.name == node_name: + node = n + break + + if attribute_name == "tosca_id": + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_VMID }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_VMID'] }}" % node.name + elif attribute_name == "tosca_name": + return node.name + elif attribute_name == "private_address": + # TODO: we suppose that iface 1 is the private one + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_NET_1_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_1_IP'] }}" % node.name + elif attribute_name == "public_address": + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_ANSIBLE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name + elif attribute_name == "ip_address": + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.network.Port": + order = node.get_property_value('order') + return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) + elif root_type == "tosca.capabilities.Endpoint": + # TODO: check this + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_ANSIBLE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name + else: + Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + return None + else: + Tosca.logger.warn("Attribute %s not supported." % attribute_name) + return None + + def _final_function_result(self, func, node): + """ + Take a translator.toscalib.functions.Function and return the final result + (in some cases the result of a function is another function) + """ + if isinstance(func, dict): + if is_function(func): + func = get_function(self.tosca, node, func) + + if isinstance(func, Function): + if isinstance(func, GetAttribute): + func = self._get_attribute_result(func, node) + while isinstance(func, Function): + func = func.result() + + if isinstance(func, dict): + if self._is_intrinsic(func): + func = self._get_intrinsic_value(func, node) + + if func is None: + # TODO: resolve function values related with run-time values as IM or ansible variables + pass + return func + + @staticmethod + def _find_host_compute(node, nodetemplates): + """ + Select the node to host each node, using the node requirements + In most of the cases the are directly specified, otherwise "node_filter" is used + """ + + # check for a HosteOn relation + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.Compute": + return node + + if node.requirements: + for r, n in node.relationships.iteritems(): + if Tosca._is_derived_from(r, r.HOSTEDON) or Tosca._is_derived_from(r, r.BINDSTO): + root_type = Tosca._get_root_parent_type(n).type + if root_type == "tosca.nodes.Compute": + return n + else: + return Tosca._find_host_compute(n, nodetemplates) + + # There are no direct HostedOn node + # check node_filter requirements + if node.requirements: + for requires in node.requirements: + if 'host' in requires: + value = requires.get('host') + if isinstance(value, dict): + if 'node_filter' in value: + node_filter = value.get('node_filter') + return Tosca._get_compute_from_node_filter(node_filter, nodetemplates) + + return None + + @staticmethod + def _node_fulfill_filter(node, node_filter): + """ + Check if a node fulfills the features of a node filter + """ + + # Get node properties + node_props = {} + for cap_type in ['os', 'host']: + if node.get_capability(cap_type): + for prop in node.get_capability(cap_type).get_properties_objects(): + if prop.value: + unit = None + value = prop.value + if prop.name in ['disk_size', 'mem_size']: + value, unit = Tosca._get_size_and_unit(prop.value) + node_props[prop.name] = (value, unit) + + filter_props = {} + # Get node_filter properties + for elem in node_filter: + if isinstance(elem, dict): + for cap_type in ['os', 'host']: + if cap_type in elem: + for p in elem.get(cap_type).get('properties'): + p_name = p.keys()[0] + p_value = p.values()[0] + if isinstance(p_value, dict): + filter_props[p_name] = (p_value.keys()[0], p_value.values()[0]) + else: + filter_props[p_name] = ("equal", p_value) + + operator_map = { + 'equal':'==', + 'greater_than':'>', + 'greater_or_equal':'>=', + 'less_than': '<', + 'less_or_equal': '<=' + } + + # Compare the properties + for name, value in filter_props.iteritems(): + operator, filter_value = value + if name in ['disk_size', 'mem_size']: + filter_value, _ = Tosca._get_size_and_unit(filter_value) + + if name in node_props: + node_value, _ = node_props[name] + + if isinstance(node_value, str) or isinstance(node_value, unicode): + str_node_value = "'" + node_value + "'" + else: + str_node_value = str(node_value) + + conv_operator = operator_map.get(operator, None) + if conv_operator: + if isinstance(filter_value, str) or isinstance(filter_value, unicode): + str_filter_value = "'" + filter_value + "'" + else: + str_filter_value = str(filter_value) + + comparation = str_node_value + conv_operator + str_filter_value + else: + if operator == "in_range": + minv = filter_value[0] + maxv = filter_value[1] + comparation = str_node_value + ">=" +str(minv) + " and " + str_node_value + "<=" + str(maxv) + elif operator == "valid_values": + comparation = str_node_value + " in " + str(filter_value) + else: + Tosca.logger.warn("Logical operator %s not supported." % operator) + + if not eval(comparation): + return False + else: + # if this property is not specified in the node, return False + # TODO: we must think about default values + return False + + return True + + @staticmethod + def _get_compute_from_node_filter(node_filter, nodetemplates): + """ + Select the first node that fulfills the specified "node_filter" + """ + #{'capabilities': [{'host': {'properties': [{'num_cpus': {'in_range': [1, 4]}}, {'mem_size': {'greater_or_equal': '2 GB'}}]}}, {'os': {'properties': [{'architecture': {'equal': 'x86_64'}}, {'type': 'linux'}, {'distribution': 'ubuntu'}]}}]} + + for node in nodetemplates: + root_type = Tosca._get_root_parent_type(node).type + + if root_type == "tosca.nodes.Compute": + if Tosca._node_fulfill_filter(node, node_filter.get('capabilities')): + return node + + return None + + @staticmethod + def _get_dependency_level(node): + """ + Check the relations to get the contextualization level + """ + if node.related_nodes: + maxl = 0 + for node_depend in node.related_nodes: + level = Tosca._get_dependency_level(node_depend) + if level > maxl: + maxl = level + return maxl + 1 + else: + return 1 + + @staticmethod + def _unit_to_bytes(unit): + """Return the value of an unit.""" + if not unit: + return 1 + unit = unit.upper() + + if unit.startswith("KI"): + return 1024 + elif unit.startswith("K"): + return 1000 + elif unit.startswith("MI"): + return 1048576 + elif unit.startswith("M"): + return 1000000 + elif unit.startswith("GI"): + return 1073741824 + elif unit.startswith("G"): + return 1000000000 + elif unit.startswith("TI"): + return 1099511627776 + elif unit.startswith("T"): + return 1000000000000 + else: + return 1 + + @staticmethod + def _get_size_and_unit(str_value): + """ + Normalize the size and units to bytes + """ + parts = str_value.split(" ") + value = float(parts[0]) + unit = 'M' + if len(parts) > 1: + unit = parts[1] + + value = int(value * Tosca._unit_to_bytes(unit)) + + return value, 'B' + + @staticmethod + def _gen_network(node): + """ + Take a node of type "Network" and get the RADL.network to represent it + """ + res = network(node.name) + + nework_type = node.get_property_value("network_type") + network_name = node.get_property_value("network_name") + + # TODO: get more properties -> must be implemented in the RADL + if nework_type == "public": + res.setValue("outbound", "yes") + + if network_name: + res.setValue("provider_id", network_name) + + return res + + + @staticmethod + def _gen_system(node, nodetemplates): + """ + Take a node of type "Compute" and get the RADL.system to represent it + """ + res = system(node.name) + + property_map = { + 'architecture':'cpu.arch', + 'type':'disk.0.os.name', + 'distribution':'disk.0.os.flavour', + 'version': 'disk.0.os.version', + 'num_cpus': 'cpu.count', + 'disk_size': 'disk.0.size', + 'mem_size': 'memory.size', + 'cpu_frequency': 'cpu.performance' + } + + for cap_type in ['os', 'host']: + if node.get_capability(cap_type): + for prop in node.get_capability(cap_type).get_properties_objects(): + name = property_map.get(prop.name, None) + if name and prop.value: + unit = None + value = prop.value + if prop.name in ['disk_size', 'mem_size']: + value, unit = Tosca._get_size_and_unit(prop.value) + + if prop.name == "version": + value= str(value) + + if isinstance(value, float) or isinstance(value, int): + operator = ">=" + else: + operator = "=" + + feature = Feature(name, operator, value, unit) + res.addFeature(feature) + + # Find associated BlockStorages + disks = Tosca._get_attached_disks(node, nodetemplates) + + for size, unit, location, device, num in disks: + res.setValue('disk.%d.size' % num, size, unit) + if device: + res.setValue('disk.%d.device' % num, device) + if location: + res.setValue('disk.%d.mount_path' % num, location) + res.setValue('disk.%d.fstype' % num, "ext4") + + # Find associated Networks + nets = Tosca._get_bind_networks(node, nodetemplates) + for net_name, ip, dns_name, num in nets: + res.setValue('net_interface.%d.connection' % num, net_name) + if dns_name: + res.setValue('net_interface.%d.dns_name' % num, dns_name) + if ip: + res.setValue('net_interface.%d.ip' % num, ip) + + return res + + @staticmethod + def _get_bind_networks(node, nodetemplates): + nets = [] + count = 0 + for requires in node.requirements: + for value in requires.values(): + name = None + ip = None + dns_name = None + if isinstance(value, dict): + if 'relationship' in value: + rel = value.get('relationship') + + rel_type = None + if isinstance(rel, dict) and 'type' in rel: + rel_type = rel.get('type') + else: + rel_type = rel + + if rel_type and rel_type.endswith("BindsTo"): + if isinstance(rel, dict) and 'properties' in rel: + prop = rel.get('properties') + if isinstance(prop, dict): + ip = prop.get('ip', None) + dns_name = prop.get('dns_name', None) + + name = value.values()[0] + nets.append((name, ip, dns_name, count)) + count += 1 + else: + Tosca.logger.error("ERROR: expected dict in requires values.") + + for port in nodetemplates: + root_type = Tosca._get_root_parent_type(port).type + if root_type == "tosca.nodes.network.Port": + binding = None + link = None + for requires in port.requirements: + binding = requires.get('binding', binding) + link = requires.get('link', link) + + if binding == node.name: + ip = port.get_property_value('ip_address') + order = port.get_property_value('order') + dns_name = None + nets.append((link, ip, dns_name, order)) + + return nets + + + @staticmethod + def _get_attached_disks(node, nodetemplates): + """ + Get the disks attached to a node + """ + disks = [] + count = 1 + for requires in node.requirements: + for value in requires.values(): + size = None + location = None + device = None + if isinstance(value, dict): + if 'relationship' in value: + rel = value.get('relationship') + + rel_type = None + if isinstance(rel, dict) and 'type' in rel: + rel_type = rel.get('type') + else: + rel_type = rel + + if rel_type and rel_type.endswith("AttachesTo"): + if isinstance(rel, dict) and 'properties' in rel: + prop = rel.get('properties') + if isinstance(prop, dict): + location = prop.get('location', None) + device = prop.get('device', None) + + # seet a default device + if not device: + device = "hdb" + + for node_name in value.values(): + for n in nodetemplates: + if n.name == node_name: + size, unit = Tosca._get_size_and_unit(n.get_property_value('size')) + break + + disks.append((size, unit, location, device, count)) + count += 1 + else: + Tosca.logger.error("ERROR: expected dict in requires values.") + + return disks + + @staticmethod + def _is_derived_from(rel, parent_type): + """ + Check if a node is a descendant from a specified parent type + """ + while True: + if rel.type == parent_type: + return True + else: + if rel.parent_type: + rel = rel.parent_type + else: + return False + @staticmethod + def _get_root_parent_type(node): + """ + Get the root parent type of a node (just before the tosca.nodes.Root) + """ + node_type = node.type_definition + + while True: + if node_type.parent_type != None: + if node_type.parent_type.type.endswith(".Root"): + return node_type + else: + node_type = node_type.parent_type + else: + return node_type + + @staticmethod + def _get_interfaces(node): + """ + Get a dict of InterfacesDef of the specified node + """ + interfaces = {} + for interface in node.interfaces: + interfaces[interface.name] = interface + + node_type = node.type_definition + + while True: + if node_type.interfaces and 'Standard' in node_type.interfaces: + for name, elems in node_type.interfaces['Standard'].iteritems(): + if name in ['create', 'configure', 'start', 'stop', 'delete']: + if name not in interfaces: + interfaces[name] = InterfacesDef(node_type, 'Standard', name=name, value=elems) + + if node_type.parent_type != None: + node_type = node_type.parent_type + else: + return interfaces + + @staticmethod + def _merge_yaml(yaml1, yaml2): + """ + Merge two ansible yaml docs + + Arguments: + - yaml1(str): string with the first YAML + - yaml1(str): string with the second YAML + Returns: The merged YAML. In case of errors, it concatenates both strings + """ + yamlo1o = {} + try: + yamlo1o = yaml.load(yaml1)[0] + if not isinstance(yamlo1o, dict): + yamlo1o = {} + except Exception: + Tosca.logger.exception("Error parsing YAML: " + yaml1 + "\n Ignore it") + + try: + yamlo2s = yaml.load(yaml2) + if not isinstance(yamlo2s, list) or any([ not isinstance(d, dict) for d in yamlo2s ]): + yamlo2s = {} + except Exception: + Tosca.logger.exception("Error parsing YAML: " + yaml2 + "\n Ignore it") + yamlo2s = {} + + if not yamlo2s and not yamlo1o: + return "" + + result = [] + for yamlo2 in yamlo2s: + yamlo1 = copy.deepcopy(yamlo1o) + all_keys = [] + all_keys.extend(yamlo1.keys()) + all_keys.extend(yamlo2.keys()) + all_keys = set(all_keys) + + for key in all_keys: + if key in yamlo1 and yamlo1[key]: + if key in yamlo2 and yamlo2[key]: + if isinstance(yamlo1[key], dict): + yamlo1[key].update(yamlo2[key]) + elif isinstance(yamlo1[key], list): + yamlo1[key].extend(yamlo2[key]) + else: + # Both use have the same key with merge in a lists + v1 = yamlo1[key] + v2 = yamlo2[key] + yamlo1[key] = [v1, v2] + elif key in yamlo2 and yamlo2[key]: + yamlo1[key] = yamlo2[key] + result.append(yamlo1) + + return yaml.dump(result, default_flow_style=False, explicit_start=True, width=256) \ No newline at end of file diff --git a/IM/tosca/__init__.py b/IM/tosca/__init__.py new file mode 100644 index 000000000..c059187df --- /dev/null +++ b/IM/tosca/__init__.py @@ -0,0 +1,16 @@ +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + diff --git a/IM/tosca/artifacts/apache/apache_install.yml b/IM/tosca/artifacts/apache/apache_install.yml new file mode 100755 index 000000000..db572f85b --- /dev/null +++ b/IM/tosca/artifacts/apache/apache_install.yml @@ -0,0 +1,26 @@ + # Disable IPv6 + - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" + with_items: + - 'net.ipv6.conf.all.disable_ipv6' + - 'net.ipv6.conf.default.disable_ipv6' + - 'net.ipv6.conf.lo.disable_ipv6' + ignore_errors: yes + + - command: sysctl -p + ignore_errors: yes + + - name: Apache | Make sure the Apache packages are installed + apt: pkg=apache2 update_cache=yes + when: ansible_os_family == "Debian" + + - name: Start Apache service + service: name=apache2 state=started + when: ansible_os_family == "Debian" + + - name: Apache | Make sure the Apache packages are installed + apt: yum=httpd + when: ansible_os_family == "RedHat" + + - name: Start Apache service + service: name=httpd state=started + when: ansible_os_family == "RedHat" diff --git a/IM/tosca/artifacts/galaxy/galaxy_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_configure.yml new file mode 100644 index 000000000..164bbaf92 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_configure.yml @@ -0,0 +1,25 @@ +--- +- vars: + GALAXY_USER_ID: 4001 + GALAXY_USER_PASSWORD: $6$Ehg4GHQT5y$6ZCTLffp.epiNEhS1M3ZB.P6Kii1wELySe/DCwUInGt8r7zgdAHfHw66DuPwpS6pfOiZ9PS/KaTiBKjoCn23t0 + + tasks: + # General configuration + - copy: src={{galaxy_install_path}}/config/galaxy.ini.sample dest={{galaxy_install_path}}/config/galaxy.ini force=no + - ini_file: dest={{galaxy_install_path}}/config/galaxy.ini section={{ item.section }} option={{ item.option }} value="{{ item.value }}" + with_items: + - { section: 'server:main', option: 'host', value: '0.0.0.0' } + - { section: 'app:main', option: 'admin_users', value: "{{galaxy_admin}}" } + - { section: 'app:main', option: 'master_api_key', value: "{{galaxy_admin_api_key}}" } + - { section: 'app:main', option: 'tool_dependency_dir', value: "{{galaxy_install_path}}/tool_dependency_dir" } + + # Create galaxy user to launch the daemon + - user: name={{galaxy_user}} password={{GALAXY_USER_PASSWORD}} generate_ssh_key=yes shell=/bin/bash uid={{GALAXY_USER_ID}} + - local_action: command cp /home/{{galaxy_user}}/.ssh/id_rsa.pub /tmp/{{galaxy_user}}_id_rsa.pub creates=/tmp/{{galaxy_user}}_id_rsa.pub + - name: Add the authorized_key to the user {{galaxy_user}} + authorized_key: user={{galaxy_user}} key="{{ lookup('file', '/tmp/' + galaxy_user + '_id_rsa.pub') }}" + + - file: path=/home/{{galaxy_user}} state=directory owner={{galaxy_user}} group={{galaxy_user}} + - file: path={{galaxy_install_path}} state=directory recurse=yes owner={{galaxy_user}} + + - copy: dest="{{galaxy_install_path}}/config/local_env.sh" content="PYTHON_EGG_CACHE={{galaxy_install_path}}/egg\nGALAXY_RUN_ALL=1\nexport PYTHON_EGG_CACHE GALAXY_RUN_ALL" \ No newline at end of file diff --git a/IM/tosca/artifacts/galaxy/galaxy_install.yml b/IM/tosca/artifacts/galaxy/galaxy_install.yml new file mode 100644 index 000000000..cd6689fb0 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_install.yml @@ -0,0 +1,9 @@ +--- +- tasks: + # Install requisites + - apt: name=git update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + - yum: name=git + when: ansible_os_family == "RedHat" + # Download Galaxy + - git: repo=https://github.com/galaxyproject/galaxy/ dest={{galaxy_install_path}} version=master diff --git a/IM/tosca/artifacts/galaxy/galaxy_start.yml b/IM/tosca/artifacts/galaxy/galaxy_start.yml new file mode 100644 index 000000000..0801d10d8 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_start.yml @@ -0,0 +1,6 @@ +--- +- tasks: + # Launch the server + - shell: bash run.sh --daemon chdir={{galaxy_install_path}}/ creates={{galaxy_install_path}}/main.pid + sudo: true + sudo_user: "{{galaxy_user}}" diff --git a/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml new file mode 100644 index 000000000..047fca373 --- /dev/null +++ b/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml @@ -0,0 +1,34 @@ +--- +- vars: + tool_content: | + tools: + - name: '{{galaxy_tool_name}}' + owner: '{{galaxy_tool_owner}}' + tool_panel_section_id: '{{galaxy_tool_panel_section_id}}' + tasks: + # Install galaxy tools + - name: Uninstall old version of python-requests in Ubuntu + shell: dpkg --force-all -r python-requests + when: ansible_os_family == "Debian" + ignore_errors: yes + + - name: Install script dependencies + pip: name={{item}} state=latest + with_items: + - bioblend + - requests + + - name: Place the tool management script + get_url: url=https://raw.githubusercontent.com/galaxyproject/ansible-galaxy-tools/master/files/install_tool_shed_tools.py dest={{galaxy_install_path}}/install_tool_shed_tools.py + + - name: Copy tool list files + copy: + content: "{{tool_content}}" + dest: "{{galaxy_install_path}}/my_tool_list.yml" + + - name: Wait for Galaxy to start + wait_for: port=8080 delay=5 state=started timeout=150 + + - name: Install Tool Shed tools + shell: chdir={{galaxy_install_path}} python install_tool_shed_tools.py -t my_tool_list.yml -a {{galaxy_admin_api_key}} -g 127.0.0.1:8080 + #creates={{galaxy_install_path}}//tool_dependency_dir/bowtie2 diff --git a/IM/tosca/artifacts/mysql/mysql_configure.yml b/IM/tosca/artifacts/mysql/mysql_configure.yml new file mode 100755 index 000000000..446ac80c9 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_configure.yml @@ -0,0 +1,4 @@ +- tasks: + - name: update mysql root password for all root accounts + mysql_user: name=root password={{root_password}} + ignore_errors: yes diff --git a/IM/tosca/artifacts/mysql/mysql_db_configure.yml b/IM/tosca/artifacts/mysql/mysql_db_configure.yml new file mode 100644 index 000000000..1217d59b5 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_db_configure.yml @@ -0,0 +1,5 @@ + - name: Create DB {{name}} + mysql_db: name={{name}} state=present login_user=root login_password={{root_password}} + + - name: Create user {{user}} for the DB {{name}} + mysql_user: name={{user}} password={{password}} login_user=root login_password={{root_password}} priv={{name}}.*:ALL,GRANT state=present diff --git a/IM/tosca/artifacts/mysql/mysql_install.yml b/IM/tosca/artifacts/mysql/mysql_install.yml new file mode 100755 index 000000000..d7e5897c8 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_install.yml @@ -0,0 +1,27 @@ +- tasks: + # Disable IPv6 + - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" + with_items: + - 'net.ipv6.conf.all.disable_ipv6' + - 'net.ipv6.conf.default.disable_ipv6' + - 'net.ipv6.conf.lo.disable_ipv6' + ignore_errors: yes + + - command: sysctl -p + ignore_errors: yes + + - name: MySQL | Make sure the MySQL packages are installed + apt: pkg=mysql-server,python-mysqldb update_cache=yes + when: ansible_os_family == "Debian" + + - name: Start MySQL service + service: name=mysql state=started + when: ansible_os_family == "Debian" + + - name: MySQL | Make sure the MySQL packages are installed + apt: yum=mysql-server,MySQL-python + when: ansible_os_family == "RedHat" + + - name: Start MySQL service + service: name=mysqld state=started + when: ansible_os_family == "RedHat" diff --git a/IM/tosca/toscaparser/__init__.py b/IM/tosca/toscaparser/__init__.py new file mode 100644 index 000000000..f418d00a9 --- /dev/null +++ b/IM/tosca/toscaparser/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# 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. + +import pbr.version + +__version__ = "1.0.0" +#__version__ = pbr.version.VersionInfo( +# 'tosca-parser').version_string() diff --git a/IM/tosca/toscaparser/capabilities.py b/IM/tosca/toscaparser/capabilities.py new file mode 100644 index 000000000..5af77f862 --- /dev/null +++ b/IM/tosca/toscaparser/capabilities.py @@ -0,0 +1,57 @@ +# 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. + +from IM.tosca.toscaparser.properties import Property + + +class Capability(object): + '''TOSCA built-in capabilities type.''' + + def __init__(self, name, properties, definition): + self.name = name + self._properties = properties + self.definition = definition + + def get_properties_objects(self): + '''Return a list of property objects.''' + properties = [] + # Miguel: cambios aqui + props_def = self.definition.get_properties_def() + if props_def: + props_name = props_def.keys() + + for name in props_name: + value = None + if name in self._properties: + value = self._properties[name] + properties.append(Property(name, value, props_def[name].schema)) + +# props = self._properties +# +# if props: +# for name, value in props.items(): +# props_def = self.definition.get_properties_def() +# if props_def and name in props_def: +# properties.append(Property(name, value, +# props_def[name].schema)) + return properties + + def get_properties(self): + '''Return a dictionary of property name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_objects()} + + def get_property_value(self, name): + '''Return the value of a given property name.''' + props = self.get_properties() + if props and name in props: + return props[name].value diff --git a/IM/tosca/toscaparser/common/__init__.py b/IM/tosca/toscaparser/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/common/exception.py b/IM/tosca/toscaparser/common/exception.py new file mode 100644 index 000000000..fb2eea9e6 --- /dev/null +++ b/IM/tosca/toscaparser/common/exception.py @@ -0,0 +1,100 @@ +# 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. + +''' +TOSCA exception classes +''' +import logging +import sys + +from IM.tosca.toscaparser.utils.gettextutils import _ + + +log = logging.getLogger(__name__) + + +class TOSCAException(Exception): + '''Base exception class for TOSCA + + To correctly use this class, inherit from it and define + a 'msg_fmt' property. + + ''' + + _FATAL_EXCEPTION_FORMAT_ERRORS = False + + message = _('An unknown exception occurred.') + + def __init__(self, **kwargs): + try: + self.message = self.msg_fmt % kwargs + except KeyError: + exc_info = sys.exc_info() + log.exception(_('Exception in string format operation: %s') + % exc_info[1]) + + if TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS: + raise exc_info[0] + + def __str__(self): + return self.message + + @staticmethod + def set_fatal_format_exception(flag): + if isinstance(flag, bool): + TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS = flag + + +class MissingRequiredFieldError(TOSCAException): + msg_fmt = _('%(what)s is missing required field: "%(required)s".') + + +class UnknownFieldError(TOSCAException): + msg_fmt = _('%(what)s contain(s) unknown field: "%(field)s", ' + 'refer to the definition to verify valid values.') + + +class TypeMismatchError(TOSCAException): + msg_fmt = _('%(what)s must be of type: "%(type)s".') + + +class InvalidNodeTypeError(TOSCAException): + msg_fmt = _('Node type "%(what)s" is not a valid type.') + + +class InvalidTypeError(TOSCAException): + msg_fmt = _('Type "%(what)s" is not a valid type.') + + +class InvalidSchemaError(TOSCAException): + msg_fmt = _("%(message)s") + + +class ValidationError(TOSCAException): + msg_fmt = _("%(message)s") + + +class UnknownInputError(TOSCAException): + msg_fmt = _('Unknown input: %(input_name)s') + + +class InvalidPropertyValueError(TOSCAException): + msg_fmt = _('Value of property "%(what)s" is invalid.') + + +class InvalidTemplateVersion(TOSCAException): + msg_fmt = _('The template version "%(what)s" is invalid. ' + 'The valid versions are: "%(valid_versions)s"') + + +class InvalidTOSCAVersionPropertyException(TOSCAException): + msg_fmt = _('Value of TOSCA version property "%(what)s" is invalid.') diff --git a/IM/tosca/toscaparser/dataentity.py b/IM/tosca/toscaparser/dataentity.py new file mode 100644 index 000000000..eb67e63a4 --- /dev/null +++ b/IM/tosca/toscaparser/dataentity.py @@ -0,0 +1,159 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import TypeMismatchError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.elements.constraints import Schema +from IM.tosca.toscaparser.elements.datatype import DataType +from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Frequency +from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Size +from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Time + +from IM.tosca.toscaparser.utils.gettextutils import _ +from IM.tosca.toscaparser.utils import validateutils + + +class DataEntity(object): + '''A complex data value entity.''' + + def __init__(self, datatypename, value_dict, custom_def=None): + self.custom_def = custom_def + self.datatype = DataType(datatypename, custom_def) + self.schema = self.datatype.get_all_properties() + self.value = value_dict + + def validate(self): + '''Validate the value by the definition of the datatype.''' + + # A datatype can not have both 'type' and 'properties' definitions. + # If the datatype has 'type' definition + if self.datatype.value_type: + self.value = DataEntity.validate_datatype(self.datatype.value_type, + self.value, + None, + self.custom_def) + schema = Schema(None, self.datatype.defs) + for constraint in schema.constraints: + constraint.validate(self.value) + # If the datatype has 'properties' definition + else: + if not isinstance(self.value, dict): + raise TypeMismatchError(what=self.value, + type=self.datatype.type) + allowed_props = [] + required_props = [] + default_props = {} + if self.schema: + allowed_props = self.schema.keys() + for name, prop_def in self.schema.items(): + if prop_def.required: + required_props.append(name) + if prop_def.default: + default_props[name] = prop_def.default + + # check allowed field + for value_key in list(self.value.keys()): + if value_key not in allowed_props: + raise UnknownFieldError(what=_('Data value of type %s') + % self.datatype.type, + field=value_key) + + # check default field + for def_key, def_value in list(default_props.items()): + if def_key not in list(self.value.keys()): + self.value[def_key] = def_value + + # check missing field + missingprop = [] + for req_key in required_props: + if req_key not in list(self.value.keys()): + missingprop.append(req_key) + if missingprop: + raise MissingRequiredFieldError(what=_('Data value of type %s') + % self.datatype.type, + required=missingprop) + + # check every field + for name, value in list(self.value.items()): + prop_schema = Schema(name, self._find_schema(name)) + # check if field value meets type defined + DataEntity.validate_datatype(prop_schema.type, value, + prop_schema.entry_schema, + self.custom_def) + # check if field value meets constraints defined + if prop_schema.constraints: + for constraint in prop_schema.constraints: + constraint.validate(value) + + return self.value + + def _find_schema(self, name): + if self.schema and name in self.schema.keys(): + return self.schema[name].schema + + @staticmethod + def validate_datatype(type, value, entry_schema=None, custom_def=None): + '''Validate value with given type. + + If type is list or map, validate its entry by entry_schema(if defined) + If type is a user-defined complex datatype, custom_def is required. + ''' + if type == Schema.STRING: + return validateutils.validate_string(value) + elif type == Schema.INTEGER: + return validateutils.validate_integer(value) + elif type == Schema.FLOAT: + return validateutils.validate_float(value) + elif type == Schema.NUMBER: + return validateutils.validate_number(value) + elif type == Schema.BOOLEAN: + return validateutils.validate_boolean(value) + elif type == Schema.TIMESTAMP: + validateutils.validate_timestamp(value) + return value + elif type == Schema.LIST: + validateutils.validate_list(value) + if entry_schema: + DataEntity.validate_entry(value, entry_schema, custom_def) + return value + elif type == Schema.SCALAR_UNIT_SIZE: + return ScalarUnit_Size(value).validate_scalar_unit() + elif type == Schema.SCALAR_UNIT_FREQUENCY: + return ScalarUnit_Frequency(value).validate_scalar_unit() + elif type == Schema.SCALAR_UNIT_TIME: + return ScalarUnit_Time(value).validate_scalar_unit() + elif type == Schema.VERSION: + return validateutils.TOSCAVersionProperty(value).get_version() + elif type == Schema.MAP: + validateutils.validate_map(value) + if entry_schema: + DataEntity.validate_entry(value, entry_schema, custom_def) + return value + else: + data = DataEntity(type, value, custom_def) + return data.validate() + + @staticmethod + def validate_entry(value, entry_schema, custom_def=None): + '''Validate entries for map and list.''' + schema = Schema(None, entry_schema) + valuelist = value + if isinstance(value, dict): + valuelist = list(value.values()) + for v in valuelist: + DataEntity.validate_datatype(schema.type, v, schema.entry_schema, + custom_def) + if schema.constraints: + for constraint in schema.constraints: + constraint.validate(v) + return value diff --git a/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml b/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml new file mode 100644 index 000000000..b819c02b4 --- /dev/null +++ b/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml @@ -0,0 +1,893 @@ +# 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. + +########################################################################## +# The content of this file reflects TOSCA Simple Profile in YAML version +# 1.0.0. It describes the definition for TOSCA types including Node Type, +# Relationship Type, Capability Type and Interfaces. +########################################################################## +tosca_definitions_version: tosca_simple_yaml_1_0 + +########################################################################## +# Node Type. +# A Node Type is a reusable entity that defines the type of one or more +# Node Templates. +########################################################################## +tosca.nodes.Root: + description: > + The TOSCA root node all other TOSCA base node types derive from. + attributes: + tosca_id: + type: string + tosca_name: + type: string + state: + type: string + capabilities: + feature: + type: tosca.capabilities.Node + requirements: + - dependency: + capability: tosca.capabilities.Node + node: tosca.nodes.Root + relationship: tosca.relationships.DependsOn + occurrences: [ 0, UNBOUNDED ] + interfaces: + Standard: + type: tosca.interfaces.node.lifecycle.Standard + +tosca.nodes.Compute: + derived_from: tosca.nodes.Root + attributes: + private_address: + type: string + public_address: + type: string + capabilities: + host: + type: tosca.capabilities.Container + binding: + type: tosca.capabilities.network.Bindable + os: + type: tosca.capabilities.OperatingSystem + scalable: + type: tosca.capabilities.Scalable + requirements: + - local_storage: + capability: tosca.capabilities.Attachment + node: tosca.nodes.BlockStorage + relationship: tosca.relationships.AttachesTo + occurrences: [0, UNBOUNDED] + +tosca.nodes.SoftwareComponent: + derived_from: tosca.nodes.Root + properties: + # domain-specific software component version + component_version: + type: version + required: false + description: > + Software component version. + admin_credential: + type: tosca.datatypes.Credential + required: false + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.Compute + relationship: tosca.relationships.HostedOn + +tosca.nodes.DBMS: + derived_from: tosca.nodes.SoftwareComponent + properties: + port: + required: no + type: integer + description: > + The port the DBMS service will listen to for data and requests. + root_password: + required: no + type: string + description: > + The root password for the DBMS service. + capabilities: + host: + type: tosca.capabilities.Container + valid_source_types: [tosca.nodes.Database] + +tosca.nodes.Database: + derived_from: tosca.nodes.Root + properties: + user: + required: no + type: string + description: > + User account name for DB administration + name: + required: no + type: string + description: > + The name of the database. + password: + required: no + type: string + description: > + The password for the DB user account + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.DBMS + relationship: tosca.relationships.HostedOn + capabilities: + database_endpoint: + type: tosca.capabilities.Endpoint.Database + +tosca.nodes.WebServer: + derived_from: tosca.nodes.SoftwareComponent + capabilities: + data_endpoint: + type: tosca.capabilities.Endpoint + admin_endpoint: + type: tosca.capabilities.Endpoint.Admin + host: + type: tosca.capabilities.Container + valid_source_types: [tosca.nodes.WebApplication] + +tosca.nodes.WebApplication: + derived_from: tosca.nodes.Root + properties: + context_root: + type: string + required: false + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.WebServer + relationship: tosca.relationships.HostedOn + capabilities: + app_endpoint: + type: tosca.capabilities.Endpoint + +tosca.nodes.BlockStorage: + derived_from: tosca.nodes.Root + properties: + size: + type: scalar-unit.size + constraints: + - greater_or_equal: 1 MB + volume_id: + type: string + required: false + snapshot_id: + type: string + required: false + attributes: + volume_id: + type: string + capabilities: + attachment: + type: tosca.capabilities.Attachment + +tosca.nodes.network.Network: + derived_from: tosca.nodes.Root + description: > + The TOSCA Network node represents a simple, logical network service. + properties: + ip_version: + type: integer + required: no + default: 4 + constraints: + - valid_values: [ 4, 6 ] + description: > + The IP version of the requested network. Valid values are 4 for ipv4 + or 6 for ipv6. + cidr: + type: string + required: no + description: > + The cidr block of the requested network. + start_ip: + type: string + required: no + description: > + The IP address to be used as the start of a pool of addresses within + the full IP range derived from the cidr block. + end_ip: + type: string + required: no + description: > + The IP address to be used as the end of a pool of addresses within + the full IP range derived from the cidr block. + gateway_ip: + type: string + required: no + description: > + The gateway IP address. + network_name: + type: string + required: no + description: > + An identifier that represents an existing Network instance in the + underlying cloud infrastructure or can be used as the name of the + newly created network. If network_name is provided and no other + properties are provided (with exception of network_id), then an + existing network instance will be used. If network_name is provided + alongside with more properties then a new network with this name will + be created. + network_id: + type: string + required: no + description: > + An identifier that represents an existing Network instance in the + underlying cloud infrastructure. This property is mutually exclusive + with all other properties except network_name. This can be used alone + or together with network_name to identify an existing network. + network_type: + type: string + required: no + description: > + It specifies the nature of the physical network in the underlying + cloud infrastructure. Examples are flat, vlan, gre or vxlan. F + segmentation_id: + type: string + required: no + description: > + A segmentation identifier in the underlying cloud infrastructure. + E.g. VLAN ID, GRE tunnel ID, etc.. + dhcp_enabled: + type: boolean + required: no + default: true + description: > + Indicates should DHCP service be enabled on the network or not. + capabilities: + link: + type: tosca.capabilities.network.Linkable + +tosca.nodes.network.Port: + derived_from: tosca.nodes.Root + description: > + The TOSCA Port node represents a logical entity that associates between + Compute and Network normative types. The Port node type effectively + represents a single virtual NIC on the Compute node instance. + properties: + ip_address: + type: string + required: no + description: > + Allow the user to set a static IP. + order: + type: integer + required: no + default: 0 + constraints: + - greater_or_equal: 0 + description: > + The order of the NIC on the compute instance (e.g. eth2). + is_default: + type: boolean + required: no + default: false + description: > + If is_default=true this port will be used for the default gateway + route. Only one port that is associated to single compute node can + set as is_default=true. + ip_range_start: + type: string + required: no + description: > + Defines the starting IP of a range to be allocated for the compute + instances that are associated with this Port. + ip_range_end: + type: string + required: no + description: > + Defines the ending IP of a range to be allocated for the compute + instances that are associated with this Port. + attributes: + ip_address: + type: string + requirements: + - binding: + description: > + Binding requirement expresses the relationship between Port and + Compute nodes. Effectevely it indicates that the Port will be + attached to specific Compute node instance + capability: tosca.capabilities.network.Bindable + relationship: tosca.relationships.network.BindsTo + - link: + description: > + Link requirement expresses the relationship between Port and Network + nodes. It indicates which network this port will connect to. + capability: tosca.capabilities.network.Linkable + relationship: tosca.relationships.network.LinksTo + +tosca.nodes.ObjectStorage: + derived_from: tosca.nodes.Root + description: > + The TOSCA ObjectStorage node represents storage that provides the ability + to store data as objects (or BLOBs of data) without consideration for the + underlying filesystem or devices + properties: + name: + type: string + required: yes + description: > + The logical name of the object store (or container). + size: + type: scalar-unit.size + required: no + constraints: + - greater_or_equal: 0 GB + description: > + The requested initial storage size. + maxsize: + type: scalar-unit.size + required: no + constraints: + - greater_or_equal: 0 GB + description: > + The requested maximum storage size. + capabilities: + storage_endpoint: + type: tosca.capabilities.Endpoint + +########################################################################## +# Relationship Type. +# A Relationship Type is a reusable entity that defines the type of one +# or more relationships between Node Types or Node Templates. +########################################################################## +tosca.relationships.Root: + description: > + The TOSCA root Relationship Type all other TOSCA base Relationship Types + derive from. + attributes: + tosca_id: + type: string + tosca_name: + type: string + interfaces: + Configure: + type: tosca.interfaces.relationship.Configure + +tosca.relationships.DependsOn: + derived_from: tosca.relationships.Root + +tosca.relationships.HostedOn: + derived_from: tosca.relationships.Root + valid_target_types: [ tosca.capabilities.Container ] + +tosca.relationships.ConnectsTo: + derived_from: tosca.relationships.Root + valid_target_types: [ tosca.capabilities.Endpoint ] + credential: + type: tosca.datatypes.Credential + required: false + +tosca.relationships.AttachesTo: + derived_from: tosca.relationships.Root + valid_target_types: [ tosca.capabilities.Attachment ] + properties: + location: + required: true + type: string + constraints: + - min_length: 1 + device: + required: false + type: string + +tosca.relationships.network.LinksTo: + derived_from: tosca.relationships.DependsOn + valid_target_types: [ tosca.capabilities.network.Linkable ] + +tosca.relationships.network.BindsTo: + derived_from: tosca.relationships.DependsOn + valid_target_types: [ tosca.capabilities.network.Bindable ] + +########################################################################## +# Capability Type. +# A Capability Type is a reusable entity that describes a kind of +# capability that a Node Type can declare to expose. +########################################################################## +tosca.capabilities.Root: + description: > + The TOSCA root Capability Type all other TOSCA base Capability Types + derive from. + +tosca.capabilities.Node: + derived_from: tosca.capabilities.Root + +tosca.capabilities.Container: + derived_from: tosca.capabilities.Root + properties: + num_cpus: + required: no + type: integer + constraints: + - greater_or_equal: 1 + cpu_frequency: + required: no + type: scalar-unit.frequency + constraints: + - greater_or_equal: 0.1 GHz + disk_size: + required: no + type: scalar-unit.size + constraints: + - greater_or_equal: 0 MB + mem_size: + required: no + type: scalar-unit.size + constraints: + - greater_or_equal: 0 MB + +tosca.capabilities.Endpoint: + derived_from: tosca.capabilities.Root + properties: + protocol: + type: string + default: tcp + port: + type: tosca.datatypes.network.PortDef + required: false + secure: + type: boolean + default: false + url_path: + type: string + required: false + port_name: + type: string + required: false + network_name: + type: string + required: false + initiator: + type: string + default: source + constraints: + - valid_values: [source, target, peer] + ports: + type: map + required: false + constraints: + - min_length: 1 + entry_schema: + type: tosca.datatypes.network.PortDef + attributes: + ip_address: + type: string + +tosca.capabilities.Endpoint.Admin: + derived_from: tosca.capabilities.Endpoint + properties: + secure: true + +tosca.capabilities.Scalable: + derived_from: tosca.capabilities.Root + properties: + min_instances: + type: integer + required: yes + default: 1 + description: > + This property is used to indicate the minimum number of instances + that should be created for the associated TOSCA Node Template by + a TOSCA orchestrator. + max_instances: + type: integer + required: yes + default: 1 + description: > + This property is used to indicate the maximum number of instances + that should be created for the associated TOSCA Node Template by + a TOSCA orchestrator. + default_instances: + type: integer + required: no + description: > + An optional property that indicates the requested default number + of instances that should be the starting number of instances a + TOSCA orchestrator should attempt to allocate. + The value for this property MUST be in the range between the values + set for min_instances and max_instances properties. + +tosca.capabilities.Endpoint.Database: + derived_from: tosca.capabilities.Endpoint + +tosca.capabilities.Attachment: + derived_from: tosca.capabilities.Root + +tosca.capabilities.network.Linkable: + derived_from: tosca.capabilities.Root + description: > + A node type that includes the Linkable capability indicates that it can + be pointed by tosca.relationships.network.LinksTo relationship type, which + represents an association relationship between Port and Network node types. + +tosca.capabilities.network.Bindable: + derived_from: tosca.capabilities.Root + description: > + A node type that includes the Bindable capability indicates that it can + be pointed by tosca.relationships.network.BindsTo relationship type, which + represents a network association relationship between Port and Compute node + types. + +tosca.capabilities.OperatingSystem: + derived_from: tosca.capabilities.Root + properties: + architecture: + required: false + type: string + description: > + The host Operating System (OS) architecture. + type: + required: false + type: string + description: > + The host Operating System (OS) type. + distribution: + required: false + type: string + description: > + The host Operating System (OS) distribution. Examples of valid values + for an “type” of “Linux” would include: + debian, fedora, rhel and ubuntu. + version: + required: false + type: version + description: > + The host Operating System version. + +########################################################################## + # Interfaces Type. + # The Interfaces element describes a list of one or more interface + # definitions for a modelable entity (e.g., a Node or Relationship Type) + # as defined within the TOSCA Simple Profile specification. +########################################################################## +tosca.interfaces.node.lifecycle.Standard: + create: + description: Standard lifecycle create operation. + configure: + description: Standard lifecycle configure operation. + start: + description: Standard lifecycle start operation. + stop: + description: Standard lifecycle stop operation. + delete: + description: Standard lifecycle delete operation. + +tosca.interfaces.relationship.Configure: + pre_configure_source: + description: Operation to pre-configure the source endpoint. + pre_configure_target: + description: Operation to pre-configure the target endpoint. + post_configure_source: + description: Operation to post-configure the source endpoint. + post_configure_target: + description: Operation to post-configure the target endpoint. + add_target: + description: Operation to add a target node. + remove_target: + description: Operation to remove a target node. + add_source: > + description: Operation to notify the target node of a source node which + is now available via a relationship. + description: + target_changed: > + description: Operation to notify source some property or attribute of the + target changed + +########################################################################## + # Data Type. + # A Datatype is a complex data type declaration which contains other + # complex or simple data types. +########################################################################## +tosca.datatypes.network.NetworkInfo: + properties: + network_name: + type: string + network_id: + type: string + addresses: + type: list + entry_schema: + type: string + +tosca.datatypes.network.PortInfo: + properties: + port_name: + type: string + port_id: + type: string + network_id: + type: string + mac_address: + type: string + addresses: + type: list + entry_schema: + type: string + +tosca.datatypes.network.PortDef: + type: integer + constraints: + - in_range: [ 1, 65535 ] + +tosca.datatypes.network.PortSpec: + properties: + protocol: + type: string + required: true + default: tcp + constraints: + - valid_values: [ udp, tcp, igmp ] + target: + type: list + entry_schema: + type: PortDef + target_range: + type: range + constraints: + - in_range: [ 1, 65535 ] + source: + type: list + entry_schema: + type: PortDef + source_range: + type: range + constraints: + - in_range: [ 1, 65535 ] + +tosca.datatypes.Credential: + properties: + protocol: + type: string + token_type: + type: string + token: + type: string + keys: + type: map + entry_schema: + type: string + user: + type: string + required: false + +########################################################################## + # Artifact Type. + # An Artifact Type is a reusable entity that defines the type of one or more + # files which Node Types or Node Templates can have dependent relationships + # and used during operations such as during installation or deployment. +########################################################################## +tosca.artifacts.Root: + description: > + The TOSCA Artifact Type all other TOSCA Artifact Types derive from + properties: + version: version + +tosca.artifacts.File: + derived_from: tosca.artifacts.Root + +tosca.artifacts.Deployment: + derived_from: tosca.artifacts.Root + description: TOSCA base type for deployment artifacts + +tosca.artifacts.Deployment.Image: + derived_from: tosca.artifacts.Deployment + +tosca.artifacts.Deployment.Image.VM: + derived_from: tosca.artifacts.Deployment.Image + +tosca.artifacts.Implementation: + derived_from: tosca.artifacts.Root + description: TOSCA base type for implementation artifacts + +tosca.artifacts.Implementation.Bash: + derived_from: tosca.artifacts.Implementation + description: Script artifact for the Unix Bash shell + mime_type: application/x-sh + file_ext: [ sh ] + +tosca.artifacts.Implementation.Python: + derived_from: tosca.artifacts.Implementation + description: Artifact for the interpreted Python language + mime_type: application/x-python + file_ext: [ py ] + +tosca.artifacts.Deployment.Image.Container.Docker: + derived_from: tosca.artifacts.Deployment.Image + description: Docker container image + +tosca.artifacts.Deployment.Image.VM.ISO: + derived_from: tosca.artifacts.Deployment.Image + description: Virtual Machine (VM) image in ISO disk format + mime_type: application/octet-stream + file_ext: [ iso ] + +tosca.artifacts.Deployment.Image.VM.QCOW2: + derived_from: tosca.artifacts.Deployment.Image + description: Virtual Machine (VM) image in QCOW v2 standard disk format + mime_type: application/octet-stream + file_ext: [ qcow2 ] + +########################################################################## + # Policy Type. + # TOSCA Policy Types represent logical grouping of TOSCA nodes that have + # an implied relationship and need to be orchestrated or managed together + # to achieve some result. +########################################################################## +tosca.policies.Root: + description: The TOSCA Policy Type all other TOSCA Policy Types derive from. + +tosca.policies.Placement: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to govern + placement of TOSCA nodes or groups of nodes. + +tosca.policies.Scaling: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to govern + scaling of TOSCA nodes or groups of nodes. + +tosca.policies.Update: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to govern + update of TOSCA nodes or groups of nodes. + +tosca.policies.Performance: + derived_from: tosca.policies.Root + description: The TOSCA Policy Type definition that is used to declare + performance requirements for TOSCA nodes or groups of nodes. + +# Miguel: new types + +tosca.nodes.Database.MySQL: + derived_from: tosca.nodes.Database + properties: + password: + type: string + required: true + name: + type: string + required: true + user: + type: string + required: true + root_password: + type: string + required: true + requirements: + - host: + capability: tosca.capabilities.Container + relationship: tosca.relationships.HostedOn + node: tosca.nodes.DBMS.MySQL + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_configure.yml + inputs: + password: { get_property: [ SELF, password ] } + name: { get_property: [ SELF, name ] } + user: { get_property: [ SELF, user ] } + root_password: { get_property: [ SELF, root_password ] } + + +tosca.nodes.DBMS.MySQL: + derived_from: tosca.nodes.DBMS + properties: + port: + type: integer + description: reflect the default MySQL server port + default: 3306 + root_password: + type: string + # MySQL requires a root_password for configuration + required: true + capabilities: + # Further constrain the ‘host’ capability to only allow MySQL databases + host: + type: tosca.capabilities.Container + valid_source_types: [ tosca.nodes.Database.MySQL ] + interfaces: + Standard: + create: mysql/mysql_install.yml + configure: + implementation: mysql/mysql_configure.yml + inputs: + root_password: { get_property: [ SELF, root_password ] } + port: { get_property: [ SELF, port ] } + +tosca.nodes.WebServer.Apache: + derived_from: tosca.nodes.WebServer + interfaces: + Standard: + create: apache/apache_install.yml + +# INDIGO non normative types + +tosca.nodes.indigo.GalaxyPortal: + derived_from: tosca.nodes.WebServer + properties: + admin: + type: string + description: email of the admin user + default: admin@admin.com + required: false + admin_api_key: + type: string + description: key to access the API with admin role + default: not_very_secret_api_key + required: false + user: + type: string + description: username to launch the galaxy daemon + default: galaxy + required: false + install_path: + type: string + description: path to install the galaxy tool + default: /home/galaxy/galaxy + required: false + interfaces: + Standard: + create: + implementation: galaxy/galaxy_install.yml + inputs: + galaxy_install_path: { get_property: [ SELF, install_path ] } + configure: + implementation: galaxy/galaxy_configure.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + galaxy_admin: { get_property: [ SELF, admin ] } + galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } + start: + implementation: galaxy/galaxy_start.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + + +tosca.nodes.indigo.GalaxyTool: + derived_from: tosca.nodes.WebApplication + properties: + name: + type: string + description: name of the tool + required: true + owner: + type: string + description: developer of the tool + required: true + tool_panel_section_id: + type: string + description: panel section to install the tool + required: true + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.indigo.GalaxyPortal + relationship: tosca.relationships.HostedOn + interfaces: + Standard: + create: + implementation: galaxy/galaxy_tools_configure.yml + inputs: + galaxy_install_path: { get_property: [ HOST, install_path ] } + galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } + galaxy_tool_name: { get_property: [ SELF, name ] } + galaxy_tool_owner: { get_property: [ SELF, owner ] } + galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } diff --git a/IM/tosca/toscaparser/elements/__init__.py b/IM/tosca/toscaparser/elements/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/elements/artifacttype.py b/IM/tosca/toscaparser/elements/artifacttype.py new file mode 100644 index 000000000..e0897b3d7 --- /dev/null +++ b/IM/tosca/toscaparser/elements/artifacttype.py @@ -0,0 +1,45 @@ +# 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. + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class ArtifactTypeDef(StatefulEntityType): + '''TOSCA built-in artifacts type.''' + + def __init__(self, atype, custom_def=None): + super(ArtifactTypeDef, self).__init__(atype, self.ARTIFACT_PREFIX, + custom_def) + self.type = atype + self.properties = None + if self.PROPERTIES in self.defs: + self.properties = self.defs[self.PROPERTIES] + self.parent_artifacts = self._get_parent_artifacts() + + def _get_parent_artifacts(self): + artifacts = {} + parent_artif = self.parent_type + if parent_artif: + while parent_artif != 'tosca.artifacts.Root': + artifacts[parent_artif] = self.TOSCA_DEF[parent_artif] + parent_artif = artifacts[parent_artif]['derived_from'] + return artifacts + + @property + def parent_type(self): + '''Return an artifact this artifact is derived from.''' + return self.derived_from(self.defs) + + def get_artifact(self, name): + '''Return the definition of an artifact field by name.''' + if name in self.defs: + return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/attribute_definition.py b/IM/tosca/toscaparser/elements/attribute_definition.py new file mode 100644 index 000000000..35ba27f22 --- /dev/null +++ b/IM/tosca/toscaparser/elements/attribute_definition.py @@ -0,0 +1,20 @@ +# 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. + + +class AttributeDef(object): + '''TOSCA built-in Attribute type.''' + + def __init__(self, name, value=None, schema=None): + self.name = name + self.value = value + self.schema = schema diff --git a/IM/tosca/toscaparser/elements/capabilitytype.py b/IM/tosca/toscaparser/elements/capabilitytype.py new file mode 100644 index 000000000..b1bd7d767 --- /dev/null +++ b/IM/tosca/toscaparser/elements/capabilitytype.py @@ -0,0 +1,71 @@ +# 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. + +from IM.tosca.toscaparser.elements.property_definition import PropertyDef +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class CapabilityTypeDef(StatefulEntityType): + '''TOSCA built-in capabilities type.''' + + def __init__(self, name, ctype, ntype, custom_def=None): + self.name = name + super(CapabilityTypeDef, self).__init__(ctype, self.CAPABILITY_PREFIX, + custom_def) + self.nodetype = ntype + self.properties = None + if self.PROPERTIES in self.defs: + self.properties = self.defs[self.PROPERTIES] + self.parent_capabilities = self._get_parent_capabilities() + + def get_properties_def_objects(self): + '''Return a list of property definition objects.''' + properties = [] + parent_properties = {} + if self.parent_capabilities: + for type, value in self.parent_capabilities.items(): + parent_properties[type] = value.get('properties') + if self.properties: + for prop, schema in self.properties.items(): + # Miguel: Cambios aqui + if isinstance(schema, dict): + properties.append(PropertyDef(prop, None, schema)) + if parent_properties: + for parent, props in parent_properties.items(): + for prop, schema in props.items(): + properties.append(PropertyDef(prop, None, schema)) + return properties + + def get_properties_def(self): + '''Return a dictionary of property definition name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_def_objects()} + + def get_property_def_value(self, name): + '''Return the definition of a given property name.''' + props_def = self.get_properties_def() + if props_def and name in props_def: + return props_def[name].value + + def _get_parent_capabilities(self): + capabilities = {} + parent_cap = self.parent_type + if parent_cap: + while parent_cap != 'tosca.capabilities.Root': + capabilities[parent_cap] = self.TOSCA_DEF[parent_cap] + parent_cap = capabilities[parent_cap]['derived_from'] + return capabilities + + @property + def parent_type(self): + '''Return a capability this capability is derived from.''' + return self.derived_from(self.defs) diff --git a/IM/tosca/toscaparser/elements/constraints.py b/IM/tosca/toscaparser/elements/constraints.py new file mode 100644 index 000000000..2f38eeffd --- /dev/null +++ b/IM/tosca/toscaparser/elements/constraints.py @@ -0,0 +1,569 @@ +# 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. + +import collections +import datetime +import re + +from IM.tosca.toscaparser.common.exception import InvalidSchemaError +from IM.tosca.toscaparser.common.exception import ValidationError +from IM.tosca.toscaparser.elements import scalarunit +from IM.tosca.toscaparser.functions import is_function +from IM.tosca.toscaparser.utils.gettextutils import _ + + +class Schema(collections.Mapping): + + KEYS = ( + TYPE, REQUIRED, DESCRIPTION, + DEFAULT, CONSTRAINTS, ENTRYSCHEMA + ) = ( + 'type', 'required', 'description', + 'default', 'constraints', 'entry_schema' + ) + + PROPERTY_TYPES = ( + INTEGER, STRING, BOOLEAN, FLOAT, + NUMBER, TIMESTAMP, LIST, MAP, + SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME, + PORTDEF, VERSION + ) = ( + 'integer', 'string', 'boolean', 'float', + 'number', 'timestamp', 'list', 'map', + 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time', + 'PortDef', 'version' + ) + + SCALAR_UNIT_SIZE_DEFAULT = 'B' + SCALAR_UNIT_SIZE_DICT = {'B': 1, 'KB': 1000, 'KIB': 1024, 'MB': 1000000, + 'MIB': 1048576, 'GB': 1000000000, + 'GIB': 1073741824, 'TB': 1000000000000, + 'TIB': 1099511627776} + + def __init__(self, name, schema_dict): + self.name = name + if not isinstance(schema_dict, collections.Mapping): + msg = _("Schema %(pname)s must be a dict.") % dict(pname=name) + raise InvalidSchemaError(message=msg) + + try: + schema_dict['type'] + except KeyError: + msg = _("Schema %(pname)s must have type.") % dict(pname=name) + raise InvalidSchemaError(message=msg) + + self.schema = schema_dict + self._len = None + self.constraints_list = [] + + @property + def type(self): + return self.schema[self.TYPE] + + @property + def required(self): + return self.schema.get(self.REQUIRED, True) + + @property + def description(self): + return self.schema.get(self.DESCRIPTION, '') + + @property + def default(self): + return self.schema.get(self.DEFAULT) + + @property + def constraints(self): + if not self.constraints_list: + constraint_schemata = self.schema.get(self.CONSTRAINTS) + if constraint_schemata: + self.constraints_list = [Constraint(self.name, + self.type, + cschema) + for cschema in constraint_schemata] + return self.constraints_list + + @property + def entry_schema(self): + return self.schema.get(self.ENTRYSCHEMA) + + def __getitem__(self, key): + return self.schema[key] + + def __iter__(self): + for k in self.KEYS: + try: + self.schema[k] + except KeyError: + pass + else: + yield k + + def __len__(self): + if self._len is None: + self._len = len(list(iter(self))) + return self._len + + +class Constraint(object): + '''Parent class for constraints for a Property or Input.''' + + CONSTRAINTS = (EQUAL, GREATER_THAN, + GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_RANGE, + VALID_VALUES, LENGTH, MIN_LENGTH, MAX_LENGTH, PATTERN) = \ + ('equal', 'greater_than', 'greater_or_equal', 'less_than', + 'less_or_equal', 'in_range', 'valid_values', 'length', + 'min_length', 'max_length', 'pattern') + + def __new__(cls, property_name, property_type, constraint): + if cls is not Constraint: + return super(Constraint, cls).__new__(cls) + + if(not isinstance(constraint, collections.Mapping) or + len(constraint) != 1): + raise InvalidSchemaError(message=_('Invalid constraint schema.')) + + for type in constraint.keys(): + ConstraintClass = get_constraint_class(type) + if not ConstraintClass: + msg = _('Invalid constraint type "%s".') % type + raise InvalidSchemaError(message=msg) + + return ConstraintClass(property_name, property_type, constraint) + + def __init__(self, property_name, property_type, constraint): + self.property_name = property_name + self.property_type = property_type + self.constraint_value = constraint[self.constraint_key] + self.constraint_value_msg = self.constraint_value + if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: + self.constraint_value = self._get_scalarunit_constraint_value() + # check if constraint is valid for property type + if property_type not in self.valid_prop_types: + msg = _('Constraint type "%(ctype)s" is not valid ' + 'for data type "%(dtype)s".') % dict( + ctype=self.constraint_key, + dtype=property_type) + raise InvalidSchemaError(message=msg) + + def _get_scalarunit_constraint_value(self): + if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: + ScalarUnit_Class = (scalarunit. + get_scalarunit_class(self.property_type)) + if isinstance(self.constraint_value, list): + return [ScalarUnit_Class(v).get_num_from_scalar_unit() + for v in self.constraint_value] + else: + return (ScalarUnit_Class(self.constraint_value). + get_num_from_scalar_unit()) + + def _err_msg(self, value): + return _('Property %s could not be validated.') % self.property_name + + def validate(self, value): + self.value_msg = value + if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: + value = scalarunit.get_scalarunit_value(self.property_type, value) + if not self._is_valid(value): + err_msg = self._err_msg(value) + raise ValidationError(message=err_msg) + + +class Equal(Constraint): + """Constraint class for "equal" + + Constrains a property or parameter to a value equal to ('=') + the value declared. + """ + + constraint_key = Constraint.EQUAL + + valid_prop_types = Schema.PROPERTY_TYPES + + def _is_valid(self, value): + if value == self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s is not equal to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class GreaterThan(Constraint): + """Constraint class for "greater_than" + + Constrains a property or parameter to a value greater than ('>') + the value declared. + """ + + constraint_key = Constraint.GREATER_THAN + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(GreaterThan, self).__init__(property_name, property_type, + constraint) + if not isinstance(constraint[self.GREATER_THAN], self.valid_types): + raise InvalidSchemaError(message=_('greater_than must ' + 'be comparable.')) + + def _is_valid(self, value): + if value > self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be greater than "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class GreaterOrEqual(Constraint): + """Constraint class for "greater_or_equal" + + Constrains a property or parameter to a value greater than or equal + to ('>=') the value declared. + """ + + constraint_key = Constraint.GREATER_OR_EQUAL + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(GreaterOrEqual, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('greater_or_equal must ' + 'be comparable.')) + + def _is_valid(self, value): + if is_function(value) or value >= self.constraint_value: + return True + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be greater or equal ' + 'to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class LessThan(Constraint): + """Constraint class for "less_than" + + Constrains a property or parameter to a value less than ('<') + the value declared. + """ + + constraint_key = Constraint.LESS_THAN + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(LessThan, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('less_than must ' + 'be comparable.')) + + def _is_valid(self, value): + if value < self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be less than "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class LessOrEqual(Constraint): + """Constraint class for "less_or_equal" + + Constrains a property or parameter to a value less than or equal + to ('<=') the value declared. + """ + + constraint_key = Constraint.LESS_OR_EQUAL + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(LessOrEqual, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('less_or_equal must ' + 'be comparable.')) + + def _is_valid(self, value): + if value <= self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s must be less or ' + 'equal to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=self.value_msg, + cvalue=self.constraint_value_msg)) + + +class InRange(Constraint): + """Constraint class for "in_range" + + Constrains a property or parameter to a value in range of (inclusive) + the two values declared. + """ + + constraint_key = Constraint.IN_RANGE + + valid_types = (int, float, datetime.date, + datetime.time, datetime.datetime) + + valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, + Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, + Schema.SCALAR_UNIT_TIME) + + def __init__(self, property_name, property_type, constraint): + super(InRange, self).__init__(property_name, property_type, constraint) + if(not isinstance(self.constraint_value, collections.Sequence) or + (len(constraint[self.IN_RANGE]) != 2)): + raise InvalidSchemaError(message=_('in_range must be a list.')) + + for value in self.constraint_value: + if not isinstance(value, self.valid_types): + raise InvalidSchemaError(_('in_range value must ' + 'be comparable.')) + + self.min = self.constraint_value[0] + self.max = self.constraint_value[1] + + def _is_valid(self, value): + if value < self.min: + return False + if value > self.max: + return False + + return True + + def _err_msg(self, value): + return (_('%(pname)s: %(pvalue)s is out of range ' + '(min:%(vmin)s, max:%(vmax)s).') % + dict(pname=self.property_name, + pvalue=self.value_msg, + vmin=self.constraint_value_msg[0], + vmax=self.constraint_value_msg[1])) + + +class ValidValues(Constraint): + """Constraint class for "valid_values" + + Constrains a property or parameter to a value that is in the list of + declared values. + """ + constraint_key = Constraint.VALID_VALUES + + valid_prop_types = Schema.PROPERTY_TYPES + + def __init__(self, property_name, property_type, constraint): + super(ValidValues, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, collections.Sequence): + raise InvalidSchemaError(message=_('valid_values must be a list.')) + + def _is_valid(self, value): + if isinstance(value, list): + return all(v in self.constraint_value for v in value) + return value in self.constraint_value + + def _err_msg(self, value): + allowed = '[%s]' % ', '.join(str(a) for a in self.constraint_value) + return (_('%(pname)s: %(pvalue)s is not an valid ' + 'value "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=allowed)) + + +class Length(Constraint): + """Constraint class for "length" + + Constrains the property or parameter to a value of a given length. + """ + + constraint_key = Constraint.LENGTH + + valid_types = (int, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(Length, self).__init__(property_name, property_type, constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('length must be integer.')) + + def _is_valid(self, value): + if isinstance(value, str) and len(value) == self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('length of %(pname)s: %(pvalue)s must be equal ' + 'to "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +class MinLength(Constraint): + """Constraint class for "min_length" + + Constrains the property or parameter to a value to a minimum length. + """ + + constraint_key = Constraint.MIN_LENGTH + + valid_types = (int, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(MinLength, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('min_length must be integer.')) + + def _is_valid(self, value): + if isinstance(value, str) and len(value) >= self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('length of %(pname)s: %(pvalue)s must be ' + 'at least "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +class MaxLength(Constraint): + """Constraint class for "max_length" + + Constrains the property or parameter to a value to a maximum length. + """ + + constraint_key = Constraint.MAX_LENGTH + + valid_types = (int, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(MaxLength, self).__init__(property_name, property_type, + constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('max_length must be integer.')) + + def _is_valid(self, value): + if isinstance(value, str) and len(value) <= self.constraint_value: + return True + + return False + + def _err_msg(self, value): + return (_('length of %(pname)s: %(pvalue)s must be no greater ' + 'than "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +class Pattern(Constraint): + """Constraint class for "pattern" + + Constrains the property or parameter to a value that is allowed by + the provided regular expression. + """ + + constraint_key = Constraint.PATTERN + + valid_types = (str, ) + + valid_prop_types = (Schema.STRING, ) + + def __init__(self, property_name, property_type, constraint): + super(Pattern, self).__init__(property_name, property_type, constraint) + if not isinstance(self.constraint_value, self.valid_types): + raise InvalidSchemaError(message=_('pattern must be string.')) + self.match = re.compile(self.constraint_value).match + + def _is_valid(self, value): + match = self.match(value) + return match is not None and match.end() == len(value) + + def _err_msg(self, value): + return (_('%(pname)s: "%(pvalue)s" does not match ' + 'pattern "%(cvalue)s".') % + dict(pname=self.property_name, + pvalue=value, + cvalue=self.constraint_value)) + + +constraint_mapping = { + Constraint.EQUAL: Equal, + Constraint.GREATER_THAN: GreaterThan, + Constraint.GREATER_OR_EQUAL: GreaterOrEqual, + Constraint.LESS_THAN: LessThan, + Constraint.LESS_OR_EQUAL: LessOrEqual, + Constraint.IN_RANGE: InRange, + Constraint.VALID_VALUES: ValidValues, + Constraint.LENGTH: Length, + Constraint.MIN_LENGTH: MinLength, + Constraint.MAX_LENGTH: MaxLength, + Constraint.PATTERN: Pattern + } + + +def get_constraint_class(type): + return constraint_mapping.get(type) diff --git a/IM/tosca/toscaparser/elements/datatype.py b/IM/tosca/toscaparser/elements/datatype.py new file mode 100644 index 000000000..e66c3b79c --- /dev/null +++ b/IM/tosca/toscaparser/elements/datatype.py @@ -0,0 +1,56 @@ +# 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. + + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class DataType(StatefulEntityType): + '''TOSCA built-in and user defined complex data type.''' + + def __init__(self, datatypename, custom_def=None): + super(DataType, self).__init__(datatypename, self.DATATYPE_PREFIX, + custom_def) + self.custom_def = custom_def + + @property + def parent_type(self): + '''Return a datatype this datatype is derived from.''' + ptype = self.derived_from(self.defs) + if ptype: + return DataType(ptype, self.custom_def) + return None + + @property + def value_type(self): + '''Return 'type' section in the datatype schema.''' + return self.entity_value(self.defs, 'type') + + def get_all_properties_objects(self): + '''Return all properties objects defined in type and parent type.''' + props_def = self.get_properties_def_objects() + ptype = self.parent_type + while ptype: + props_def.extend(ptype.get_properties_def_objects()) + ptype = ptype.parent_type + return props_def + + def get_all_properties(self): + '''Return a dictionary of all property definition name-object pairs.''' + return {prop.name: prop + for prop in self.get_all_properties_objects()} + + def get_all_property_value(self, name): + '''Return the value of a given property name.''' + props_def = self.get_all_properties() + if props_def and name in props_def.key(): + return props_def[name].value diff --git a/IM/tosca/toscaparser/elements/entity_type.py b/IM/tosca/toscaparser/elements/entity_type.py new file mode 100644 index 000000000..241556093 --- /dev/null +++ b/IM/tosca/toscaparser/elements/entity_type.py @@ -0,0 +1,113 @@ +# 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. + +import logging +import os +import IM.tosca.toscaparser.utils.yamlparser + +log = logging.getLogger('tosca') + + +class EntityType(object): + '''Base class for TOSCA elements.''' + + SECTIONS = (DERIVED_FROM, PROPERTIES, ATTRIBUTES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE, ARTIFACTS) = \ + ('derived_from', 'properties', 'attributes', 'requirements', + 'interfaces', 'capabilities', 'type', 'artifacts') + + '''TOSCA definition file.''' + TOSCA_DEF_FILE = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "TOSCA_definition_1_0.yaml") + + loader = IM.tosca.toscaparser.utils.yamlparser.load_yaml + + TOSCA_DEF = loader(TOSCA_DEF_FILE) + + RELATIONSHIP_TYPE = (DEPENDSON, HOSTEDON, CONNECTSTO, ATTACHESTO, + LINKSTO, BINDSTO) = \ + ('tosca.relationships.DependsOn', + 'tosca.relationships.HostedOn', + 'tosca.relationships.ConnectsTo', + 'tosca.relationships.AttachesTo', + 'tosca.relationships.network.LinksTo', + 'tosca.relationships.network.BindsTo') + + NODE_PREFIX = 'tosca.nodes.' + RELATIONSHIP_PREFIX = 'tosca.relationships.' + CAPABILITY_PREFIX = 'tosca.capabilities.' + INTERFACE_PREFIX = 'tosca.interfaces.' + ARTIFACT_PREFIX = 'tosca.artifacts.' + POLICY_PREFIX = 'tosca.policies.' + # currently the data types are defined only for network + # but may have changes in the future. + DATATYPE_PREFIX = 'tosca.datatypes.network.' + TOSCA = 'tosca' + + def derived_from(self, defs): + '''Return a type this type is derived from.''' + return self.entity_value(defs, 'derived_from') + + def is_derived_from(self, type_str): + '''Check if object inherits from the given type. + + Returns true if this object is derived from 'type_str'. + False otherwise. + ''' + if not self.type: + return False + elif self.type == type_str: + return True + elif self.parent_type: + return self.parent_type.is_derived_from(type_str) + else: + return False + + def entity_value(self, defs, key): + if key in defs: + return defs[key] + + def get_value(self, ndtype, defs=None, parent=None): + value = None + if defs is None: + defs = self.defs + if ndtype in defs: + value = defs[ndtype] + if parent and not value: + p = self.parent_type + while value is None: + # check parent node + if not p: + break + if p and p.type == 'tosca.nodes.Root': + break + value = p.get_value(ndtype) + p = p.parent_type + return value + + def get_definition(self, ndtype): + value = None + defs = self.defs + if ndtype in defs: + value = defs[ndtype] + p = self.parent_type + if p: + inherited = p.get_definition(ndtype) + if inherited: + inherited = dict(inherited) + if not value: + value = inherited + else: + inherited.update(value) + value.update(inherited) + return value diff --git a/IM/tosca/toscaparser/elements/interfaces.py b/IM/tosca/toscaparser/elements/interfaces.py new file mode 100644 index 000000000..b763294f8 --- /dev/null +++ b/IM/tosca/toscaparser/elements/interfaces.py @@ -0,0 +1,74 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + +SECTIONS = (LIFECYCLE, CONFIGURE, LIFECYCLE_SHORTNAME, + CONFIGURE_SHORTNAME) = \ + ('tosca.interfaces.node.lifecycle.Standard', + 'tosca.interfaces.relationship.Configure', + 'Standard', 'Configure') + +INTERFACEVALUE = (IMPLEMENTATION, INPUTS) = ('implementation', 'inputs') + + +class InterfacesDef(StatefulEntityType): + '''TOSCA built-in interfaces type.''' + + def __init__(self, node_type, interfacetype, + node_template=None, name=None, value=None): + self.ntype = node_type + self.node_template = node_template + self.type = interfacetype + self.name = name + self.value = value + self.implementation = None + self.inputs = None + self.defs = {} + if interfacetype == LIFECYCLE_SHORTNAME: + interfacetype = LIFECYCLE + if interfacetype == CONFIGURE_SHORTNAME: + interfacetype = CONFIGURE + if node_type: + self.defs = self.TOSCA_DEF[interfacetype] + if value: + if isinstance(self.value, dict): + for i, j in self.value.items(): + if i == IMPLEMENTATION: + self.implementation = j + elif i == INPUTS: + self.inputs = j + else: + what = ('Interfaces of template %s' % + self.node_template.name) + raise UnknownFieldError(what=what, field=i) + else: + self.implementation = value + + @property + def lifecycle_ops(self): + if self.defs: + if self.type == LIFECYCLE: + return self._ops() + + @property + def configure_ops(self): + if self.defs: + if self.type == CONFIGURE: + return self._ops() + + def _ops(self): + ops = [] + for name in list(self.defs.keys()): + ops.append(name) + return ops diff --git a/IM/tosca/toscaparser/elements/nodetype.py b/IM/tosca/toscaparser/elements/nodetype.py new file mode 100644 index 000000000..f0ee53453 --- /dev/null +++ b/IM/tosca/toscaparser/elements/nodetype.py @@ -0,0 +1,200 @@ +# 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. + +from IM.tosca.toscaparser.elements.capabilitytype import CapabilityTypeDef +import IM.tosca.toscaparser.elements.interfaces as ifaces +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class NodeType(StatefulEntityType): + '''TOSCA built-in node type.''' + + def __init__(self, ntype, custom_def=None): + super(NodeType, self).__init__(ntype, self.NODE_PREFIX, custom_def) + self.custom_def = custom_def + + @property + def parent_type(self): + '''Return a node this node is derived from.''' + pnode = self.derived_from(self.defs) + if pnode: + return NodeType(pnode) + + @property + def relationship(self): + '''Return a dictionary of relationships to other node types. + + This method returns a dictionary of named relationships that nodes + of the current node type (self) can have to other nodes (of specific + types) in a TOSCA template. + + ''' + relationship = {} + requires = self.get_all_requirements() + if requires: + # NOTE(sdmonov): Check if requires is a dict. + # If it is a dict convert it to a list of dicts. + # This is needed because currently the code below supports only + # lists as requirements definition. The following check will + # make sure if a map (dict) was provided it will be converted to + # a list before proceeding to the parsing. + if isinstance(requires, dict): + requires = [{key: value} for key, value in requires.items()] + + keyword = None + node_type = None + for require in requires: + for key, req in require.items(): + if 'relationship' in req: + relation = req.get('relationship') + if 'type' in relation: + relation = relation.get('type') + node_type = req.get('node') + value = req + if node_type: + keyword = 'node' + else: + # If value is a dict and has a type key + # we need to lookup the node type using + # the capability type + value = req + if isinstance(value, dict): + captype = value['capability'] + value = (self. + _get_node_type_by_cap(key, captype)) + relation = self._get_relation(key, value) + keyword = key + node_type = value + rtype = RelationshipType(relation, keyword, req) + relatednode = NodeType(node_type, self.custom_def) + relationship[rtype] = relatednode + return relationship + + def _get_node_type_by_cap(self, key, cap): + '''Find the node type that has the provided capability + + This method will lookup all node types if they have the + provided capability. + ''' + + # Filter the node types + node_types = [node_type for node_type in self.TOSCA_DEF.keys() + if node_type.startswith(self.NODE_PREFIX) and + node_type != 'tosca.nodes.Root'] + + for node_type in node_types: + node_def = self.TOSCA_DEF[node_type] + if isinstance(node_def, dict) and 'capabilities' in node_def: + node_caps = node_def['capabilities'] + for value in node_caps.values(): + if isinstance(value, dict) and \ + 'type' in value and value['type'] == cap: + return node_type + + def _get_relation(self, key, ndtype): + relation = None + ntype = NodeType(ndtype) + caps = ntype.get_capabilities() + if caps and key in caps.keys(): + c = caps[key] + for r in self.RELATIONSHIP_TYPE: + rtypedef = ntype.TOSCA_DEF[r] + for properties in rtypedef.values(): + if c.type in properties: + relation = r + break + if relation: + break + else: + for properties in rtypedef.values(): + if c.parent_type in properties: + relation = r + break + return relation + + def get_capabilities_objects(self): + '''Return a list of capability objects.''' + typecapabilities = [] + caps = self.get_value(self.CAPABILITIES) + if caps is None: + caps = self.get_value(self.CAPABILITIES, None, True) + if caps: + for name, value in caps.items(): + ctype = value.get('type') + cap = CapabilityTypeDef(name, ctype, self.type, + self.custom_def) + typecapabilities.append(cap) + return typecapabilities + + def get_capabilities(self): + '''Return a dictionary of capability name-objects pairs.''' + return {cap.name: cap + for cap in self.get_capabilities_objects()} + + @property + def requirements(self): + return self.get_value(self.REQUIREMENTS) + + def get_all_requirements(self): + requires = self.requirements + parent_node = self.parent_type + if requires is None: + requires = self.get_value(self.REQUIREMENTS, None, True) + parent_node = parent_node.parent_type + if parent_node: + while parent_node.type != 'tosca.nodes.Root': + req = parent_node.get_value(self.REQUIREMENTS, None, True) + for r in req: + if r not in requires: + requires.append(r) + parent_node = parent_node.parent_type + return requires + + @property + def interfaces(self): + return self.get_value(self.INTERFACES) + + @property + def lifecycle_inputs(self): + '''Return inputs to life cycle operations if found.''' + inputs = [] + interfaces = self.interfaces + if interfaces: + for name, value in interfaces.items(): + if name == ifaces.LIFECYCLE: + for x, y in value.items(): + if x == 'inputs': + for i in y.iterkeys(): + inputs.append(i) + return inputs + + @property + def lifecycle_operations(self): + '''Return available life cycle operations if found.''' + ops = None + interfaces = self.interfaces + if interfaces: + i = InterfacesDef(self.type, ifaces.LIFECYCLE) + ops = i.lifecycle_ops + return ops + + def get_capability(self, name): + caps = self.get_capabilities() + if caps and name in caps.keys(): + return caps[name].value + + def get_capability_type(self, name): + captype = self.get_capability(name) + if captype and name in captype.keys(): + return captype[name].value diff --git a/IM/tosca/toscaparser/elements/policytype.py b/IM/tosca/toscaparser/elements/policytype.py new file mode 100644 index 000000000..573e04509 --- /dev/null +++ b/IM/tosca/toscaparser/elements/policytype.py @@ -0,0 +1,45 @@ +# 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. + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class PolicyType(StatefulEntityType): + '''TOSCA built-in policies type.''' + + def __init__(self, ptype, custom_def=None): + super(PolicyType, self).__init__(ptype, self.POLICY_PREFIX, + custom_def) + self.type = ptype + self.properties = None + if self.PROPERTIES in self.defs: + self.properties = self.defs[self.PROPERTIES] + self.parent_policies = self._get_parent_policies() + + def _get_parent_policies(self): + policies = {} + parent_policy = self.parent_type + if parent_policy: + while parent_policy != 'tosca.policies.Root': + policies[parent_policy] = self.TOSCA_DEF[parent_policy] + parent_policy = policies[parent_policy]['derived_from'] + return policies + + @property + def parent_type(self): + '''Return a policy this policy is derived from.''' + return self.derived_from(self.defs) + + def get_policy(self, name): + '''Return the definition of a policy field by name.''' + if name in self.defs: + return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/property_definition.py b/IM/tosca/toscaparser/elements/property_definition.py new file mode 100644 index 000000000..c2c7f0089 --- /dev/null +++ b/IM/tosca/toscaparser/elements/property_definition.py @@ -0,0 +1,46 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import InvalidSchemaError +# Miguel: add import +from IM.tosca.toscaparser.utils.gettextutils import _ + +class PropertyDef(object): + '''TOSCA built-in Property type.''' + + def __init__(self, name, value=None, schema=None): + self.name = name + self.value = value + self.schema = schema + + try: + self.schema['type'] + except KeyError: + msg = (_("Property definition of %(pname)s must have type.") % + dict(pname=self.name)) + raise InvalidSchemaError(message=msg) + + @property + def required(self): + if self.schema: + for prop_key, prop_value in self.schema.items(): + if prop_key == 'required' and prop_value: + return True + return False + + @property + def default(self): + if self.schema: + for prop_key, prop_value in self.schema.items(): + if prop_key == 'default': + return prop_value + return None diff --git a/IM/tosca/toscaparser/elements/relationshiptype.py b/IM/tosca/toscaparser/elements/relationshiptype.py new file mode 100644 index 000000000..e45ee3d93 --- /dev/null +++ b/IM/tosca/toscaparser/elements/relationshiptype.py @@ -0,0 +1,33 @@ +# 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. + +from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType + + +class RelationshipType(StatefulEntityType): + '''TOSCA built-in relationship type.''' + def __init__(self, type, capability_name=None, custom_def=None): + super(RelationshipType, self).__init__(type, self.RELATIONSHIP_PREFIX, + custom_def) + self.capability_name = capability_name + self.custom_def = custom_def + + @property + def parent_type(self): + '''Return a relationship this reletionship is derived from.''' + prel = self.derived_from(self.defs) + if prel: + return RelationshipType(prel) + + @property + def valid_target_types(self): + return self.entity_value(self.defs, 'valid_target_types') diff --git a/IM/tosca/toscaparser/elements/scalarunit.py b/IM/tosca/toscaparser/elements/scalarunit.py new file mode 100644 index 000000000..836427085 --- /dev/null +++ b/IM/tosca/toscaparser/elements/scalarunit.py @@ -0,0 +1,130 @@ +# 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. + +import logging +import re + +from IM.tosca.toscaparser.utils.gettextutils import _ +from IM.tosca.toscaparser.utils import validateutils + +log = logging.getLogger('tosca') + + +class ScalarUnit(object): + '''Parent class for scalar-unit type.''' + + SCALAR_UNIT_TYPES = ( + SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME + ) = ( + 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time' + ) + + def __init__(self, value): + self.value = value + + def _check_unit_in_scalar_standard_units(self, input_unit): + """Check whether the input unit is following specified standard + + If unit is not following specified standard, convert it to standard + unit after displaying a warning message. + """ + if input_unit in self.SCALAR_UNIT_DICT.keys(): + return input_unit + else: + for key in self.SCALAR_UNIT_DICT.keys(): + if key.upper() == input_unit.upper(): + log.warning(_('Given unit %(unit)s does not follow scalar ' + 'unit standards; using %(key)s instead.') % { + 'unit': input_unit, 'key': key}) + return key + msg = (_('Provided unit "%(unit)s" is not valid. The valid units' + ' are %(valid_units)s') % {'unit': input_unit, + 'valid_units': sorted(self.SCALAR_UNIT_DICT.keys())}) + raise ValueError(msg) + + def validate_scalar_unit(self): + # Miguel: Cambios aqui + if self.value is None: + return None + regex = re.compile('([0-9.]+)\s*(\w+)') + try: + result = regex.match(str(self.value)).groups() + validateutils.str_to_num(result[0]) + scalar_unit = self._check_unit_in_scalar_standard_units(result[1]) + self.value = ' '.join([result[0], scalar_unit]) + return self.value + + except Exception: + raise ValueError(_('"%s" is not a valid scalar-unit') + % self.value) + + def get_num_from_scalar_unit(self, unit=None): + #Miguel: Cambios aqui + if self.value is None: + return None + if unit: + unit = self._check_unit_in_scalar_standard_units(unit) + else: + unit = self.SCALAR_UNIT_DEFAULT + self.validate_scalar_unit() + + regex = re.compile('([0-9.]+)\s*(\w+)') + result = regex.match(str(self.value)).groups() + converted = (float(validateutils.str_to_num(result[0])) + * self.SCALAR_UNIT_DICT[result[1]] + / self.SCALAR_UNIT_DICT[unit]) + if converted - int(converted) < 0.0000000000001: + converted = int(converted) + return converted + + +class ScalarUnit_Size(ScalarUnit): + + SCALAR_UNIT_DEFAULT = 'B' + SCALAR_UNIT_DICT = {'B': 1, 'kB': 1000, 'KiB': 1024, 'MB': 1000000, + 'MiB': 1048576, 'GB': 1000000000, + 'GiB': 1073741824, 'TB': 1000000000000, + 'TiB': 1099511627776} + + +class ScalarUnit_Time(ScalarUnit): + + SCALAR_UNIT_DEFAULT = 'ms' + SCALAR_UNIT_DICT = {'d': 86400, 'h': 3600, 'm': 60, 's': 1, + 'ms': 0.001, 'us': 0.000001, 'ns': 0.000000001} + + +class ScalarUnit_Frequency(ScalarUnit): + + SCALAR_UNIT_DEFAULT = 'GHz' + SCALAR_UNIT_DICT = {'Hz': 1, 'kHz': 1000, + 'MHz': 1000000, 'GHz': 1000000000} + + +scalarunit_mapping = { + ScalarUnit.SCALAR_UNIT_FREQUENCY: ScalarUnit_Frequency, + ScalarUnit.SCALAR_UNIT_SIZE: ScalarUnit_Size, + ScalarUnit.SCALAR_UNIT_TIME: ScalarUnit_Time, + } + + +def get_scalarunit_class(type): + return scalarunit_mapping.get(type) + + +def get_scalarunit_value(type, value, unit=None): + if type in ScalarUnit.SCALAR_UNIT_TYPES: + ScalarUnit_Class = get_scalarunit_class(type) + return (ScalarUnit_Class(value). + get_num_from_scalar_unit(unit)) + else: + raise TypeError(_('"%s" is not a valid scalar-unit type') % type) diff --git a/IM/tosca/toscaparser/elements/statefulentitytype.py b/IM/tosca/toscaparser/elements/statefulentitytype.py new file mode 100644 index 000000000..af820bd69 --- /dev/null +++ b/IM/tosca/toscaparser/elements/statefulentitytype.py @@ -0,0 +1,81 @@ +# 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. + +from IM.tosca.toscaparser.common.exception import InvalidTypeError +from IM.tosca.toscaparser.elements.attribute_definition import AttributeDef +from IM.tosca.toscaparser.elements.entity_type import EntityType +from IM.tosca.toscaparser.elements.property_definition import PropertyDef + + +class StatefulEntityType(EntityType): + '''Class representing TOSCA states.''' + + interfaces_node_lifecycle_operations = ['create', + 'configure', 'start', + 'stop', 'delete'] + + interfaces_relationship_confiure_operations = ['post_configure_source', + 'post_configure_target', + 'add_target', + 'remove_target'] + + def __init__(self, entitytype, prefix, custom_def=None): + entire_entitytype = entitytype + if not entitytype.startswith(self.TOSCA): + entire_entitytype = prefix + entitytype + if entire_entitytype in list(self.TOSCA_DEF.keys()): + self.defs = self.TOSCA_DEF[entire_entitytype] + entitytype = entire_entitytype + elif custom_def and entitytype in list(custom_def.keys()): + self.defs = custom_def[entitytype] + else: + raise InvalidTypeError(what=entitytype) + self.type = entitytype + + def get_properties_def_objects(self): + '''Return a list of property definition objects.''' + properties = [] + props = self.get_definition(self.PROPERTIES) + if props: + for prop, schema in props.items(): + properties.append(PropertyDef(prop, None, schema)) + return properties + + def get_properties_def(self): + '''Return a dictionary of property definition name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_def_objects()} + + def get_property_def_value(self, name): + '''Return the property definition associated with a given name.''' + props_def = self.get_properties_def() + if props_def and name in props_def.keys(): + return props_def[name].value + + def get_attributes_def_objects(self): + '''Return a list of attribute definition objects.''' + attrs = self.get_value(self.ATTRIBUTES) + if attrs: + return [AttributeDef(attr, None, schema) + for attr, schema in attrs.items()] + return [] + + def get_attributes_def(self): + '''Return a dictionary of attribute definition name-object pairs.''' + return {attr.name: attr + for attr in self.get_attributes_def_objects()} + + def get_attribute_def_value(self, name): + '''Return the attribute definition associated with a given name.''' + attrs_def = self.get_attributes_def() + if attrs_def and name in attrs_def.keys(): + return attrs_def[name].value diff --git a/IM/tosca/toscaparser/entity_template.py b/IM/tosca/toscaparser/entity_template.py new file mode 100644 index 000000000..f8f3ced25 --- /dev/null +++ b/IM/tosca/toscaparser/entity_template.py @@ -0,0 +1,285 @@ +# 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. + +from IM.tosca.toscaparser.capabilities import Capability +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.common.exception import ValidationError +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.elements.nodetype import NodeType +from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType +from IM.tosca.toscaparser.properties import Property + + +class EntityTemplate(object): + '''Base class for TOSCA templates.''' + + SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE, DESCRIPTION, DIRECTIVES, + ATTRIBUTES, ARTIFACTS, NODE_FILTER, COPY) = \ + ('derived_from', 'properties', 'requirements', 'interfaces', + 'capabilities', 'type', 'description', 'directives', + 'attributes', 'artifacts', 'node_filter', 'copy') + # Miguel: Add NODE_FILTER to the REQUIREMENTS_SECTION + REQUIREMENTS_SECTION = (NODE, CAPABILITY, RELATIONSHIP, OCCURRENCES, NODE_FILTER) = \ + ('node', 'capability', 'relationship', + 'occurrences','node_filter') + + def __init__(self, name, template, entity_name, custom_def=None): + self.name = name + self.entity_tpl = template + self.custom_def = custom_def + self._validate_field(self.entity_tpl) + if entity_name == 'node_type': + self.type_definition = NodeType(self.entity_tpl['type'], + custom_def) + if entity_name == 'relationship_type': + relationship = template.get('relationship') + type = None + if relationship and isinstance(relationship, dict): + type = relationship.get('type') + elif isinstance(relationship, str): + type = self.entity_tpl['relationship'] + else: + type = self.entity_tpl['type'] + self.type_definition = RelationshipType(type, + None, custom_def) + self._properties = None + self._interfaces = None + self._requirements = None + self._capabilities = None + + @property + def type(self): + return self.type_definition.type + + @property + def requirements(self): + if self._requirements is None: + self._requirements = self.type_definition.get_value( + self.REQUIREMENTS, + self.entity_tpl) or [] + return self._requirements + + def get_properties_objects(self): + '''Return properties objects for this template.''' + if self._properties is None: + self._properties = self._create_properties() + return self._properties + + def get_properties(self): + '''Return a dictionary of property name-object pairs.''' + return {prop.name: prop + for prop in self.get_properties_objects()} + + def get_property_value(self, name): + '''Return the value of a given property name.''' + props = self.get_properties() + if props and name in props.keys(): + return props[name].value + + @property + def interfaces(self): + #if self._interfaces is None: + if not self._interfaces: + self._interfaces = self._create_interfaces() + return self._interfaces + + def get_capabilities_objects(self): + '''Return capabilities objects for this template.''' + if not self._capabilities: + self._capabilities = self._create_capabilities() + return self._capabilities + + def get_capabilities(self): + '''Return a dictionary of capability name-object pairs.''' + return {cap.name: cap + for cap in self.get_capabilities_objects()} + + def is_derived_from(self, type_str): + '''Check if object inherits from the given type. + + Returns true if this object is derived from 'type_str'. + False otherwise. + ''' + if not self.type: + return False + elif self.type == type_str: + return True + elif self.parent_type: + return self.parent_type.is_derived_from(type_str) + else: + return False + + def _create_capabilities(self): + capability = [] + # Miguel: cambios aqui + caps = self.type_definition.get_value(self.CAPABILITIES, + self.entity_tpl, + self.type_definition) + if caps: + for name, props in caps.items(): + capabilities = self.type_definition.get_capabilities() + if name in capabilities.keys(): + c = capabilities[name] + if 'properties' in props: + cap = Capability(name, props['properties'], c) + else: + cap = Capability(name, [], c) + capability.append(cap) + return capability + + def _validate_properties(self, template, entitytype): + properties = entitytype.get_value(self.PROPERTIES, template) + self._common_validate_properties(entitytype, properties) + + def _validate_capabilities(self): + type_capabilities = self.type_definition.get_capabilities() + allowed_caps = \ + type_capabilities.keys() if type_capabilities else [] + capabilities = self.type_definition.get_value(self.CAPABILITIES, + self.entity_tpl) + if capabilities: + self._common_validate_field(capabilities, allowed_caps, + 'Capabilities') + self._validate_capabilities_properties(capabilities) + + def _validate_capabilities_properties(self, capabilities): + for cap, props in capabilities.items(): + capabilitydef = self.get_capability(cap).definition + self._common_validate_properties(capabilitydef, + props[self.PROPERTIES]) + + # validating capability properties values + for prop in self.get_capability(cap).get_properties_objects(): + prop.validate() + + # TODO(srinivas_tadepalli): temporary work around to validate + # default_instances until standardized in specification + if cap == "scalable" and prop.name == "default_instances": + prop_dict = props[self.PROPERTIES] + min_instances = prop_dict.get("min_instances") + max_instances = prop_dict.get("max_instances") + default_instances = prop_dict.get("default_instances") + if not (min_instances <= default_instances + <= max_instances): + err_msg = ("Properties of template %s : " + "default_instances value is not" + " between min_instances and " + "max_instances" % self.name) + raise ValidationError(message=err_msg) + + def _common_validate_properties(self, entitytype, properties): + allowed_props = [] + required_props = [] + for p in entitytype.get_properties_def_objects(): + allowed_props.append(p.name) + if p.required: + required_props.append(p.name) + if properties: + self._common_validate_field(properties, allowed_props, + 'Properties') + # make sure it's not missing any property required by a tosca type + missingprop = [] + for r in required_props: + if r not in properties.keys(): + missingprop.append(r) + if missingprop: + raise MissingRequiredFieldError( + what='Properties of template %s' % self.name, + required=missingprop) + else: + if required_props: + raise MissingRequiredFieldError( + what='Properties of template %s' % self.name, + required=missingprop) + + def _validate_field(self, template): + if not isinstance(template, dict): + raise MissingRequiredFieldError( + what='Template %s' % self.name, required=self.TYPE) + try: + relationship = template.get('relationship') + if relationship and not isinstance(relationship, str): + relationship[self.TYPE] + elif isinstance(relationship, str): + template['relationship'] + else: + template[self.TYPE] + except KeyError: + raise MissingRequiredFieldError( + what='Template %s' % self.name, required=self.TYPE) + + def _common_validate_field(self, schema, allowedlist, section): + for name in schema: + if name not in allowedlist: + raise UnknownFieldError( + what='%(section)s of template %(nodename)s' + % {'section': section, 'nodename': self.name}, + field=name) + + def _create_properties(self): + props = [] + properties = self.type_definition.get_value(self.PROPERTIES, + self.entity_tpl) or {} + for name, value in properties.items(): + props_def = self.type_definition.get_properties_def() + if props_def and name in props_def: + prop = Property(name, value, + props_def[name].schema, self.custom_def) + props.append(prop) + for p in self.type_definition.get_properties_def_objects(): + if p.default is not None and p.name not in properties.keys(): + prop = Property(p.name, p.default, p.schema, self.custom_def) + props.append(prop) + return props + + def _create_interfaces(self): + interfaces = [] + type_interfaces = None + if isinstance(self.type_definition, RelationshipType): + if isinstance(self.entity_tpl, dict): + # Miguel: cambios aqui + for key, value in self.entity_tpl.items(): + if key == 'interfaces': + type_interfaces = value + elif key != 'type': + rel = None + if isinstance(value, dict): + rel = value.get('relationship') + if rel: + if self.INTERFACES in rel: + type_interfaces = rel[self.INTERFACES] + break + else: + type_interfaces = self.type_definition.get_value(self.INTERFACES, + self.entity_tpl) + if type_interfaces: + for interface_type, value in type_interfaces.items(): + for op, op_def in value.items(): + iface = InterfacesDef(self.type_definition, + interfacetype=interface_type, + node_template=self, + name=op, + value=op_def) + interfaces.append(iface) + return interfaces + + def get_capability(self, name): + """Provide named capability + + :param name: name of capability + :return: capability object if found, None otherwise + """ + caps = self.get_capabilities() + if caps and name in caps.keys(): + return caps[name] diff --git a/IM/tosca/toscaparser/functions.py b/IM/tosca/toscaparser/functions.py new file mode 100644 index 000000000..5ecb905c9 --- /dev/null +++ b/IM/tosca/toscaparser/functions.py @@ -0,0 +1,410 @@ +# +# 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. + + +import abc +import six + +from IM.tosca.toscaparser.common.exception import UnknownInputError +from IM.tosca.toscaparser.utils.gettextutils import _ + + +GET_PROPERTY = 'get_property' +GET_ATTRIBUTE = 'get_attribute' +GET_INPUT = 'get_input' + +SELF = 'SELF' +HOST = 'HOST' + +HOSTED_ON = 'tosca.relationships.HostedOn' + + +@six.add_metaclass(abc.ABCMeta) +class Function(object): + """An abstract type for representing a Tosca template function.""" + + def __init__(self, tosca_tpl, context, name, args): + self.tosca_tpl = tosca_tpl + self.context = context + self.name = name + self.args = args + self.validate() + + @abc.abstractmethod + def result(self): + """Invokes the function and returns its result + + Some methods invocation may only be relevant on runtime (for example, + getting runtime properties) and therefore its the responsibility of + the orchestrator/translator to take care of such functions invocation. + + :return: Function invocation result. + """ + return {self.name: self.args} + + @abc.abstractmethod + def validate(self): + """Validates function arguments.""" + pass + + +class GetInput(Function): + """Get a property value declared within the input of the service template. + + Arguments: + + * Input name. + + Example: + + * get_input: port + """ + + def validate(self): + if len(self.args) != 1: + raise ValueError(_( + 'Expected one argument for get_input function but received: ' + '{0}.').format(self.args)) + inputs = [input.name for input in self.tosca_tpl.inputs] + if self.args[0] not in inputs: + raise UnknownInputError(input_name=self.args[0]) + + def result(self): + found_input = [input_def for input_def in self.tosca_tpl.inputs + if self.input_name == input_def.name][0] + return found_input.default + + @property + def input_name(self): + return self.args[0] + + +class GetAttribute(Function): + """Get an attribute value of an entity defined in the service template + + Node template attributes values are set in runtime and therefore its the + responsibility of the Tosca engine to implement the evaluation of + get_attribute functions. + + Arguments: + + * Node template name | HOST. + * Attribute name. + + If the HOST keyword is passed as the node template name argument the + function will search each node template along the HostedOn relationship + chain until a node which contains the attribute is found. + + Examples: + + * { get_attribute: [ server, private_address ] } + * { get_attribute: [ HOST, private_address ] } + """ + + def validate(self): + # Miguel: this is not true: + # { get_attribute: [ HOST, networks, private, addresses, 0 ] } + if len(self.args) != 2: + raise ValueError(_( + 'Illegal arguments for {0} function. Expected arguments: ' + 'node-template-name, attribute-name').format(GET_ATTRIBUTE)) + self._find_node_template_containing_attribute() + + def result(self): + return self.args + + def get_referenced_node_template(self): + """Gets the NodeTemplate instance the get_attribute function refers to. + + If HOST keyword was used as the node template argument, the node + template which contains the attribute along the HostedOn relationship + chain will be returned. + """ + return self._find_node_template_containing_attribute() + + def _find_node_template_containing_attribute(self): + if self.node_template_name == HOST: + # Currently this is the only way to tell whether the function + # is used within the outputs section of the TOSCA template. + if isinstance(self.context, list): + raise ValueError(_( + "get_attribute HOST keyword is not allowed within the " + "outputs section of the TOSCA template")) + node_tpl = self._find_host_containing_attribute() + if not node_tpl: + raise ValueError(_( + "get_attribute HOST keyword is used in '{0}' node " + "template but {1} was not found " + "in relationship chain").format(self.context.name, + HOSTED_ON)) + else: + node_tpl = self._find_node_template(self.args[0]) + if not self._attribute_exists_in_type(node_tpl.type_definition): + raise KeyError(_( + "Attribute '{0}' not found in node template: {1}.").format( + self.attribute_name, node_tpl.name)) + return node_tpl + + def _attribute_exists_in_type(self, type_definition): + attrs_def = type_definition.get_attributes_def() + found = [attrs_def[self.attribute_name]] \ + if self.attribute_name in attrs_def else [] + return len(found) == 1 + + def _find_host_containing_attribute(self, node_template_name=SELF): + node_template = self._find_node_template(node_template_name) + from IM.tosca.toscaparser.elements.entity_type import EntityType + hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] + for r in node_template.requirements: + for requirement, target_name in r.items(): + target_node = self._find_node_template(target_name) + target_type = target_node.type_definition + for capability in target_type.get_capabilities_objects(): + if capability.type in hosted_on_rel['valid_target_types']: + if self._attribute_exists_in_type(target_type): + return target_node + return self._find_host_containing_attribute( + target_name) + return None + + def _find_node_template(self, node_template_name): + name = self.context.name if node_template_name == SELF else \ + node_template_name + for node_template in self.tosca_tpl.nodetemplates: + if node_template.name == name: + return node_template + raise KeyError(_( + 'No such node template: {0}.').format(node_template_name)) + + @property + def node_template_name(self): + return self.args[0] + + @property + def attribute_name(self): + return self.args[1] + + +class GetProperty(Function): + """Get a property value of an entity defined in the same service template. + + Arguments: + + * Node template name. + * Requirement or capability name (optional). + * Property name. + + If requirement or capability name is specified, the behavior is as follows: + The req or cap name is first looked up in the specified node template's + requirements. + If found, it would search for a matching capability + of an other node template and get its property as specified in function + arguments. + Otherwise, the req or cap name would be looked up in the specified + node template's capabilities and if found, it would return the property of + the capability as specified in function arguments. + + Examples: + + * { get_property: [ mysql_server, port ] } + * { get_property: [ SELF, db_port ] } + * { get_property: [ SELF, database_endpoint, port ] } + """ + + def validate(self): + if len(self.args) < 2 or len(self.args) > 3: + raise ValueError(_( + 'Expected arguments: [node-template-name, req-or-cap ' + '(optional), property name.')) + if len(self.args) == 2: + prop = self._find_property(self.args[1]).value + if not isinstance(prop, Function): + get_function(self.tosca_tpl, self.context, prop) + elif len(self.args) == 3: + get_function(self.tosca_tpl, + self.context, + self._find_req_or_cap_property(self.args[1], + self.args[2])) + else: + raise NotImplementedError(_( + 'Nested properties are not supported.')) + + def _find_req_or_cap_property(self, req_or_cap, property_name): + node_tpl = self._find_node_template(self.args[0]) + # Find property in node template's requirements + for r in node_tpl.requirements: + for req, node_name in r.items(): + if req == req_or_cap: + node_template = self._find_node_template(node_name) + return self._get_capability_property( + node_template, + req, + property_name) + # If requirement was not found, look in node template's capabilities + return self._get_capability_property(node_tpl, + req_or_cap, + property_name) + + def _get_capability_property(self, + node_template, + capability_name, + property_name): + """Gets a node template capability property.""" + caps = node_template.get_capabilities() + if caps and capability_name in caps.keys(): + cap = caps[capability_name] + # Miguel: Cambios aqui + property = None + props = cap.get_properties() + if props and property_name in props.keys(): + property = props[property_name] + if not property: + raise KeyError(_( + "Property '{0}' not found in capability '{1}' of node" + " template '{2}' referenced from node template" + " '{3}'.").format(property_name, + capability_name, + node_template.name, + self.context.name)) + if property.value: + return property.value + else: + return property.default + msg = _("Requirement/Capability '{0}' referenced from '{1}' node " + "template not found in '{2}' node template.").format( + capability_name, + self.context.name, + node_template.name) + raise KeyError(msg) + + def _find_property(self, property_name): + node_tpl = self._find_node_template(self.args[0]) + props = node_tpl.get_properties() + found = [props[property_name]] if property_name in props else [] + if len(found) == 0: + raise KeyError(_( + "Property: '{0}' not found in node template: {1}.").format( + property_name, node_tpl.name)) + return found[0] + + def _find_node_template(self, node_template_name): + if node_template_name == SELF: + return self.context + # Miguel: cambios aqui + elif node_template_name == HOST: + return self._find_host_containing_property() + for node_template in self.tosca_tpl.nodetemplates: + if node_template.name == node_template_name: + return node_template + raise KeyError(_( + 'No such node template: {0}.').format(node_template_name)) + + # Miguel: anyado esto + def _find_host_containing_property(self, node_template_name=SELF): + node_template = self._find_node_template(node_template_name) + from IM.tosca.toscaparser.elements.entity_type import EntityType + hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] + for r in node_template.requirements: + for requirement, target_name in r.items(): + target_node = self._find_node_template(target_name) + target_type = target_node.type_definition + for capability in target_type.get_capabilities_objects(): + if capability.type in hosted_on_rel['valid_target_types']: + if self._property_exists_in_type(target_type): + return target_node + return self._find_host_containing_attribute( + target_name) + return None + + def _property_exists_in_type(self, type_definition): + props_def = type_definition.get_properties_def() + found = [props_def[self.args[1]]] \ + if self.args[1] in props_def else [] + return len(found) == 1 + + def result(self): + if len(self.args) == 3: + property_value = self._find_req_or_cap_property(self.args[1], + self.args[2]) + else: + property_value = self._find_property(self.args[1]).value + if isinstance(property_value, Function): + return property_value + return get_function(self.tosca_tpl, + self.context, + property_value) + + @property + def node_template_name(self): + return self.args[0] + + @property + def property_name(self): + if len(self.args) > 2: + return self.args[2] + return self.args[1] + + @property + def req_or_cap(self): + if len(self.args) > 2: + return self.args[1] + return None + + +function_mappings = { + GET_PROPERTY: GetProperty, + GET_INPUT: GetInput, + GET_ATTRIBUTE: GetAttribute +} + + +def is_function(function): + """Returns True if the provided function is a Tosca intrinsic function. + + Examples: + + * "{ get_property: { SELF, port } }" + * "{ get_input: db_name }" + * Function instance + + :param function: Function as string or a Function instance. + :return: True if function is a Tosca intrinsic function, otherwise False. + """ + if isinstance(function, dict) and len(function) == 1: + func_name = list(function.keys())[0] + return func_name in function_mappings + return isinstance(function, Function) + + +def get_function(tosca_tpl, node_template, raw_function): + """Gets a Function instance representing the provided template function. + + If the format provided raw_function format is not relevant for template + functions or if the function name doesn't exist in function mapping the + method returns the provided raw_function. + + :param tosca_tpl: The tosca template. + :param node_template: The node template the function is specified for. + :param raw_function: The raw function as dict. + :return: Template function as Function instance or the raw_function if + parsing was unsuccessful. + """ + if is_function(raw_function): + func_name = list(raw_function.keys())[0] + if func_name in function_mappings: + func = function_mappings[func_name] + func_args = list(raw_function.values())[0] + if not isinstance(func_args, list): + func_args = [func_args] + return func(tosca_tpl, node_template, func_name, func_args) + return raw_function diff --git a/IM/tosca/toscaparser/groups.py b/IM/tosca/toscaparser/groups.py new file mode 100644 index 000000000..40ebcf548 --- /dev/null +++ b/IM/tosca/toscaparser/groups.py @@ -0,0 +1,27 @@ +# 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. + + +class NodeGroup(object): + + def __init__(self, name, group_templates, member_nodes): + self.name = name + self.tpl = group_templates + self.members = member_nodes + + @property + def member_names(self): + return self.tpl.get('members') + + @property + def policies(self): + return self.tpl.get('policies') diff --git a/IM/tosca/toscaparser/nodetemplate.py b/IM/tosca/toscaparser/nodetemplate.py new file mode 100644 index 000000000..9288571b1 --- /dev/null +++ b/IM/tosca/toscaparser/nodetemplate.py @@ -0,0 +1,242 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.common.exception import InvalidPropertyValueError +from IM.tosca.toscaparser.common.exception import TypeMismatchError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.dataentity import DataEntity +from IM.tosca.toscaparser.elements.interfaces import CONFIGURE +from IM.tosca.toscaparser.elements.interfaces import CONFIGURE_SHORTNAME +from IM.tosca.toscaparser.elements.interfaces import InterfacesDef +from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE +from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE_SHORTNAME +from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType +from IM.tosca.toscaparser.entity_template import EntityTemplate +from IM.tosca.toscaparser.relationship_template import RelationshipTemplate +from IM.tosca.toscaparser.utils.gettextutils import _ + +log = logging.getLogger('tosca') + + +class NodeTemplate(EntityTemplate): + '''Node template from a Tosca profile.''' + def __init__(self, name, node_templates, custom_def=None, + available_rel_tpls=None, available_rel_types=None): + super(NodeTemplate, self).__init__(name, node_templates[name], + 'node_type', + custom_def) + self.templates = node_templates + self._validate_fields(node_templates[name]) + self.custom_def = custom_def + self.related = {} + self.relationship_tpl = [] + self.available_rel_tpls = available_rel_tpls + self.available_rel_types = available_rel_types + self._relationships = {} + + @property + def relationships(self): + if not self._relationships: + requires = self.requirements + if requires: + for r in requires: + for _, value in r.items(): + explicit = self._get_explicit_relationship(r, value) + if explicit: + for key, value in explicit.items(): + self._relationships[key] = value + return self._relationships + + def _get_explicit_relationship(self, req, value): + """Handle explicit relationship + + For example, + - req: + node: DBMS + relationship: tosca.relationships.HostedOn + """ + explicit_relation = {} + node = value.get('node') if isinstance(value, dict) else value + + if node: + # TODO(spzala) implement look up once Glance meta data is available + # to find a matching TOSCA node using the TOSCA types + msg = _('Lookup by TOSCA types are not supported. ' + 'Requirement for %s can not be full-filled.') % self.name + if (node in list(self.type_definition.TOSCA_DEF.keys()) + or node in self.custom_def): + raise NotImplementedError(msg) + related_tpl = NodeTemplate(node, self.templates, self.custom_def) + relationship = value.get('relationship') \ + if isinstance(value, dict) else None + # check if it's type has relationship defined + if not relationship: + parent_reqs = self.type_definition.get_all_requirements() + for key in req.keys(): + for req_dict in parent_reqs: + if key in req_dict.keys(): + relationship = (req_dict.get(key). + get('relationship')) + break + if relationship: + found_relationship_tpl = False + # apply available relationship templates if found + # Miguel: add this if + if self.available_rel_tpls: + for tpl in self.available_rel_tpls: + if tpl.name == relationship: + rtype = RelationshipType(tpl.type, None, + self.custom_def) + explicit_relation[rtype] = related_tpl + self.relationship_tpl.append(tpl) + found_relationship_tpl = True + + # create relationship template object. + rel_prfx = self.type_definition.RELATIONSHIP_PREFIX + if not found_relationship_tpl: + if isinstance(relationship, dict): + relationship = relationship.get('type') + if self.available_rel_types and \ + relationship in self.available_rel_types.keys(): + pass + elif not relationship.startswith(rel_prfx): + relationship = rel_prfx + relationship + for rtype in self.type_definition.relationship.keys(): + if rtype.type == relationship: + explicit_relation[rtype] = related_tpl + related_tpl._add_relationship_template(req, + rtype.type) + elif self.available_rel_types: + if relationship in self.available_rel_types.keys(): + rel_type_def = self.available_rel_types.\ + get(relationship) + if 'derived_from' in rel_type_def: + super_type = \ + rel_type_def.get('derived_from') + if not super_type.startswith(rel_prfx): + super_type = rel_prfx + super_type + if rtype.type == super_type: + explicit_relation[rtype] = related_tpl + related_tpl.\ + _add_relationship_template( + req, rtype.type) + return explicit_relation + + def _add_relationship_template(self, requirement, rtype): + req = requirement.copy() + req['type'] = rtype + tpl = RelationshipTemplate(req, rtype, None) + self.relationship_tpl.append(tpl) + + def get_relationship_template(self): + return self.relationship_tpl + + def _add_next(self, nodetpl, relationship): + self.related[nodetpl] = relationship + + @property + def related_nodes(self): + if not self.related: + for relation, node in self.type_definition.relationship.items(): + for tpl in self.templates: + if tpl == node.type: + self.related[NodeTemplate(tpl)] = relation + return self.related.keys() + + def validate(self, tosca_tpl=None): + self._validate_capabilities() + self._validate_requirements() + self._validate_properties(self.entity_tpl, self.type_definition) + self._validate_interfaces() + for prop in self.get_properties_objects(): + prop.validate() + + def _validate_requirements(self): + type_requires = self.type_definition.get_all_requirements() + allowed_reqs = ["template"] + if type_requires: + for treq in type_requires: + for key, value in treq.items(): + allowed_reqs.append(key) + if isinstance(value, dict): + for key in value: + allowed_reqs.append(key) + + requires = self.type_definition.get_value(self.REQUIREMENTS, + self.entity_tpl) + if requires: + if not isinstance(requires, list): + raise TypeMismatchError( + what='Requirements of template %s' % self.name, + type='list') + for req in requires: + for r1, value in req.items(): + if isinstance(value, dict): + self._validate_requirements_keys(value) + self._validate_requirements_properties(value) + allowed_reqs.append(r1) + self._common_validate_field(req, allowed_reqs, 'Requirements') + + def _validate_requirements_properties(self, requirements): + # TODO(anyone): Only occurences property of the requirements is + # validated here. Validation of other requirement properties are being + # validated in different files. Better to keep all the requirements + # properties validation here. + for key, value in requirements.items(): + if key == 'occurrences': + self._validate_occurrences(value) + break + + def _validate_occurrences(self, occurrences): + DataEntity.validate_datatype('list', occurrences) + for value in occurrences: + DataEntity.validate_datatype('integer', value) + if len(occurrences) != 2 or not (0 <= occurrences[0] <= occurrences[1]) \ + or occurrences[1] == 0: + raise InvalidPropertyValueError(what=(occurrences)) + + def _validate_requirements_keys(self, requirement): + for key in requirement.keys(): + if key not in self.REQUIREMENTS_SECTION: + raise UnknownFieldError( + what='Requirements of template %s' % self.name, + field=key) + + def _validate_interfaces(self): + ifaces = self.type_definition.get_value(self.INTERFACES, + self.entity_tpl) + if ifaces: + for i in ifaces: + for name, value in ifaces.items(): + if name in (LIFECYCLE, LIFECYCLE_SHORTNAME): + self._common_validate_field( + value, InterfacesDef. + interfaces_node_lifecycle_operations, + 'Interfaces') + elif name in (CONFIGURE, CONFIGURE_SHORTNAME): + self._common_validate_field( + value, InterfacesDef. + interfaces_relationship_confiure_operations, + 'Interfaces') + else: + raise UnknownFieldError( + what='Interfaces of template %s' % self.name, + field=name) + + def _validate_fields(self, nodetemplate): + for name in nodetemplate.keys(): + if name not in self.SECTIONS: + raise UnknownFieldError(what='Node template %s' + % self.name, field=name) diff --git a/IM/tosca/toscaparser/parameters.py b/IM/tosca/toscaparser/parameters.py new file mode 100644 index 000000000..a8a3f76e4 --- /dev/null +++ b/IM/tosca/toscaparser/parameters.py @@ -0,0 +1,110 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.dataentity import DataEntity +from IM.tosca.toscaparser.elements.constraints import Schema +from IM.tosca.toscaparser.elements.entity_type import EntityType +from IM.tosca.toscaparser.utils.gettextutils import _ + + +log = logging.getLogger('tosca') + + +class Input(object): + + INPUTFIELD = (TYPE, DESCRIPTION, DEFAULT, CONSTRAINTS) = \ + ('type', 'description', 'default', 'constraints') + + def __init__(self, name, schema_dict): + self.name = name + self.schema = Schema(name, schema_dict) + + @property + def type(self): + return self.schema.type + + @property + def description(self): + return self.schema.description + + @property + def default(self): + return self.schema.default + + @property + def constraints(self): + return self.schema.constraints + + def validate(self, value=None): + self._validate_field() + self.validate_type(self.type) + if value: + self._validate_value(value) + + def _validate_field(self): + for name in self.schema: + if name not in self.INPUTFIELD: + raise UnknownFieldError(what='Input %s' % self.name, + field=name) + + def validate_type(self, input_type): + if input_type not in Schema.PROPERTY_TYPES: + raise ValueError(_('Invalid type %s') % type) + + def _validate_value(self, value): + tosca = EntityType.TOSCA_DEF + datatype = None + if self.type in tosca: + datatype = tosca[self.type] + elif EntityType.DATATYPE_PREFIX + self.type in tosca: + datatype = tosca[EntityType.DATATYPE_PREFIX + self.type] + + DataEntity.validate_datatype(self.type, value, None, datatype) + + +class Output(object): + + OUTPUTFIELD = (DESCRIPTION, VALUE) = ('description', 'value') + + def __init__(self, name, attrs): + self.name = name + self.attrs = attrs + + @property + def description(self): + return self.attrs[self.DESCRIPTION] + + @property + def value(self): + return self.attrs[self.VALUE] + + def validate(self): + self._validate_field() + + def _validate_field(self): + if not isinstance(self.attrs, dict): + raise MissingRequiredFieldError(what='Output %s' % self.name, + required=self.VALUE) + try: + self.value + except KeyError: + raise MissingRequiredFieldError(what='Output %s' % self.name, + required=self.VALUE) + for name in self.attrs: + if name not in self.OUTPUTFIELD: + raise UnknownFieldError(what='Output %s' % self.name, + field=name) diff --git a/IM/tosca/toscaparser/prereq/__init__.py b/IM/tosca/toscaparser/prereq/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/prereq/csar.py b/IM/tosca/toscaparser/prereq/csar.py new file mode 100644 index 000000000..9f17b902c --- /dev/null +++ b/IM/tosca/toscaparser/prereq/csar.py @@ -0,0 +1,122 @@ +# 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. + +import os.path +import yaml +import zipfile + +from IM.tosca.toscaparser.common.exception import ValidationError +from IM.tosca.toscaparser.utils.gettextutils import _ + + +class CSAR(object): + + def __init__(self, csar_file): + self.csar_file = csar_file + self.is_validated = False + + def validate(self): + """Validate the provided CSAR file.""" + + self.is_validated = True + + # validate that the file exists + if not os.path.isfile(self.csar_file): + err_msg = (_('The file %s does not exist.') % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that it is a valid zip file + if not zipfile.is_zipfile(self.csar_file): + err_msg = (_('The file %s is not a valid zip file.') + % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that it contains the metadata file in the correct location + self.zfile = zipfile.ZipFile(self.csar_file, 'r') + filelist = self.zfile.namelist() + if 'TOSCA-Metadata/TOSCA.meta' not in filelist: + err_msg = (_('The file %s is not a valid CSAR as it does not ' + 'contain the required file "TOSCA.meta" in the ' + 'folder "TOSCA-Metadata".') % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that 'Entry-Definitions' property exists in TOSCA.meta + data = self.zfile.read('TOSCA-Metadata/TOSCA.meta') + invalid_yaml_err_msg = (_('The file "TOSCA-Metadata/TOSCA.meta" in %s ' + 'does not contain valid YAML content.') % + self.csar_file) + try: + meta = yaml.load(data) + if type(meta) is not dict: + raise ValidationError(message=invalid_yaml_err_msg) + self.metadata = meta + except yaml.YAMLError: + raise ValidationError(message=invalid_yaml_err_msg) + + if 'Entry-Definitions' not in self.metadata: + err_msg = (_('The CSAR file "%s" is missing the required metadata ' + '"Entry-Definitions" in "TOSCA-Metadata/TOSCA.meta".') + % self.csar_file) + raise ValidationError(message=err_msg) + + # validate that 'Entry-Definitions' metadata value points to an + # existing file in the CSAR + entry = self.metadata['Entry-Definitions'] + if entry not in filelist: + err_msg = (_('The "Entry-Definitions" file defined in the CSAR ' + '"%s" does not exist.') % self.csar_file) + raise ValidationError(message=err_msg) + + def get_metadata(self): + """Return the metadata dictionary.""" + + # validate the csar if not already validated + if not self.is_validated: + self.validate() + + # return a copy to avoid changes overwrite the original + return dict(self.metadata) if self.metadata else None + + def _get_metadata(self, key): + if not self.is_validated: + self.validate() + return self.metadata[key] if key in self.metadata else None + + def get_author(self): + return self._get_metadata('Created-By') + + def get_version(self): + return self._get_metadata('CSAR-Version') + + def get_main_template(self): + return self._get_metadata('Entry-Definitions') + + def get_description(self): + desc = self._get_metadata('Description') + if desc is not None: + return desc + + main_template = self.get_main_template() + # extract the description from the main template + data = self.zfile.read(main_template) + invalid_tosca_yaml_err_msg = ( + _('The file %(template)s in %(csar)s does not contain valid TOSCA ' + 'YAML content.') % {'template': main_template, + 'csar': self.csar_file}) + try: + tosca_yaml = yaml.load(data) + if type(tosca_yaml) is not dict: + raise ValidationError(message=invalid_tosca_yaml_err_msg) + self.metadata['Description'] = tosca_yaml['description'] + except Exception: + raise ValidationError(message=invalid_tosca_yaml_err_msg) + return self.metadata['Description'] diff --git a/IM/tosca/toscaparser/properties.py b/IM/tosca/toscaparser/properties.py new file mode 100644 index 000000000..f35c4394a --- /dev/null +++ b/IM/tosca/toscaparser/properties.py @@ -0,0 +1,79 @@ +# 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. + +from IM.tosca.toscaparser.dataentity import DataEntity +from IM.tosca.toscaparser.elements.constraints import Schema +from IM.tosca.toscaparser.functions import is_function + + +class Property(object): + '''TOSCA built-in Property type.''' + + PROPERTY_KEYS = ( + TYPE, REQUIRED, DESCRIPTION, DEFAULT, CONSTRAINTS + ) = ( + 'type', 'required', 'description', 'default', 'constraints' + ) + + ENTRY_SCHEMA_KEYS = ( + ENTRYTYPE, ENTRYPROPERTIES + ) = ( + 'type', 'properties' + ) + + def __init__(self, property_name, value, schema_dict, custom_def=None): + self.name = property_name + self.value = value + self.custom_def = custom_def + self.schema = Schema(property_name, schema_dict) + + @property + def type(self): + return self.schema.type + + @property + def required(self): + return self.schema.required + + @property + def description(self): + return self.schema.description + + @property + def default(self): + return self.schema.default + + @property + def constraints(self): + return self.schema.constraints + + @property + def entry_schema(self): + return self.schema.entry_schema + + def validate(self): + '''Validate if not a reference property.''' + # Miguel: Cambios aqui + if not is_function(self.value): + if self.value is not None: + if self.type == Schema.STRING: + self.value = str(self.value) + self.value = DataEntity.validate_datatype(self.type, self.value, + self.entry_schema, + self.custom_def) + self._validate_constraints() + + def _validate_constraints(self): + # Miguel: Cambios aqui + if self.value and self.constraints: + for constraint in self.constraints: + constraint.validate(self.value) diff --git a/IM/tosca/toscaparser/relationship_template.py b/IM/tosca/toscaparser/relationship_template.py new file mode 100644 index 000000000..a213595ca --- /dev/null +++ b/IM/tosca/toscaparser/relationship_template.py @@ -0,0 +1,68 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.entity_template import EntityTemplate +from IM.tosca.toscaparser.properties import Property + +SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, + INTERFACES, CAPABILITIES, TYPE) = \ + ('derived_from', 'properties', 'requirements', 'interfaces', + 'capabilities', 'type') + +log = logging.getLogger('tosca') + + +class RelationshipTemplate(EntityTemplate): + '''Relationship template.''' + def __init__(self, relationship_template, name, custom_def=None): + super(RelationshipTemplate, self).__init__(name, + relationship_template, + 'relationship_type', + custom_def) + self.name = name.lower() + + def get_properties_objects(self): + '''Return properties objects for this template.''' + if self._properties is None: + self._properties = self._create_relationship_properties() + return self._properties + + def _create_relationship_properties(self): + props = [] + properties = {} + relationship = self.entity_tpl.get('relationship') + if relationship: + properties = self.type_definition.get_value(self.PROPERTIES, + relationship) or {} + if not properties: + properties = self.entity_tpl.get(self.PROPERTIES) or {} + + if properties: + for name, value in properties.items(): + props_def = self.type_definition.get_properties_def() + if props_def and name in props_def: + if name in properties.keys(): + value = properties.get(name) + prop = Property(name, value, + props_def[name].schema, self.custom_def) + props.append(prop) + for p in self.type_definition.get_properties_def_objects(): + if p.default is not None and p.name not in properties.keys(): + prop = Property(p.name, p.default, p.schema, self.custom_def) + props.append(prop) + return props + + def validate(self): + self._validate_properties(self.entity_tpl, self.type_definition) diff --git a/IM/tosca/toscaparser/topology_template.py b/IM/tosca/toscaparser/topology_template.py new file mode 100644 index 000000000..6822189fa --- /dev/null +++ b/IM/tosca/toscaparser/topology_template.py @@ -0,0 +1,213 @@ +# 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. + + +import logging + +from IM.tosca.toscaparser.common import exception +from IM.tosca.toscaparser import functions +from IM.tosca.toscaparser.groups import NodeGroup +from IM.tosca.toscaparser.nodetemplate import NodeTemplate +from IM.tosca.toscaparser.parameters import Input +from IM.tosca.toscaparser.parameters import Output +from IM.tosca.toscaparser.relationship_template import RelationshipTemplate +from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph + + +# Topology template key names +SECTIONS = (DESCRIPTION, INPUTS, NODE_TEMPLATES, + RELATIONSHIP_TEMPLATES, OUTPUTS, GROUPS, + SUBSTITUION_MAPPINGS) = \ + ('description', 'inputs', 'node_templates', + 'relationship_templates', 'outputs', 'groups', + 'substitution_mappings') + +log = logging.getLogger("tosca.model") + + +class TopologyTemplate(object): + + '''Load the template data.''' + def __init__(self, template, custom_defs, + rel_types=None, parsed_params=None): + self.tpl = template + self.custom_defs = custom_defs + self.rel_types = rel_types + self.parsed_params = parsed_params + self._validate_field() + self.description = self._tpl_description() + self.inputs = self._inputs() + self.relationship_templates = self._relationship_templates() + self.nodetemplates = self._nodetemplates() + self.outputs = self._outputs() + self.graph = ToscaGraph(self.nodetemplates) + self.groups = self._groups() + self._process_intrinsic_functions() + + def _inputs(self): + inputs = [] + for name, attrs in self._tpl_inputs().items(): + input = Input(name, attrs) + if self.parsed_params and name in self.parsed_params: + input.validate(self.parsed_params[name]) + inputs.append(input) + return inputs + + def _nodetemplates(self): + nodetemplates = [] + tpls = self._tpl_nodetemplates() + for name in tpls: + tpl = NodeTemplate(name, tpls, self.custom_defs, + self.relationship_templates, + self.rel_types) + tpl.validate(self) + nodetemplates.append(tpl) + return nodetemplates + + def _relationship_templates(self): + rel_templates = [] + tpls = self._tpl_relationship_templates() + for name in tpls: + tpl = RelationshipTemplate(tpls[name], name, self.custom_defs) + rel_templates.append(tpl) + return rel_templates + + def _outputs(self): + outputs = [] + for name, attrs in self._tpl_outputs().items(): + output = Output(name, attrs) + output.validate() + outputs.append(output) + return outputs + + def _substitution_mappings(self): + pass + + def _groups(self): + groups = [] + for group_name, group_tpl in self._tpl_groups().items(): + member_names = group_tpl.get('members') + if member_names and len(member_names) > 1: + group = NodeGroup(group_name, group_tpl, + self._get_group_memerbs(member_names)) + groups.append(group) + else: + raise ValueError + return groups + + def _get_group_memerbs(self, member_names): + member_nodes = [] + for member in member_names: + for node in self.nodetemplates: + if node.name == member: + member_nodes.append(node) + return member_nodes + + # topology template can act like node template + # it is exposed by substitution_mappings. + def nodetype(self): + pass + + def capabilities(self): + pass + + def requirements(self): + pass + + def _tpl_description(self): + description = self.tpl.get(DESCRIPTION) + if description: + description = description.rstrip() + return description + + def _tpl_inputs(self): + return self.tpl.get(INPUTS) or {} + + def _tpl_nodetemplates(self): + return self.tpl[NODE_TEMPLATES] + + def _tpl_relationship_templates(self): + return self.tpl.get(RELATIONSHIP_TEMPLATES) or {} + + def _tpl_outputs(self): + return self.tpl.get(OUTPUTS) or {} + + def _tpl_substitution_mappings(self): + return self.tpl.get(SUBSTITUION_MAPPINGS) or {} + + def _tpl_groups(self): + return self.tpl.get(GROUPS) or {} + + def _validate_field(self): + for name in self.tpl: + if name not in SECTIONS: + raise exception.UnknownFieldError(what='Template', field=name) + + def _process_intrinsic_functions(self): + """Process intrinsic functions + + Current implementation processes functions within node template + properties, requirements, interfaces inputs and template outputs. + """ + for node_template in self.nodetemplates: + for prop in node_template.get_properties_objects(): + prop.value = functions.get_function(self, + node_template, + prop.value) + for interface in node_template.interfaces: + if interface.inputs: + for name, value in interface.inputs.items(): + interface.inputs[name] = functions.get_function( + self, + node_template, + value) + if node_template.requirements: + for req in node_template.requirements: + rel = req + for req_name, req_item in req.items(): + if isinstance(req_item, dict): + rel = req_item.get('relationship') + break + if rel and 'properties' in rel: + for key, value in rel['properties'].items(): + rel['properties'][key] = functions.get_function( + self, + req, + value) + if node_template.get_capabilities_objects(): + for cap in node_template.get_capabilities_objects(): + if cap.get_properties_objects(): + for prop in cap.get_properties_objects(): + propvalue = functions.get_function( + self, + node_template, + prop.value) + if isinstance(propvalue, functions.GetInput): + propvalue = propvalue.result() + for p, v in cap._properties.items(): + if p == prop.name: + cap._properties[p] = propvalue + for rel, node in node_template.relationships.items(): + rel_tpls = node.relationship_tpl + if rel_tpls: + for rel_tpl in rel_tpls: + for interface in rel_tpl.interfaces: + if interface.inputs: + for name, value in interface.inputs.items(): + interface.inputs[name] = \ + functions.get_function(self, + rel_tpl, + value) + for output in self.outputs: + func = functions.get_function(self, self.outputs, output.value) + if isinstance(func, functions.GetAttribute): + output.attrs[output.VALUE] = func diff --git a/IM/tosca/toscaparser/tosca_template.py b/IM/tosca/toscaparser/tosca_template.py new file mode 100644 index 000000000..0c7589fec --- /dev/null +++ b/IM/tosca/toscaparser/tosca_template.py @@ -0,0 +1,190 @@ +# 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. + + +import logging +import os + +from IM.tosca.toscaparser.common.exception import InvalidTemplateVersion +from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError +from IM.tosca.toscaparser.common.exception import UnknownFieldError +from IM.tosca.toscaparser.topology_template import TopologyTemplate +from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph +from IM.tosca.toscaparser.utils.gettextutils import _ +import IM.tosca.toscaparser.utils.urlutils +import IM.tosca.toscaparser.utils.yamlparser + + +# TOSCA template key names +SECTIONS = (DEFINITION_VERSION, DEFAULT_NAMESPACE, TEMPLATE_NAME, + TOPOLOGY_TEMPLATE, TEMPLATE_AUTHOR, TEMPLATE_VERSION, + DESCRIPTION, IMPORTS, DSL_DEFINITIONS, NODE_TYPES, + RELATIONSHIP_TYPES, RELATIONSHIP_TEMPLATES, + CAPABILITY_TYPES, ARTIFACT_TYPES, DATATYPE_DEFINITIONS) = \ + ('tosca_definitions_version', 'tosca_default_namespace', + 'template_name', 'topology_template', 'template_author', + 'template_version', 'description', 'imports', 'dsl_definitions', + 'node_types', 'relationship_types', 'relationship_templates', + 'capability_types', 'artifact_types', 'datatype_definitions') + +log = logging.getLogger("tosca.model") + +YAML_LOADER = IM.tosca.toscaparser.utils.yamlparser.load_yaml + + +class ToscaTemplate(object): + + VALID_TEMPLATE_VERSIONS = ['tosca_simple_yaml_1_0'] + + '''Load the template data.''' + def __init__(self, path, a_file=True, parsed_params=None): + self.tpl = YAML_LOADER(path, a_file) + self.path = path + self.a_file = a_file + self.parsed_params = parsed_params + self._validate_field() + self.version = self._tpl_version() + self.relationship_types = self._tpl_relationship_types() + self.description = self._tpl_description() + self.topology_template = self._topology_template() + self.inputs = self._inputs() + self.relationship_templates = self._relationship_templates() + self.nodetemplates = self._nodetemplates() + self.outputs = self._outputs() + self.graph = ToscaGraph(self.nodetemplates) + + def _topology_template(self): + return TopologyTemplate(self._tpl_topology_template(), + self._get_all_custom_defs(), + self.relationship_types, + self.parsed_params) + + def _inputs(self): + return self.topology_template.inputs + + def _nodetemplates(self): + return self.topology_template.nodetemplates + + def _relationship_templates(self): + return self.topology_template.relationship_templates + + def _outputs(self): + return self.topology_template.outputs + + def _tpl_version(self): + return self.tpl[DEFINITION_VERSION] + + def _tpl_description(self): + return self.tpl[DESCRIPTION].rstrip() + + def _tpl_imports(self): + if IMPORTS in self.tpl: + return self.tpl[IMPORTS] + + def _tpl_relationship_types(self): + return self._get_custom_types(RELATIONSHIP_TYPES) + + def _tpl_relationship_templates(self): + topology_template = self._tpl_topology_template() + if RELATIONSHIP_TEMPLATES in topology_template.keys(): + return topology_template[RELATIONSHIP_TEMPLATES] + else: + return None + + def _tpl_topology_template(self): + return self.tpl.get(TOPOLOGY_TEMPLATE) + + def _get_all_custom_defs(self): + types = [NODE_TYPES, CAPABILITY_TYPES, RELATIONSHIP_TYPES, + DATATYPE_DEFINITIONS] + custom_defs = {} + for type in types: + custom_def = self._get_custom_types(type) + if custom_def: + custom_defs.update(custom_def) + return custom_defs + + def _get_custom_types(self, type_definition): + """Handle custom types defined in imported template files + + This method loads the custom type definitions referenced in "imports" + section of the TOSCA YAML template by determining whether each import + is specified via a file reference (by relative or absolute path) or a + URL reference. It then assigns the correct value to "def_file" variable + so the YAML content of those imports can be loaded. + + Possibilities: + +----------+--------+------------------------------+ + | template | import | comment | + +----------+--------+------------------------------+ + | file | file | OK | + | file | URL | OK | + | URL | file | file must be a relative path | + | URL | URL | OK | + +----------+--------+------------------------------+ + """ + + custom_defs = {} + imports = self._tpl_imports() + if imports: + main_a_file = os.path.isfile(self.path) + for definition in imports: + def_file = definition + a_file = False + if main_a_file: + if os.path.isfile(definition): + a_file = True + else: + full_path = os.path.join( + os.path.dirname(os.path.abspath(self.path)), + definition) + if os.path.isfile(full_path): + a_file = True + def_file = full_path + else: # main_a_url + a_url = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ + validate_url(definition) + if not a_url: + if os.path.isabs(definition): + raise ImportError(_("Absolute file name cannot be " + "used for a URL-based input " + "template.")) + def_file = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ + join_url(self.path, definition) + + custom_type = YAML_LOADER(def_file, a_file) + outer_custom_types = custom_type.get(type_definition) + if outer_custom_types: + custom_defs.update(outer_custom_types) + + # Handle custom types defined in current template file + inner_custom_types = self.tpl.get(type_definition) or {} + if inner_custom_types: + custom_defs.update(inner_custom_types) + return custom_defs + + def _validate_field(self): + try: + version = self._tpl_version() + self._validate_version(version) + except KeyError: + raise MissingRequiredFieldError(what='Template', + required=DEFINITION_VERSION) + for name in self.tpl: + if name not in SECTIONS: + raise UnknownFieldError(what='Template', field=name) + + def _validate_version(self, version): + if version not in self.VALID_TEMPLATE_VERSIONS: + raise InvalidTemplateVersion( + what=version, + valid_versions=', '. join(self.VALID_TEMPLATE_VERSIONS)) diff --git a/IM/tosca/toscaparser/tpl_relationship_graph.py b/IM/tosca/toscaparser/tpl_relationship_graph.py new file mode 100644 index 000000000..1a5ea7b66 --- /dev/null +++ b/IM/tosca/toscaparser/tpl_relationship_graph.py @@ -0,0 +1,46 @@ +# 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. + + +class ToscaGraph(object): + '''Graph of Tosca Node Templates.''' + def __init__(self, nodetemplates): + self.nodetemplates = nodetemplates + self.vertices = {} + self._create() + + def _create_vertex(self, node): + if node not in self.vertices: + self.vertices[node.name] = node + + def _create_edge(self, node1, node2, relationship): + if node1 not in self.vertices: + self._create_vertex(node1) + self.vertices[node1.name]._add_next(node2, + relationship) + + def vertex(self, node): + if node in self.vertices: + return self.vertices[node] + + def __iter__(self): + return iter(self.vertices.values()) + + def _create(self): + for node in self.nodetemplates: + relation = node.relationships + if relation: + for rel, nodetpls in relation.items(): + for tpl in self.nodetemplates: + if tpl.name == nodetpls.name: + self._create_edge(node, tpl, rel) + self._create_vertex(node) diff --git a/IM/tosca/toscaparser/utils/__init__.py b/IM/tosca/toscaparser/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tosca/toscaparser/utils/gettextutils.py b/IM/tosca/toscaparser/utils/gettextutils.py new file mode 100644 index 000000000..f5562e2d7 --- /dev/null +++ b/IM/tosca/toscaparser/utils/gettextutils.py @@ -0,0 +1,22 @@ +# 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. + +import gettext +import os + +_localedir = os.environ.get('tosca-parser'.upper() + '_LOCALEDIR') +_t = gettext.translation('tosca-parser', localedir=_localedir, + fallback=True) + + +def _(msg): + return _t.gettext(msg) diff --git a/IM/tosca/toscaparser/utils/urlutils.py b/IM/tosca/toscaparser/utils/urlutils.py new file mode 100644 index 000000000..628314cdf --- /dev/null +++ b/IM/tosca/toscaparser/utils/urlutils.py @@ -0,0 +1,43 @@ +# 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. + + +from six.moves.urllib.parse import urljoin +from six.moves.urllib.parse import urlparse +from IM.tosca.toscaparser.utils.gettextutils import _ + + +class UrlUtils(object): + + @staticmethod + def validate_url(path): + """Validates whether the given path is a URL or not. + + If the given path includes a scheme (http, https, ftp, ...) and a net + location (a domain name such as www.github.com) it is validated as a + URL. + """ + parsed = urlparse(path) + return bool(parsed.scheme) and bool(parsed.netloc) + + @staticmethod + def join_url(url, relative_path): + """Builds a new URL from the given URL and the relative path. + + Example: + url: http://www.githib.com/openstack/heat + relative_path: heat-translator + - joined: http://www.githib.com/openstack/heat-translator + """ + if not UrlUtils.validate_url(url): + raise ValueError(_("Provided URL is invalid.")) + return urljoin(url, relative_path) diff --git a/IM/tosca/toscaparser/utils/validateutils.py b/IM/tosca/toscaparser/utils/validateutils.py new file mode 100644 index 000000000..42bfc4664 --- /dev/null +++ b/IM/tosca/toscaparser/utils/validateutils.py @@ -0,0 +1,154 @@ +# 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. + +import collections +import dateutil.parser +import logging +import numbers +import re +import six + +from IM.tosca.toscaparser.common.exception import ( + InvalidTOSCAVersionPropertyException) +from IM.tosca.toscaparser.utils.gettextutils import _ +log = logging.getLogger('tosca') + + +def str_to_num(value): + '''Convert a string representation of a number into a numeric type.''' + if isinstance(value, numbers.Number): + return value + try: + return int(value) + except ValueError: + return float(value) + + +def validate_number(value): + return str_to_num(value) + + +def validate_integer(value): + if not isinstance(value, int): + try: + value = int(value) + except Exception: + raise ValueError(_('"%s" is not an integer') % value) + return value + + +def validate_float(value): + if not isinstance(value, float): + raise ValueError(_('"%s" is not a float') % value) + return validate_number(value) + + +def validate_string(value): + if not isinstance(value, six.string_types): + raise ValueError(_('"%s" is not a string') % value) + return value + + +def validate_list(value): + if not isinstance(value, list): + raise ValueError(_('"%s" is not a list') % value) + return value + + +def validate_map(value): + if not isinstance(value, collections.Mapping): + raise ValueError(_('"%s" is not a map') % value) + return value + + +def validate_boolean(value): + if isinstance(value, bool): + return value + + if isinstance(value, str): + normalised = value.lower() + if normalised in ['true', 'false']: + return normalised == 'true' + raise ValueError(_('"%s" is not a boolean') % value) + + +def validate_timestamp(value): + return dateutil.parser.parse(value) + + +class TOSCAVersionProperty(object): + + VERSION_RE = re.compile('^(?P([0-9][0-9]*))' + '(\.(?P([0-9][0-9]*)))?' + '(\.(?P([0-9][0-9]*)))?' + '(\.(?P([0-9A-Za-z]+)))?' + '(\-(?P[0-9])*)?$') + + def __init__(self, version): + self.version = str(version) + match = self.VERSION_RE.match(self.version) + if not match: + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + ver = match.groupdict() + if self.version in ['0', '0.0', '0.0.0']: + log.warning(_('Version assumed as not provided')) + self.version = None + self.minor_version = ver['minor_version'] + self.major_version = ver['major_version'] + self.fix_version = ver['fix_version'] + self.qualifier = self._validate_qualifier(ver['qualifier']) + self.build_version = self._validate_build(ver['build_version']) + self._validate_major_version(self.major_version) + + def _validate_major_version(self, value): + """Validate major version + + Checks if only major version is provided and assumes + minor version as 0. + Eg: If version = 18, then it returns version = '18.0' + """ + + if self.minor_version is None and self.build_version is None and \ + value != '0': + log.warning(_('Minor version assumed "0"')) + self.version = '.'.join([value, '0']) + return value + + def _validate_qualifier(self, value): + """Validate qualifier + + TOSCA version is invalid if a qualifier is present without the + fix version or with all of major, minor and fix version 0s. + + For example, the following versions are invalid + 18.0.abc + 0.0.0.abc + """ + if (self.fix_version is None and value) or \ + (self.minor_version == self.major_version == + self.fix_version == '0' and value): + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + return value + + def _validate_build(self, value): + """Validate build version + + TOSCA version is invalid if build version is present without the + qualifier. + Eg: version = 18.0.0-1 is invalid. + """ + if not self.qualifier and value: + raise InvalidTOSCAVersionPropertyException(what=(self.version)) + return value + + def get_version(self): + return self.version diff --git a/IM/tosca/toscaparser/utils/yamlparser.py b/IM/tosca/toscaparser/utils/yamlparser.py new file mode 100644 index 000000000..cd6c5b224 --- /dev/null +++ b/IM/tosca/toscaparser/utils/yamlparser.py @@ -0,0 +1,73 @@ +# 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. + +import codecs +from collections import OrderedDict +import yaml + +try: + # Python 3.x + import urllib.request as urllib2 +except ImportError: + # Python 2.x + import urllib2 + +if hasattr(yaml, 'CSafeLoader'): + yaml_loader = yaml.CSafeLoader +else: + yaml_loader = yaml.SafeLoader + + +def load_yaml(path, a_file=True): + # Miguel: enable to load also a TOSCA string + if path.find("\n") == -1: + f = codecs.open(path, encoding='utf-8', errors='strict') if a_file \ + else urllib2.urlopen(path) + return yaml.load(f.read(), Loader=yaml_loader) + else: + return yaml.load(path, Loader=yaml_loader) + + +def simple_parse(tmpl_str): + try: + tpl = yaml.load(tmpl_str, Loader=yaml_loader) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + return tpl + + +def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict): + class OrderedLoader(Loader): + pass + + def construct_mapping(loader, node): + loader.flatten_mapping(node) + return object_pairs_hook(loader.construct_pairs(node)) + + OrderedLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + construct_mapping) + return yaml.load(stream, OrderedLoader) + + +def simple_ordered_parse(tmpl_str): + try: + tpl = ordered_load(tmpl_str) + except yaml.YAMLError as yea: + raise ValueError(yea) + else: + if tpl is None: + tpl = {} + return tpl diff --git a/examples/galaxy_tosca.yml b/examples/galaxy_tosca.yml new file mode 100644 index 000000000..dc8e7a7e8 --- /dev/null +++ b/examples/galaxy_tosca.yml @@ -0,0 +1,38 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA Galaxy test for the IM + +topology_template: + + node_templates: + + bowtie2_galaxy_tool: + type: tosca.nodes.indigo.GalaxyTool + properties: + name: bowtie2 + owner: devteam + tool_panel_section_id: ngs_mapping + requirements: + - host: galaxy + + galaxy: + type: tosca.nodes.indigo.GalaxyPortal + requirements: + - host: galaxy_server + + galaxy_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + #distribution: scientific + #version: 6.6 + diff --git a/examples/tosca.yml b/examples/tosca.yml new file mode 100644 index 000000000..ea0f9689a --- /dev/null +++ b/examples/tosca.yml @@ -0,0 +1,118 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: dbname + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + relationship_templates: + my_custom_connection: + type: HostedOn + interfaces: + Configure: + pre_configure_source: scripts/wp_db_configure.sh + + node_templates: + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: scientific + version: 6.6 + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: mysql + relationship: my_custom_connection + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node_filter: + capabilities: + # Constraints for selecting “host” (Container Capability) + - host: + properties: + - num_cpus: { in_range: [1,4] } + - mem_size: { greater_or_equal: 1 GB } + # Constraints for selecting “os” (OperatingSystem Capability) + - os: + properties: + - architecture: { equal: x86_64 } + - type: linux + - distribution: ubuntu + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + requirements: + # contextually this can only be a relationship type + - local_storage: + # capability is provided by Compute Node Type + node: my_block_storage + relationship: + type: AttachesTo + properties: + location: /mnt/disk + # This maps the local requirement name ‘local_storage’ to the + # target node’s capability name ‘attachment’ + device: hdb + interfaces: + Configure: + pre_configure_source: scripts/wp_db_configure.sh + + my_block_storage: + type: BlockStorage + properties: + size: 1 GB + + + From 9d2eb8176c76e7d43ed309913fab38558c010376 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 27 Oct 2015 12:06:12 +0100 Subject: [PATCH 003/509] Bugfixes in configuration with deleted VMs --- IM/ConfManager.py | 4 +++- IM/InfrastructureManager.py | 8 +++++--- IM/REST.py | 30 +++++++++++++++--------------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 022522acb..5ede39cdc 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -189,7 +189,9 @@ def run(self): last_step = step else: if isinstance(vm,VirtualMachine): - if vm.is_configured() is False: + if vm.destroy: + ConfManager.logger.warn("Inf ID: " + str(self.inf.id) + ": VM ID " + str(vm.im_id) + " has been destroyed. Not launching new tasks for it.") + elif vm.is_configured() is False: ConfManager.logger.debug("Inf ID: " + str(self.inf.id) + ": Configuration process of step " + str(last_step) + " failed, ignoring tasks of later steps.") # Check that the VM has no other ansible process running elif vm.ctxt_pid: diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 2cf4a9c76..533054268 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -580,11 +580,13 @@ def RemoveResource(inf_id, vm_list, auth, context = True): if str(vm.im_id) == str(vmid): InfrastructureManager.logger.debug("Removing the VM ID: '" + vmid + "'") try: - vm.finalize(auth) + success, msg = vm.finalize(auth) + if success: + cont += 1 + else: + exceptions.append(msg) except Exception, e: exceptions.append(e) - else: - cont += 1 InfrastructureManager.save_data(inf_id) InfrastructureManager.logger.info(str(cont) + " VMs successfully removed") diff --git a/IM/REST.py b/IM/REST.py index 83f948848..1d4146d2e 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -63,7 +63,7 @@ def RESTDestroyInfrastructure(id=None): bottle.abort(401, "No authentication data provided") try: - InfrastructureManager.DestroyInfrastructure(int(id), auth) + InfrastructureManager.DestroyInfrastructure(id, auth) return "" except DeletedInfrastructureException, ex: bottle.abort(404, "Error Destroying Inf: " + str(ex)) @@ -84,7 +84,7 @@ def RESTGetInfrastructureInfo(id=None): bottle.abort(401, "No authentication data provided") try: - vm_ids = InfrastructureManager.GetInfrastructureInfo(int(id), auth) + vm_ids = InfrastructureManager.GetInfrastructureInfo(id, auth) res = "" server_ip = bottle.request.environ['SERVER_NAME'] @@ -116,9 +116,9 @@ def RESTGetInfrastructureProperty(id=None, prop=None): try: if prop == "contmsg": - res = InfrastructureManager.GetInfrastructureContMsg(int(id), auth) + res = InfrastructureManager.GetInfrastructureContMsg(id, auth) elif prop == "radl": - res = InfrastructureManager.GetInfrastructureRADL(int(id), auth) + res = InfrastructureManager.GetInfrastructureRADL(id, auth) else: bottle.abort(403, "Incorrect infrastructure property") bottle.response.content_type = "text/plain" @@ -193,7 +193,7 @@ def RESTGetVMInfo(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: - info = InfrastructureManager.GetVMInfo(int(infid), vmid, auth) + info = InfrastructureManager.GetVMInfo(infid, vmid, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: @@ -222,9 +222,9 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): try: if prop == 'contmsg': - info = InfrastructureManager.GetVMContMsg(int(infid), vmid, auth) + info = InfrastructureManager.GetVMContMsg(infid, vmid, auth) else: - info = InfrastructureManager.GetVMProperty(int(infid), vmid, prop, auth) + info = InfrastructureManager.GetVMProperty(infid, vmid, prop, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: @@ -263,7 +263,7 @@ def RESTAddResource(id=None): bottle.abort(400, "Incorrect value in context parameter") radl_data = bottle.request.body.read() - vm_ids = InfrastructureManager.AddResource(int(id), radl_data, auth, context) + vm_ids = InfrastructureManager.AddResource(id, radl_data, auth, context) server_ip = bottle.request.environ['SERVER_NAME'] server_port = bottle.request.environ['SERVER_PORT'] @@ -305,7 +305,7 @@ def RESTRemoveResource(infid=None, vmid=None): else: bottle.abort(400, "Incorrect value in context parameter") - InfrastructureManager.RemoveResource(int(infid), vmid, auth, context) + InfrastructureManager.RemoveResource(infid, vmid, auth, context) return "" except DeletedInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) @@ -335,7 +335,7 @@ def RESTAlterVM(infid=None, vmid=None): radl_data = bottle.request.body.read() bottle.response.content_type = "text/plain" - return InfrastructureManager.AlterVM(int(infid), vmid, radl_data, auth) + return InfrastructureManager.AlterVM(infid, vmid, radl_data, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) return False @@ -373,7 +373,7 @@ def RESTReconfigureInfrastructure(id=None): radl_data = bottle.request.forms.get('radl') else: radl_data = "" - return InfrastructureManager.Reconfigure(int(id), radl_data, auth, vm_list) + return InfrastructureManager.Reconfigure(id, radl_data, auth, vm_list) except DeletedInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) return False @@ -393,7 +393,7 @@ def RESTStartInfrastructure(id=None): bottle.abort(401, "No authentication data provided") try: - return InfrastructureManager.StartInfrastructure(int(id), auth) + return InfrastructureManager.StartInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error starting infrastructure: " + str(ex)) return False @@ -413,7 +413,7 @@ def RESTStopInfrastructure(id=None): bottle.abort(401, "No authentication data provided") try: - return InfrastructureManager.StopInfrastructure(int(id), auth) + return InfrastructureManager.StopInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error stopping infrastructure: " + str(ex)) return False @@ -433,7 +433,7 @@ def RESTStartVM(infid=None, vmid=None, prop=None): bottle.abort(401, "No authentication data provided") try: - info = InfrastructureManager.StartVM(int(infid), vmid, auth) + info = InfrastructureManager.StartVM(infid, vmid, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: @@ -461,7 +461,7 @@ def RESTStopVM(infid=None, vmid=None, prop=None): bottle.abort(401, "No authentication data provided") try: - info = InfrastructureManager.StopVM(int(infid), vmid, auth) + info = InfrastructureManager.StopVM(infid, vmid, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: From ad67aebb0d9dc2f616596e6e22d3eb4f0c9d7841 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 27 Oct 2015 12:06:12 +0100 Subject: [PATCH 004/509] Bugfixes in configuration with deleted VMs --- IM/ConfManager.py | 4 +++- IM/InfrastructureManager.py | 8 +++++--- IM/REST.py | 30 +++++++++++++++--------------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 022522acb..5ede39cdc 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -189,7 +189,9 @@ def run(self): last_step = step else: if isinstance(vm,VirtualMachine): - if vm.is_configured() is False: + if vm.destroy: + ConfManager.logger.warn("Inf ID: " + str(self.inf.id) + ": VM ID " + str(vm.im_id) + " has been destroyed. Not launching new tasks for it.") + elif vm.is_configured() is False: ConfManager.logger.debug("Inf ID: " + str(self.inf.id) + ": Configuration process of step " + str(last_step) + " failed, ignoring tasks of later steps.") # Check that the VM has no other ansible process running elif vm.ctxt_pid: diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 2cf4a9c76..533054268 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -580,11 +580,13 @@ def RemoveResource(inf_id, vm_list, auth, context = True): if str(vm.im_id) == str(vmid): InfrastructureManager.logger.debug("Removing the VM ID: '" + vmid + "'") try: - vm.finalize(auth) + success, msg = vm.finalize(auth) + if success: + cont += 1 + else: + exceptions.append(msg) except Exception, e: exceptions.append(e) - else: - cont += 1 InfrastructureManager.save_data(inf_id) InfrastructureManager.logger.info(str(cont) + " VMs successfully removed") diff --git a/IM/REST.py b/IM/REST.py index 83f948848..1d4146d2e 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -63,7 +63,7 @@ def RESTDestroyInfrastructure(id=None): bottle.abort(401, "No authentication data provided") try: - InfrastructureManager.DestroyInfrastructure(int(id), auth) + InfrastructureManager.DestroyInfrastructure(id, auth) return "" except DeletedInfrastructureException, ex: bottle.abort(404, "Error Destroying Inf: " + str(ex)) @@ -84,7 +84,7 @@ def RESTGetInfrastructureInfo(id=None): bottle.abort(401, "No authentication data provided") try: - vm_ids = InfrastructureManager.GetInfrastructureInfo(int(id), auth) + vm_ids = InfrastructureManager.GetInfrastructureInfo(id, auth) res = "" server_ip = bottle.request.environ['SERVER_NAME'] @@ -116,9 +116,9 @@ def RESTGetInfrastructureProperty(id=None, prop=None): try: if prop == "contmsg": - res = InfrastructureManager.GetInfrastructureContMsg(int(id), auth) + res = InfrastructureManager.GetInfrastructureContMsg(id, auth) elif prop == "radl": - res = InfrastructureManager.GetInfrastructureRADL(int(id), auth) + res = InfrastructureManager.GetInfrastructureRADL(id, auth) else: bottle.abort(403, "Incorrect infrastructure property") bottle.response.content_type = "text/plain" @@ -193,7 +193,7 @@ def RESTGetVMInfo(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: - info = InfrastructureManager.GetVMInfo(int(infid), vmid, auth) + info = InfrastructureManager.GetVMInfo(infid, vmid, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: @@ -222,9 +222,9 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): try: if prop == 'contmsg': - info = InfrastructureManager.GetVMContMsg(int(infid), vmid, auth) + info = InfrastructureManager.GetVMContMsg(infid, vmid, auth) else: - info = InfrastructureManager.GetVMProperty(int(infid), vmid, prop, auth) + info = InfrastructureManager.GetVMProperty(infid, vmid, prop, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: @@ -263,7 +263,7 @@ def RESTAddResource(id=None): bottle.abort(400, "Incorrect value in context parameter") radl_data = bottle.request.body.read() - vm_ids = InfrastructureManager.AddResource(int(id), radl_data, auth, context) + vm_ids = InfrastructureManager.AddResource(id, radl_data, auth, context) server_ip = bottle.request.environ['SERVER_NAME'] server_port = bottle.request.environ['SERVER_PORT'] @@ -305,7 +305,7 @@ def RESTRemoveResource(infid=None, vmid=None): else: bottle.abort(400, "Incorrect value in context parameter") - InfrastructureManager.RemoveResource(int(infid), vmid, auth, context) + InfrastructureManager.RemoveResource(infid, vmid, auth, context) return "" except DeletedInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) @@ -335,7 +335,7 @@ def RESTAlterVM(infid=None, vmid=None): radl_data = bottle.request.body.read() bottle.response.content_type = "text/plain" - return InfrastructureManager.AlterVM(int(infid), vmid, radl_data, auth) + return InfrastructureManager.AlterVM(infid, vmid, radl_data, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) return False @@ -373,7 +373,7 @@ def RESTReconfigureInfrastructure(id=None): radl_data = bottle.request.forms.get('radl') else: radl_data = "" - return InfrastructureManager.Reconfigure(int(id), radl_data, auth, vm_list) + return InfrastructureManager.Reconfigure(id, radl_data, auth, vm_list) except DeletedInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) return False @@ -393,7 +393,7 @@ def RESTStartInfrastructure(id=None): bottle.abort(401, "No authentication data provided") try: - return InfrastructureManager.StartInfrastructure(int(id), auth) + return InfrastructureManager.StartInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error starting infrastructure: " + str(ex)) return False @@ -413,7 +413,7 @@ def RESTStopInfrastructure(id=None): bottle.abort(401, "No authentication data provided") try: - return InfrastructureManager.StopInfrastructure(int(id), auth) + return InfrastructureManager.StopInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error stopping infrastructure: " + str(ex)) return False @@ -433,7 +433,7 @@ def RESTStartVM(infid=None, vmid=None, prop=None): bottle.abort(401, "No authentication data provided") try: - info = InfrastructureManager.StartVM(int(infid), vmid, auth) + info = InfrastructureManager.StartVM(infid, vmid, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: @@ -461,7 +461,7 @@ def RESTStopVM(infid=None, vmid=None, prop=None): bottle.abort(401, "No authentication data provided") try: - info = InfrastructureManager.StopVM(int(infid), vmid, auth) + info = InfrastructureManager.StopVM(infid, vmid, auth) bottle.response.content_type = "text/plain" return info except DeletedInfrastructureException, ex: From 212a1ef8c1daa1fbb179ea0f2b5a42d379bd53e7 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 08:49:00 +0100 Subject: [PATCH 005/509] Bugfix in REST GetVMProperty with integer data --- IM/REST.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 1d4146d2e..d2e86ed38 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -226,7 +226,7 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): else: info = InfrastructureManager.GetVMProperty(infid, vmid, prop, auth) bottle.response.content_type = "text/plain" - return info + return str(info) except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) return False @@ -478,4 +478,4 @@ def RESTStopVM(infid=None, vmid=None, prop=None): return False except Exception, ex: bottle.abort(400, "Error stopping VM: " + str(ex)) - return False \ No newline at end of file + return False From 209ee1b565e1a7ca966c850a6fa05f5d72ffe1ce Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 08:49:00 +0100 Subject: [PATCH 006/509] Bugfix in REST GetVMProperty with integer data --- IM/REST.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 1d4146d2e..d2e86ed38 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -226,7 +226,7 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): else: info = InfrastructureManager.GetVMProperty(infid, vmid, prop, auth) bottle.response.content_type = "text/plain" - return info + return str(info) except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) return False @@ -478,4 +478,4 @@ def RESTStopVM(infid=None, vmid=None, prop=None): return False except Exception, ex: bottle.abort(400, "Error stopping VM: " + str(ex)) - return False \ No newline at end of file + return False From f83d156912f302b5bed95647dc5ab352b66161f8 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:17:51 +0100 Subject: [PATCH 007/509] Bugfix when stopping the im service --- IM/InfrastructureInfo.py | 11 +- IM/InfrastructureManager.py | 8 +- IM/tosca/toscaparser/__init__.py | 19 - IM/tosca/toscaparser/capabilities.py | 57 -- IM/tosca/toscaparser/common/__init__.py | 0 IM/tosca/toscaparser/common/exception.py | 100 -- IM/tosca/toscaparser/dataentity.py | 159 ---- .../elements/TOSCA_definition_1_0.yaml | 893 ------------------ IM/tosca/toscaparser/elements/__init__.py | 0 IM/tosca/toscaparser/elements/artifacttype.py | 45 - .../elements/attribute_definition.py | 20 - .../toscaparser/elements/capabilitytype.py | 71 -- IM/tosca/toscaparser/elements/constraints.py | 569 ----------- IM/tosca/toscaparser/elements/datatype.py | 56 -- IM/tosca/toscaparser/elements/entity_type.py | 113 --- IM/tosca/toscaparser/elements/interfaces.py | 74 -- IM/tosca/toscaparser/elements/nodetype.py | 200 ---- IM/tosca/toscaparser/elements/policytype.py | 45 - .../elements/property_definition.py | 46 - .../toscaparser/elements/relationshiptype.py | 33 - IM/tosca/toscaparser/elements/scalarunit.py | 130 --- .../elements/statefulentitytype.py | 81 -- IM/tosca/toscaparser/entity_template.py | 285 ------ IM/tosca/toscaparser/functions.py | 410 -------- IM/tosca/toscaparser/groups.py | 27 - IM/tosca/toscaparser/nodetemplate.py | 242 ----- IM/tosca/toscaparser/parameters.py | 110 --- IM/tosca/toscaparser/prereq/__init__.py | 0 IM/tosca/toscaparser/prereq/csar.py | 122 --- IM/tosca/toscaparser/properties.py | 79 -- IM/tosca/toscaparser/relationship_template.py | 68 -- IM/tosca/toscaparser/topology_template.py | 213 ----- IM/tosca/toscaparser/tosca_template.py | 190 ---- .../toscaparser/tpl_relationship_graph.py | 46 - IM/tosca/toscaparser/utils/__init__.py | 0 IM/tosca/toscaparser/utils/gettextutils.py | 22 - IM/tosca/toscaparser/utils/urlutils.py | 43 - IM/tosca/toscaparser/utils/validateutils.py | 154 --- IM/tosca/toscaparser/utils/yamlparser.py | 73 -- 39 files changed, 12 insertions(+), 4802 deletions(-) delete mode 100644 IM/tosca/toscaparser/__init__.py delete mode 100644 IM/tosca/toscaparser/capabilities.py delete mode 100644 IM/tosca/toscaparser/common/__init__.py delete mode 100644 IM/tosca/toscaparser/common/exception.py delete mode 100644 IM/tosca/toscaparser/dataentity.py delete mode 100644 IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml delete mode 100644 IM/tosca/toscaparser/elements/__init__.py delete mode 100644 IM/tosca/toscaparser/elements/artifacttype.py delete mode 100644 IM/tosca/toscaparser/elements/attribute_definition.py delete mode 100644 IM/tosca/toscaparser/elements/capabilitytype.py delete mode 100644 IM/tosca/toscaparser/elements/constraints.py delete mode 100644 IM/tosca/toscaparser/elements/datatype.py delete mode 100644 IM/tosca/toscaparser/elements/entity_type.py delete mode 100644 IM/tosca/toscaparser/elements/interfaces.py delete mode 100644 IM/tosca/toscaparser/elements/nodetype.py delete mode 100644 IM/tosca/toscaparser/elements/policytype.py delete mode 100644 IM/tosca/toscaparser/elements/property_definition.py delete mode 100644 IM/tosca/toscaparser/elements/relationshiptype.py delete mode 100644 IM/tosca/toscaparser/elements/scalarunit.py delete mode 100644 IM/tosca/toscaparser/elements/statefulentitytype.py delete mode 100644 IM/tosca/toscaparser/entity_template.py delete mode 100644 IM/tosca/toscaparser/functions.py delete mode 100644 IM/tosca/toscaparser/groups.py delete mode 100644 IM/tosca/toscaparser/nodetemplate.py delete mode 100644 IM/tosca/toscaparser/parameters.py delete mode 100644 IM/tosca/toscaparser/prereq/__init__.py delete mode 100644 IM/tosca/toscaparser/prereq/csar.py delete mode 100644 IM/tosca/toscaparser/properties.py delete mode 100644 IM/tosca/toscaparser/relationship_template.py delete mode 100644 IM/tosca/toscaparser/topology_template.py delete mode 100644 IM/tosca/toscaparser/tosca_template.py delete mode 100644 IM/tosca/toscaparser/tpl_relationship_graph.py delete mode 100644 IM/tosca/toscaparser/utils/__init__.py delete mode 100644 IM/tosca/toscaparser/utils/gettextutils.py delete mode 100644 IM/tosca/toscaparser/utils/urlutils.py delete mode 100644 IM/tosca/toscaparser/utils/validateutils.py delete mode 100644 IM/tosca/toscaparser/utils/yamlparser.py diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 9d24078af..d18ed58d6 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -119,16 +119,21 @@ def delete(self): """ Set this Inf as deleted """ - self.stop_cm_thread() + self.stop() self.deleted = True - def stop_cm_thread(self): + def stop(self): """ - Stop the Ctxt thread if is is alive. + Stop all the Ctxt threads """ + # Stop the Ctxt thread if it is alive. if self.cm and self.cm.isAlive(): self.cm.stop() + # kill all the ctxt processes in the VMs + for vm in self.get_vm_list(): + vm.kill_check_ctxt_process() + def get_cont_out(self): """ Returns the contextualization message diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 533054268..a6578e513 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1052,7 +1052,7 @@ def CreateInfrastructure(radl, auth): InfrastructureManager.logger.exception("Error Creating Inf id " + str(inf.id)) inf.delete() InfrastructureManager.save_data(inf.id) - InfrastructureManager.remove_inf(inf.id) + InfrastructureManager.remove_inf(inf) raise e InfrastructureManager.logger.info("Infrastructure id " + str(inf.id) + " successfully created") @@ -1094,7 +1094,7 @@ def ExportInfrastructure(inf_id, delete, auth_data): if delete: sel_inf.delete() InfrastructureManager.save_data(sel_inf.id) - InfrastructureManager.remove_inf(sel_inf.id) + InfrastructureManager.remove_inf(sel_inf) return str_inf @staticmethod @@ -1202,6 +1202,6 @@ def stop(): # Acquire the lock to avoid writing data to the DATA_FILE with InfrastructureManager._lock: InfrastructureManager._exiting = True - # Stop all the Ctxt threads of the + # Stop all the Ctxt threads of the Infrastructure for inf in InfrastructureManager.infrastructure_list.values(): - inf.stop_cm_thread() \ No newline at end of file + inf.stop() \ No newline at end of file diff --git a/IM/tosca/toscaparser/__init__.py b/IM/tosca/toscaparser/__init__.py deleted file mode 100644 index f418d00a9..000000000 --- a/IM/tosca/toscaparser/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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. - -import pbr.version - -__version__ = "1.0.0" -#__version__ = pbr.version.VersionInfo( -# 'tosca-parser').version_string() diff --git a/IM/tosca/toscaparser/capabilities.py b/IM/tosca/toscaparser/capabilities.py deleted file mode 100644 index 5af77f862..000000000 --- a/IM/tosca/toscaparser/capabilities.py +++ /dev/null @@ -1,57 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.properties import Property - - -class Capability(object): - '''TOSCA built-in capabilities type.''' - - def __init__(self, name, properties, definition): - self.name = name - self._properties = properties - self.definition = definition - - def get_properties_objects(self): - '''Return a list of property objects.''' - properties = [] - # Miguel: cambios aqui - props_def = self.definition.get_properties_def() - if props_def: - props_name = props_def.keys() - - for name in props_name: - value = None - if name in self._properties: - value = self._properties[name] - properties.append(Property(name, value, props_def[name].schema)) - -# props = self._properties -# -# if props: -# for name, value in props.items(): -# props_def = self.definition.get_properties_def() -# if props_def and name in props_def: -# properties.append(Property(name, value, -# props_def[name].schema)) - return properties - - def get_properties(self): - '''Return a dictionary of property name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_objects()} - - def get_property_value(self, name): - '''Return the value of a given property name.''' - props = self.get_properties() - if props and name in props: - return props[name].value diff --git a/IM/tosca/toscaparser/common/__init__.py b/IM/tosca/toscaparser/common/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/common/exception.py b/IM/tosca/toscaparser/common/exception.py deleted file mode 100644 index fb2eea9e6..000000000 --- a/IM/tosca/toscaparser/common/exception.py +++ /dev/null @@ -1,100 +0,0 @@ -# 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. - -''' -TOSCA exception classes -''' -import logging -import sys - -from IM.tosca.toscaparser.utils.gettextutils import _ - - -log = logging.getLogger(__name__) - - -class TOSCAException(Exception): - '''Base exception class for TOSCA - - To correctly use this class, inherit from it and define - a 'msg_fmt' property. - - ''' - - _FATAL_EXCEPTION_FORMAT_ERRORS = False - - message = _('An unknown exception occurred.') - - def __init__(self, **kwargs): - try: - self.message = self.msg_fmt % kwargs - except KeyError: - exc_info = sys.exc_info() - log.exception(_('Exception in string format operation: %s') - % exc_info[1]) - - if TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS: - raise exc_info[0] - - def __str__(self): - return self.message - - @staticmethod - def set_fatal_format_exception(flag): - if isinstance(flag, bool): - TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS = flag - - -class MissingRequiredFieldError(TOSCAException): - msg_fmt = _('%(what)s is missing required field: "%(required)s".') - - -class UnknownFieldError(TOSCAException): - msg_fmt = _('%(what)s contain(s) unknown field: "%(field)s", ' - 'refer to the definition to verify valid values.') - - -class TypeMismatchError(TOSCAException): - msg_fmt = _('%(what)s must be of type: "%(type)s".') - - -class InvalidNodeTypeError(TOSCAException): - msg_fmt = _('Node type "%(what)s" is not a valid type.') - - -class InvalidTypeError(TOSCAException): - msg_fmt = _('Type "%(what)s" is not a valid type.') - - -class InvalidSchemaError(TOSCAException): - msg_fmt = _("%(message)s") - - -class ValidationError(TOSCAException): - msg_fmt = _("%(message)s") - - -class UnknownInputError(TOSCAException): - msg_fmt = _('Unknown input: %(input_name)s') - - -class InvalidPropertyValueError(TOSCAException): - msg_fmt = _('Value of property "%(what)s" is invalid.') - - -class InvalidTemplateVersion(TOSCAException): - msg_fmt = _('The template version "%(what)s" is invalid. ' - 'The valid versions are: "%(valid_versions)s"') - - -class InvalidTOSCAVersionPropertyException(TOSCAException): - msg_fmt = _('Value of TOSCA version property "%(what)s" is invalid.') diff --git a/IM/tosca/toscaparser/dataentity.py b/IM/tosca/toscaparser/dataentity.py deleted file mode 100644 index eb67e63a4..000000000 --- a/IM/tosca/toscaparser/dataentity.py +++ /dev/null @@ -1,159 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import TypeMismatchError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.elements.constraints import Schema -from IM.tosca.toscaparser.elements.datatype import DataType -from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Frequency -from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Size -from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Time - -from IM.tosca.toscaparser.utils.gettextutils import _ -from IM.tosca.toscaparser.utils import validateutils - - -class DataEntity(object): - '''A complex data value entity.''' - - def __init__(self, datatypename, value_dict, custom_def=None): - self.custom_def = custom_def - self.datatype = DataType(datatypename, custom_def) - self.schema = self.datatype.get_all_properties() - self.value = value_dict - - def validate(self): - '''Validate the value by the definition of the datatype.''' - - # A datatype can not have both 'type' and 'properties' definitions. - # If the datatype has 'type' definition - if self.datatype.value_type: - self.value = DataEntity.validate_datatype(self.datatype.value_type, - self.value, - None, - self.custom_def) - schema = Schema(None, self.datatype.defs) - for constraint in schema.constraints: - constraint.validate(self.value) - # If the datatype has 'properties' definition - else: - if not isinstance(self.value, dict): - raise TypeMismatchError(what=self.value, - type=self.datatype.type) - allowed_props = [] - required_props = [] - default_props = {} - if self.schema: - allowed_props = self.schema.keys() - for name, prop_def in self.schema.items(): - if prop_def.required: - required_props.append(name) - if prop_def.default: - default_props[name] = prop_def.default - - # check allowed field - for value_key in list(self.value.keys()): - if value_key not in allowed_props: - raise UnknownFieldError(what=_('Data value of type %s') - % self.datatype.type, - field=value_key) - - # check default field - for def_key, def_value in list(default_props.items()): - if def_key not in list(self.value.keys()): - self.value[def_key] = def_value - - # check missing field - missingprop = [] - for req_key in required_props: - if req_key not in list(self.value.keys()): - missingprop.append(req_key) - if missingprop: - raise MissingRequiredFieldError(what=_('Data value of type %s') - % self.datatype.type, - required=missingprop) - - # check every field - for name, value in list(self.value.items()): - prop_schema = Schema(name, self._find_schema(name)) - # check if field value meets type defined - DataEntity.validate_datatype(prop_schema.type, value, - prop_schema.entry_schema, - self.custom_def) - # check if field value meets constraints defined - if prop_schema.constraints: - for constraint in prop_schema.constraints: - constraint.validate(value) - - return self.value - - def _find_schema(self, name): - if self.schema and name in self.schema.keys(): - return self.schema[name].schema - - @staticmethod - def validate_datatype(type, value, entry_schema=None, custom_def=None): - '''Validate value with given type. - - If type is list or map, validate its entry by entry_schema(if defined) - If type is a user-defined complex datatype, custom_def is required. - ''' - if type == Schema.STRING: - return validateutils.validate_string(value) - elif type == Schema.INTEGER: - return validateutils.validate_integer(value) - elif type == Schema.FLOAT: - return validateutils.validate_float(value) - elif type == Schema.NUMBER: - return validateutils.validate_number(value) - elif type == Schema.BOOLEAN: - return validateutils.validate_boolean(value) - elif type == Schema.TIMESTAMP: - validateutils.validate_timestamp(value) - return value - elif type == Schema.LIST: - validateutils.validate_list(value) - if entry_schema: - DataEntity.validate_entry(value, entry_schema, custom_def) - return value - elif type == Schema.SCALAR_UNIT_SIZE: - return ScalarUnit_Size(value).validate_scalar_unit() - elif type == Schema.SCALAR_UNIT_FREQUENCY: - return ScalarUnit_Frequency(value).validate_scalar_unit() - elif type == Schema.SCALAR_UNIT_TIME: - return ScalarUnit_Time(value).validate_scalar_unit() - elif type == Schema.VERSION: - return validateutils.TOSCAVersionProperty(value).get_version() - elif type == Schema.MAP: - validateutils.validate_map(value) - if entry_schema: - DataEntity.validate_entry(value, entry_schema, custom_def) - return value - else: - data = DataEntity(type, value, custom_def) - return data.validate() - - @staticmethod - def validate_entry(value, entry_schema, custom_def=None): - '''Validate entries for map and list.''' - schema = Schema(None, entry_schema) - valuelist = value - if isinstance(value, dict): - valuelist = list(value.values()) - for v in valuelist: - DataEntity.validate_datatype(schema.type, v, schema.entry_schema, - custom_def) - if schema.constraints: - for constraint in schema.constraints: - constraint.validate(v) - return value diff --git a/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml b/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml deleted file mode 100644 index b819c02b4..000000000 --- a/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml +++ /dev/null @@ -1,893 +0,0 @@ -# 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. - -########################################################################## -# The content of this file reflects TOSCA Simple Profile in YAML version -# 1.0.0. It describes the definition for TOSCA types including Node Type, -# Relationship Type, Capability Type and Interfaces. -########################################################################## -tosca_definitions_version: tosca_simple_yaml_1_0 - -########################################################################## -# Node Type. -# A Node Type is a reusable entity that defines the type of one or more -# Node Templates. -########################################################################## -tosca.nodes.Root: - description: > - The TOSCA root node all other TOSCA base node types derive from. - attributes: - tosca_id: - type: string - tosca_name: - type: string - state: - type: string - capabilities: - feature: - type: tosca.capabilities.Node - requirements: - - dependency: - capability: tosca.capabilities.Node - node: tosca.nodes.Root - relationship: tosca.relationships.DependsOn - occurrences: [ 0, UNBOUNDED ] - interfaces: - Standard: - type: tosca.interfaces.node.lifecycle.Standard - -tosca.nodes.Compute: - derived_from: tosca.nodes.Root - attributes: - private_address: - type: string - public_address: - type: string - capabilities: - host: - type: tosca.capabilities.Container - binding: - type: tosca.capabilities.network.Bindable - os: - type: tosca.capabilities.OperatingSystem - scalable: - type: tosca.capabilities.Scalable - requirements: - - local_storage: - capability: tosca.capabilities.Attachment - node: tosca.nodes.BlockStorage - relationship: tosca.relationships.AttachesTo - occurrences: [0, UNBOUNDED] - -tosca.nodes.SoftwareComponent: - derived_from: tosca.nodes.Root - properties: - # domain-specific software component version - component_version: - type: version - required: false - description: > - Software component version. - admin_credential: - type: tosca.datatypes.Credential - required: false - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.Compute - relationship: tosca.relationships.HostedOn - -tosca.nodes.DBMS: - derived_from: tosca.nodes.SoftwareComponent - properties: - port: - required: no - type: integer - description: > - The port the DBMS service will listen to for data and requests. - root_password: - required: no - type: string - description: > - The root password for the DBMS service. - capabilities: - host: - type: tosca.capabilities.Container - valid_source_types: [tosca.nodes.Database] - -tosca.nodes.Database: - derived_from: tosca.nodes.Root - properties: - user: - required: no - type: string - description: > - User account name for DB administration - name: - required: no - type: string - description: > - The name of the database. - password: - required: no - type: string - description: > - The password for the DB user account - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.DBMS - relationship: tosca.relationships.HostedOn - capabilities: - database_endpoint: - type: tosca.capabilities.Endpoint.Database - -tosca.nodes.WebServer: - derived_from: tosca.nodes.SoftwareComponent - capabilities: - data_endpoint: - type: tosca.capabilities.Endpoint - admin_endpoint: - type: tosca.capabilities.Endpoint.Admin - host: - type: tosca.capabilities.Container - valid_source_types: [tosca.nodes.WebApplication] - -tosca.nodes.WebApplication: - derived_from: tosca.nodes.Root - properties: - context_root: - type: string - required: false - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.WebServer - relationship: tosca.relationships.HostedOn - capabilities: - app_endpoint: - type: tosca.capabilities.Endpoint - -tosca.nodes.BlockStorage: - derived_from: tosca.nodes.Root - properties: - size: - type: scalar-unit.size - constraints: - - greater_or_equal: 1 MB - volume_id: - type: string - required: false - snapshot_id: - type: string - required: false - attributes: - volume_id: - type: string - capabilities: - attachment: - type: tosca.capabilities.Attachment - -tosca.nodes.network.Network: - derived_from: tosca.nodes.Root - description: > - The TOSCA Network node represents a simple, logical network service. - properties: - ip_version: - type: integer - required: no - default: 4 - constraints: - - valid_values: [ 4, 6 ] - description: > - The IP version of the requested network. Valid values are 4 for ipv4 - or 6 for ipv6. - cidr: - type: string - required: no - description: > - The cidr block of the requested network. - start_ip: - type: string - required: no - description: > - The IP address to be used as the start of a pool of addresses within - the full IP range derived from the cidr block. - end_ip: - type: string - required: no - description: > - The IP address to be used as the end of a pool of addresses within - the full IP range derived from the cidr block. - gateway_ip: - type: string - required: no - description: > - The gateway IP address. - network_name: - type: string - required: no - description: > - An identifier that represents an existing Network instance in the - underlying cloud infrastructure or can be used as the name of the - newly created network. If network_name is provided and no other - properties are provided (with exception of network_id), then an - existing network instance will be used. If network_name is provided - alongside with more properties then a new network with this name will - be created. - network_id: - type: string - required: no - description: > - An identifier that represents an existing Network instance in the - underlying cloud infrastructure. This property is mutually exclusive - with all other properties except network_name. This can be used alone - or together with network_name to identify an existing network. - network_type: - type: string - required: no - description: > - It specifies the nature of the physical network in the underlying - cloud infrastructure. Examples are flat, vlan, gre or vxlan. F - segmentation_id: - type: string - required: no - description: > - A segmentation identifier in the underlying cloud infrastructure. - E.g. VLAN ID, GRE tunnel ID, etc.. - dhcp_enabled: - type: boolean - required: no - default: true - description: > - Indicates should DHCP service be enabled on the network or not. - capabilities: - link: - type: tosca.capabilities.network.Linkable - -tosca.nodes.network.Port: - derived_from: tosca.nodes.Root - description: > - The TOSCA Port node represents a logical entity that associates between - Compute and Network normative types. The Port node type effectively - represents a single virtual NIC on the Compute node instance. - properties: - ip_address: - type: string - required: no - description: > - Allow the user to set a static IP. - order: - type: integer - required: no - default: 0 - constraints: - - greater_or_equal: 0 - description: > - The order of the NIC on the compute instance (e.g. eth2). - is_default: - type: boolean - required: no - default: false - description: > - If is_default=true this port will be used for the default gateway - route. Only one port that is associated to single compute node can - set as is_default=true. - ip_range_start: - type: string - required: no - description: > - Defines the starting IP of a range to be allocated for the compute - instances that are associated with this Port. - ip_range_end: - type: string - required: no - description: > - Defines the ending IP of a range to be allocated for the compute - instances that are associated with this Port. - attributes: - ip_address: - type: string - requirements: - - binding: - description: > - Binding requirement expresses the relationship between Port and - Compute nodes. Effectevely it indicates that the Port will be - attached to specific Compute node instance - capability: tosca.capabilities.network.Bindable - relationship: tosca.relationships.network.BindsTo - - link: - description: > - Link requirement expresses the relationship between Port and Network - nodes. It indicates which network this port will connect to. - capability: tosca.capabilities.network.Linkable - relationship: tosca.relationships.network.LinksTo - -tosca.nodes.ObjectStorage: - derived_from: tosca.nodes.Root - description: > - The TOSCA ObjectStorage node represents storage that provides the ability - to store data as objects (or BLOBs of data) without consideration for the - underlying filesystem or devices - properties: - name: - type: string - required: yes - description: > - The logical name of the object store (or container). - size: - type: scalar-unit.size - required: no - constraints: - - greater_or_equal: 0 GB - description: > - The requested initial storage size. - maxsize: - type: scalar-unit.size - required: no - constraints: - - greater_or_equal: 0 GB - description: > - The requested maximum storage size. - capabilities: - storage_endpoint: - type: tosca.capabilities.Endpoint - -########################################################################## -# Relationship Type. -# A Relationship Type is a reusable entity that defines the type of one -# or more relationships between Node Types or Node Templates. -########################################################################## -tosca.relationships.Root: - description: > - The TOSCA root Relationship Type all other TOSCA base Relationship Types - derive from. - attributes: - tosca_id: - type: string - tosca_name: - type: string - interfaces: - Configure: - type: tosca.interfaces.relationship.Configure - -tosca.relationships.DependsOn: - derived_from: tosca.relationships.Root - -tosca.relationships.HostedOn: - derived_from: tosca.relationships.Root - valid_target_types: [ tosca.capabilities.Container ] - -tosca.relationships.ConnectsTo: - derived_from: tosca.relationships.Root - valid_target_types: [ tosca.capabilities.Endpoint ] - credential: - type: tosca.datatypes.Credential - required: false - -tosca.relationships.AttachesTo: - derived_from: tosca.relationships.Root - valid_target_types: [ tosca.capabilities.Attachment ] - properties: - location: - required: true - type: string - constraints: - - min_length: 1 - device: - required: false - type: string - -tosca.relationships.network.LinksTo: - derived_from: tosca.relationships.DependsOn - valid_target_types: [ tosca.capabilities.network.Linkable ] - -tosca.relationships.network.BindsTo: - derived_from: tosca.relationships.DependsOn - valid_target_types: [ tosca.capabilities.network.Bindable ] - -########################################################################## -# Capability Type. -# A Capability Type is a reusable entity that describes a kind of -# capability that a Node Type can declare to expose. -########################################################################## -tosca.capabilities.Root: - description: > - The TOSCA root Capability Type all other TOSCA base Capability Types - derive from. - -tosca.capabilities.Node: - derived_from: tosca.capabilities.Root - -tosca.capabilities.Container: - derived_from: tosca.capabilities.Root - properties: - num_cpus: - required: no - type: integer - constraints: - - greater_or_equal: 1 - cpu_frequency: - required: no - type: scalar-unit.frequency - constraints: - - greater_or_equal: 0.1 GHz - disk_size: - required: no - type: scalar-unit.size - constraints: - - greater_or_equal: 0 MB - mem_size: - required: no - type: scalar-unit.size - constraints: - - greater_or_equal: 0 MB - -tosca.capabilities.Endpoint: - derived_from: tosca.capabilities.Root - properties: - protocol: - type: string - default: tcp - port: - type: tosca.datatypes.network.PortDef - required: false - secure: - type: boolean - default: false - url_path: - type: string - required: false - port_name: - type: string - required: false - network_name: - type: string - required: false - initiator: - type: string - default: source - constraints: - - valid_values: [source, target, peer] - ports: - type: map - required: false - constraints: - - min_length: 1 - entry_schema: - type: tosca.datatypes.network.PortDef - attributes: - ip_address: - type: string - -tosca.capabilities.Endpoint.Admin: - derived_from: tosca.capabilities.Endpoint - properties: - secure: true - -tosca.capabilities.Scalable: - derived_from: tosca.capabilities.Root - properties: - min_instances: - type: integer - required: yes - default: 1 - description: > - This property is used to indicate the minimum number of instances - that should be created for the associated TOSCA Node Template by - a TOSCA orchestrator. - max_instances: - type: integer - required: yes - default: 1 - description: > - This property is used to indicate the maximum number of instances - that should be created for the associated TOSCA Node Template by - a TOSCA orchestrator. - default_instances: - type: integer - required: no - description: > - An optional property that indicates the requested default number - of instances that should be the starting number of instances a - TOSCA orchestrator should attempt to allocate. - The value for this property MUST be in the range between the values - set for min_instances and max_instances properties. - -tosca.capabilities.Endpoint.Database: - derived_from: tosca.capabilities.Endpoint - -tosca.capabilities.Attachment: - derived_from: tosca.capabilities.Root - -tosca.capabilities.network.Linkable: - derived_from: tosca.capabilities.Root - description: > - A node type that includes the Linkable capability indicates that it can - be pointed by tosca.relationships.network.LinksTo relationship type, which - represents an association relationship between Port and Network node types. - -tosca.capabilities.network.Bindable: - derived_from: tosca.capabilities.Root - description: > - A node type that includes the Bindable capability indicates that it can - be pointed by tosca.relationships.network.BindsTo relationship type, which - represents a network association relationship between Port and Compute node - types. - -tosca.capabilities.OperatingSystem: - derived_from: tosca.capabilities.Root - properties: - architecture: - required: false - type: string - description: > - The host Operating System (OS) architecture. - type: - required: false - type: string - description: > - The host Operating System (OS) type. - distribution: - required: false - type: string - description: > - The host Operating System (OS) distribution. Examples of valid values - for an “type” of “Linux” would include: - debian, fedora, rhel and ubuntu. - version: - required: false - type: version - description: > - The host Operating System version. - -########################################################################## - # Interfaces Type. - # The Interfaces element describes a list of one or more interface - # definitions for a modelable entity (e.g., a Node or Relationship Type) - # as defined within the TOSCA Simple Profile specification. -########################################################################## -tosca.interfaces.node.lifecycle.Standard: - create: - description: Standard lifecycle create operation. - configure: - description: Standard lifecycle configure operation. - start: - description: Standard lifecycle start operation. - stop: - description: Standard lifecycle stop operation. - delete: - description: Standard lifecycle delete operation. - -tosca.interfaces.relationship.Configure: - pre_configure_source: - description: Operation to pre-configure the source endpoint. - pre_configure_target: - description: Operation to pre-configure the target endpoint. - post_configure_source: - description: Operation to post-configure the source endpoint. - post_configure_target: - description: Operation to post-configure the target endpoint. - add_target: - description: Operation to add a target node. - remove_target: - description: Operation to remove a target node. - add_source: > - description: Operation to notify the target node of a source node which - is now available via a relationship. - description: - target_changed: > - description: Operation to notify source some property or attribute of the - target changed - -########################################################################## - # Data Type. - # A Datatype is a complex data type declaration which contains other - # complex or simple data types. -########################################################################## -tosca.datatypes.network.NetworkInfo: - properties: - network_name: - type: string - network_id: - type: string - addresses: - type: list - entry_schema: - type: string - -tosca.datatypes.network.PortInfo: - properties: - port_name: - type: string - port_id: - type: string - network_id: - type: string - mac_address: - type: string - addresses: - type: list - entry_schema: - type: string - -tosca.datatypes.network.PortDef: - type: integer - constraints: - - in_range: [ 1, 65535 ] - -tosca.datatypes.network.PortSpec: - properties: - protocol: - type: string - required: true - default: tcp - constraints: - - valid_values: [ udp, tcp, igmp ] - target: - type: list - entry_schema: - type: PortDef - target_range: - type: range - constraints: - - in_range: [ 1, 65535 ] - source: - type: list - entry_schema: - type: PortDef - source_range: - type: range - constraints: - - in_range: [ 1, 65535 ] - -tosca.datatypes.Credential: - properties: - protocol: - type: string - token_type: - type: string - token: - type: string - keys: - type: map - entry_schema: - type: string - user: - type: string - required: false - -########################################################################## - # Artifact Type. - # An Artifact Type is a reusable entity that defines the type of one or more - # files which Node Types or Node Templates can have dependent relationships - # and used during operations such as during installation or deployment. -########################################################################## -tosca.artifacts.Root: - description: > - The TOSCA Artifact Type all other TOSCA Artifact Types derive from - properties: - version: version - -tosca.artifacts.File: - derived_from: tosca.artifacts.Root - -tosca.artifacts.Deployment: - derived_from: tosca.artifacts.Root - description: TOSCA base type for deployment artifacts - -tosca.artifacts.Deployment.Image: - derived_from: tosca.artifacts.Deployment - -tosca.artifacts.Deployment.Image.VM: - derived_from: tosca.artifacts.Deployment.Image - -tosca.artifacts.Implementation: - derived_from: tosca.artifacts.Root - description: TOSCA base type for implementation artifacts - -tosca.artifacts.Implementation.Bash: - derived_from: tosca.artifacts.Implementation - description: Script artifact for the Unix Bash shell - mime_type: application/x-sh - file_ext: [ sh ] - -tosca.artifacts.Implementation.Python: - derived_from: tosca.artifacts.Implementation - description: Artifact for the interpreted Python language - mime_type: application/x-python - file_ext: [ py ] - -tosca.artifacts.Deployment.Image.Container.Docker: - derived_from: tosca.artifacts.Deployment.Image - description: Docker container image - -tosca.artifacts.Deployment.Image.VM.ISO: - derived_from: tosca.artifacts.Deployment.Image - description: Virtual Machine (VM) image in ISO disk format - mime_type: application/octet-stream - file_ext: [ iso ] - -tosca.artifacts.Deployment.Image.VM.QCOW2: - derived_from: tosca.artifacts.Deployment.Image - description: Virtual Machine (VM) image in QCOW v2 standard disk format - mime_type: application/octet-stream - file_ext: [ qcow2 ] - -########################################################################## - # Policy Type. - # TOSCA Policy Types represent logical grouping of TOSCA nodes that have - # an implied relationship and need to be orchestrated or managed together - # to achieve some result. -########################################################################## -tosca.policies.Root: - description: The TOSCA Policy Type all other TOSCA Policy Types derive from. - -tosca.policies.Placement: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to govern - placement of TOSCA nodes or groups of nodes. - -tosca.policies.Scaling: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to govern - scaling of TOSCA nodes or groups of nodes. - -tosca.policies.Update: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to govern - update of TOSCA nodes or groups of nodes. - -tosca.policies.Performance: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to declare - performance requirements for TOSCA nodes or groups of nodes. - -# Miguel: new types - -tosca.nodes.Database.MySQL: - derived_from: tosca.nodes.Database - properties: - password: - type: string - required: true - name: - type: string - required: true - user: - type: string - required: true - root_password: - type: string - required: true - requirements: - - host: - capability: tosca.capabilities.Container - relationship: tosca.relationships.HostedOn - node: tosca.nodes.DBMS.MySQL - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_configure.yml - inputs: - password: { get_property: [ SELF, password ] } - name: { get_property: [ SELF, name ] } - user: { get_property: [ SELF, user ] } - root_password: { get_property: [ SELF, root_password ] } - - -tosca.nodes.DBMS.MySQL: - derived_from: tosca.nodes.DBMS - properties: - port: - type: integer - description: reflect the default MySQL server port - default: 3306 - root_password: - type: string - # MySQL requires a root_password for configuration - required: true - capabilities: - # Further constrain the ‘host’ capability to only allow MySQL databases - host: - type: tosca.capabilities.Container - valid_source_types: [ tosca.nodes.Database.MySQL ] - interfaces: - Standard: - create: mysql/mysql_install.yml - configure: - implementation: mysql/mysql_configure.yml - inputs: - root_password: { get_property: [ SELF, root_password ] } - port: { get_property: [ SELF, port ] } - -tosca.nodes.WebServer.Apache: - derived_from: tosca.nodes.WebServer - interfaces: - Standard: - create: apache/apache_install.yml - -# INDIGO non normative types - -tosca.nodes.indigo.GalaxyPortal: - derived_from: tosca.nodes.WebServer - properties: - admin: - type: string - description: email of the admin user - default: admin@admin.com - required: false - admin_api_key: - type: string - description: key to access the API with admin role - default: not_very_secret_api_key - required: false - user: - type: string - description: username to launch the galaxy daemon - default: galaxy - required: false - install_path: - type: string - description: path to install the galaxy tool - default: /home/galaxy/galaxy - required: false - interfaces: - Standard: - create: - implementation: galaxy/galaxy_install.yml - inputs: - galaxy_install_path: { get_property: [ SELF, install_path ] } - configure: - implementation: galaxy/galaxy_configure.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - galaxy_admin: { get_property: [ SELF, admin ] } - galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } - start: - implementation: galaxy/galaxy_start.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - - -tosca.nodes.indigo.GalaxyTool: - derived_from: tosca.nodes.WebApplication - properties: - name: - type: string - description: name of the tool - required: true - owner: - type: string - description: developer of the tool - required: true - tool_panel_section_id: - type: string - description: panel section to install the tool - required: true - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.indigo.GalaxyPortal - relationship: tosca.relationships.HostedOn - interfaces: - Standard: - create: - implementation: galaxy/galaxy_tools_configure.yml - inputs: - galaxy_install_path: { get_property: [ HOST, install_path ] } - galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } - galaxy_tool_name: { get_property: [ SELF, name ] } - galaxy_tool_owner: { get_property: [ SELF, owner ] } - galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } diff --git a/IM/tosca/toscaparser/elements/__init__.py b/IM/tosca/toscaparser/elements/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/elements/artifacttype.py b/IM/tosca/toscaparser/elements/artifacttype.py deleted file mode 100644 index e0897b3d7..000000000 --- a/IM/tosca/toscaparser/elements/artifacttype.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class ArtifactTypeDef(StatefulEntityType): - '''TOSCA built-in artifacts type.''' - - def __init__(self, atype, custom_def=None): - super(ArtifactTypeDef, self).__init__(atype, self.ARTIFACT_PREFIX, - custom_def) - self.type = atype - self.properties = None - if self.PROPERTIES in self.defs: - self.properties = self.defs[self.PROPERTIES] - self.parent_artifacts = self._get_parent_artifacts() - - def _get_parent_artifacts(self): - artifacts = {} - parent_artif = self.parent_type - if parent_artif: - while parent_artif != 'tosca.artifacts.Root': - artifacts[parent_artif] = self.TOSCA_DEF[parent_artif] - parent_artif = artifacts[parent_artif]['derived_from'] - return artifacts - - @property - def parent_type(self): - '''Return an artifact this artifact is derived from.''' - return self.derived_from(self.defs) - - def get_artifact(self, name): - '''Return the definition of an artifact field by name.''' - if name in self.defs: - return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/attribute_definition.py b/IM/tosca/toscaparser/elements/attribute_definition.py deleted file mode 100644 index 35ba27f22..000000000 --- a/IM/tosca/toscaparser/elements/attribute_definition.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - - -class AttributeDef(object): - '''TOSCA built-in Attribute type.''' - - def __init__(self, name, value=None, schema=None): - self.name = name - self.value = value - self.schema = schema diff --git a/IM/tosca/toscaparser/elements/capabilitytype.py b/IM/tosca/toscaparser/elements/capabilitytype.py deleted file mode 100644 index b1bd7d767..000000000 --- a/IM/tosca/toscaparser/elements/capabilitytype.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.property_definition import PropertyDef -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class CapabilityTypeDef(StatefulEntityType): - '''TOSCA built-in capabilities type.''' - - def __init__(self, name, ctype, ntype, custom_def=None): - self.name = name - super(CapabilityTypeDef, self).__init__(ctype, self.CAPABILITY_PREFIX, - custom_def) - self.nodetype = ntype - self.properties = None - if self.PROPERTIES in self.defs: - self.properties = self.defs[self.PROPERTIES] - self.parent_capabilities = self._get_parent_capabilities() - - def get_properties_def_objects(self): - '''Return a list of property definition objects.''' - properties = [] - parent_properties = {} - if self.parent_capabilities: - for type, value in self.parent_capabilities.items(): - parent_properties[type] = value.get('properties') - if self.properties: - for prop, schema in self.properties.items(): - # Miguel: Cambios aqui - if isinstance(schema, dict): - properties.append(PropertyDef(prop, None, schema)) - if parent_properties: - for parent, props in parent_properties.items(): - for prop, schema in props.items(): - properties.append(PropertyDef(prop, None, schema)) - return properties - - def get_properties_def(self): - '''Return a dictionary of property definition name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_def_objects()} - - def get_property_def_value(self, name): - '''Return the definition of a given property name.''' - props_def = self.get_properties_def() - if props_def and name in props_def: - return props_def[name].value - - def _get_parent_capabilities(self): - capabilities = {} - parent_cap = self.parent_type - if parent_cap: - while parent_cap != 'tosca.capabilities.Root': - capabilities[parent_cap] = self.TOSCA_DEF[parent_cap] - parent_cap = capabilities[parent_cap]['derived_from'] - return capabilities - - @property - def parent_type(self): - '''Return a capability this capability is derived from.''' - return self.derived_from(self.defs) diff --git a/IM/tosca/toscaparser/elements/constraints.py b/IM/tosca/toscaparser/elements/constraints.py deleted file mode 100644 index 2f38eeffd..000000000 --- a/IM/tosca/toscaparser/elements/constraints.py +++ /dev/null @@ -1,569 +0,0 @@ -# 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. - -import collections -import datetime -import re - -from IM.tosca.toscaparser.common.exception import InvalidSchemaError -from IM.tosca.toscaparser.common.exception import ValidationError -from IM.tosca.toscaparser.elements import scalarunit -from IM.tosca.toscaparser.functions import is_function -from IM.tosca.toscaparser.utils.gettextutils import _ - - -class Schema(collections.Mapping): - - KEYS = ( - TYPE, REQUIRED, DESCRIPTION, - DEFAULT, CONSTRAINTS, ENTRYSCHEMA - ) = ( - 'type', 'required', 'description', - 'default', 'constraints', 'entry_schema' - ) - - PROPERTY_TYPES = ( - INTEGER, STRING, BOOLEAN, FLOAT, - NUMBER, TIMESTAMP, LIST, MAP, - SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME, - PORTDEF, VERSION - ) = ( - 'integer', 'string', 'boolean', 'float', - 'number', 'timestamp', 'list', 'map', - 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time', - 'PortDef', 'version' - ) - - SCALAR_UNIT_SIZE_DEFAULT = 'B' - SCALAR_UNIT_SIZE_DICT = {'B': 1, 'KB': 1000, 'KIB': 1024, 'MB': 1000000, - 'MIB': 1048576, 'GB': 1000000000, - 'GIB': 1073741824, 'TB': 1000000000000, - 'TIB': 1099511627776} - - def __init__(self, name, schema_dict): - self.name = name - if not isinstance(schema_dict, collections.Mapping): - msg = _("Schema %(pname)s must be a dict.") % dict(pname=name) - raise InvalidSchemaError(message=msg) - - try: - schema_dict['type'] - except KeyError: - msg = _("Schema %(pname)s must have type.") % dict(pname=name) - raise InvalidSchemaError(message=msg) - - self.schema = schema_dict - self._len = None - self.constraints_list = [] - - @property - def type(self): - return self.schema[self.TYPE] - - @property - def required(self): - return self.schema.get(self.REQUIRED, True) - - @property - def description(self): - return self.schema.get(self.DESCRIPTION, '') - - @property - def default(self): - return self.schema.get(self.DEFAULT) - - @property - def constraints(self): - if not self.constraints_list: - constraint_schemata = self.schema.get(self.CONSTRAINTS) - if constraint_schemata: - self.constraints_list = [Constraint(self.name, - self.type, - cschema) - for cschema in constraint_schemata] - return self.constraints_list - - @property - def entry_schema(self): - return self.schema.get(self.ENTRYSCHEMA) - - def __getitem__(self, key): - return self.schema[key] - - def __iter__(self): - for k in self.KEYS: - try: - self.schema[k] - except KeyError: - pass - else: - yield k - - def __len__(self): - if self._len is None: - self._len = len(list(iter(self))) - return self._len - - -class Constraint(object): - '''Parent class for constraints for a Property or Input.''' - - CONSTRAINTS = (EQUAL, GREATER_THAN, - GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_RANGE, - VALID_VALUES, LENGTH, MIN_LENGTH, MAX_LENGTH, PATTERN) = \ - ('equal', 'greater_than', 'greater_or_equal', 'less_than', - 'less_or_equal', 'in_range', 'valid_values', 'length', - 'min_length', 'max_length', 'pattern') - - def __new__(cls, property_name, property_type, constraint): - if cls is not Constraint: - return super(Constraint, cls).__new__(cls) - - if(not isinstance(constraint, collections.Mapping) or - len(constraint) != 1): - raise InvalidSchemaError(message=_('Invalid constraint schema.')) - - for type in constraint.keys(): - ConstraintClass = get_constraint_class(type) - if not ConstraintClass: - msg = _('Invalid constraint type "%s".') % type - raise InvalidSchemaError(message=msg) - - return ConstraintClass(property_name, property_type, constraint) - - def __init__(self, property_name, property_type, constraint): - self.property_name = property_name - self.property_type = property_type - self.constraint_value = constraint[self.constraint_key] - self.constraint_value_msg = self.constraint_value - if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: - self.constraint_value = self._get_scalarunit_constraint_value() - # check if constraint is valid for property type - if property_type not in self.valid_prop_types: - msg = _('Constraint type "%(ctype)s" is not valid ' - 'for data type "%(dtype)s".') % dict( - ctype=self.constraint_key, - dtype=property_type) - raise InvalidSchemaError(message=msg) - - def _get_scalarunit_constraint_value(self): - if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: - ScalarUnit_Class = (scalarunit. - get_scalarunit_class(self.property_type)) - if isinstance(self.constraint_value, list): - return [ScalarUnit_Class(v).get_num_from_scalar_unit() - for v in self.constraint_value] - else: - return (ScalarUnit_Class(self.constraint_value). - get_num_from_scalar_unit()) - - def _err_msg(self, value): - return _('Property %s could not be validated.') % self.property_name - - def validate(self, value): - self.value_msg = value - if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: - value = scalarunit.get_scalarunit_value(self.property_type, value) - if not self._is_valid(value): - err_msg = self._err_msg(value) - raise ValidationError(message=err_msg) - - -class Equal(Constraint): - """Constraint class for "equal" - - Constrains a property or parameter to a value equal to ('=') - the value declared. - """ - - constraint_key = Constraint.EQUAL - - valid_prop_types = Schema.PROPERTY_TYPES - - def _is_valid(self, value): - if value == self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s is not equal to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class GreaterThan(Constraint): - """Constraint class for "greater_than" - - Constrains a property or parameter to a value greater than ('>') - the value declared. - """ - - constraint_key = Constraint.GREATER_THAN - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(GreaterThan, self).__init__(property_name, property_type, - constraint) - if not isinstance(constraint[self.GREATER_THAN], self.valid_types): - raise InvalidSchemaError(message=_('greater_than must ' - 'be comparable.')) - - def _is_valid(self, value): - if value > self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be greater than "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class GreaterOrEqual(Constraint): - """Constraint class for "greater_or_equal" - - Constrains a property or parameter to a value greater than or equal - to ('>=') the value declared. - """ - - constraint_key = Constraint.GREATER_OR_EQUAL - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(GreaterOrEqual, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('greater_or_equal must ' - 'be comparable.')) - - def _is_valid(self, value): - if is_function(value) or value >= self.constraint_value: - return True - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be greater or equal ' - 'to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class LessThan(Constraint): - """Constraint class for "less_than" - - Constrains a property or parameter to a value less than ('<') - the value declared. - """ - - constraint_key = Constraint.LESS_THAN - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(LessThan, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('less_than must ' - 'be comparable.')) - - def _is_valid(self, value): - if value < self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be less than "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class LessOrEqual(Constraint): - """Constraint class for "less_or_equal" - - Constrains a property or parameter to a value less than or equal - to ('<=') the value declared. - """ - - constraint_key = Constraint.LESS_OR_EQUAL - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(LessOrEqual, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('less_or_equal must ' - 'be comparable.')) - - def _is_valid(self, value): - if value <= self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be less or ' - 'equal to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class InRange(Constraint): - """Constraint class for "in_range" - - Constrains a property or parameter to a value in range of (inclusive) - the two values declared. - """ - - constraint_key = Constraint.IN_RANGE - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(InRange, self).__init__(property_name, property_type, constraint) - if(not isinstance(self.constraint_value, collections.Sequence) or - (len(constraint[self.IN_RANGE]) != 2)): - raise InvalidSchemaError(message=_('in_range must be a list.')) - - for value in self.constraint_value: - if not isinstance(value, self.valid_types): - raise InvalidSchemaError(_('in_range value must ' - 'be comparable.')) - - self.min = self.constraint_value[0] - self.max = self.constraint_value[1] - - def _is_valid(self, value): - if value < self.min: - return False - if value > self.max: - return False - - return True - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s is out of range ' - '(min:%(vmin)s, max:%(vmax)s).') % - dict(pname=self.property_name, - pvalue=self.value_msg, - vmin=self.constraint_value_msg[0], - vmax=self.constraint_value_msg[1])) - - -class ValidValues(Constraint): - """Constraint class for "valid_values" - - Constrains a property or parameter to a value that is in the list of - declared values. - """ - constraint_key = Constraint.VALID_VALUES - - valid_prop_types = Schema.PROPERTY_TYPES - - def __init__(self, property_name, property_type, constraint): - super(ValidValues, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, collections.Sequence): - raise InvalidSchemaError(message=_('valid_values must be a list.')) - - def _is_valid(self, value): - if isinstance(value, list): - return all(v in self.constraint_value for v in value) - return value in self.constraint_value - - def _err_msg(self, value): - allowed = '[%s]' % ', '.join(str(a) for a in self.constraint_value) - return (_('%(pname)s: %(pvalue)s is not an valid ' - 'value "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=allowed)) - - -class Length(Constraint): - """Constraint class for "length" - - Constrains the property or parameter to a value of a given length. - """ - - constraint_key = Constraint.LENGTH - - valid_types = (int, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(Length, self).__init__(property_name, property_type, constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('length must be integer.')) - - def _is_valid(self, value): - if isinstance(value, str) and len(value) == self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('length of %(pname)s: %(pvalue)s must be equal ' - 'to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -class MinLength(Constraint): - """Constraint class for "min_length" - - Constrains the property or parameter to a value to a minimum length. - """ - - constraint_key = Constraint.MIN_LENGTH - - valid_types = (int, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(MinLength, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('min_length must be integer.')) - - def _is_valid(self, value): - if isinstance(value, str) and len(value) >= self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('length of %(pname)s: %(pvalue)s must be ' - 'at least "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -class MaxLength(Constraint): - """Constraint class for "max_length" - - Constrains the property or parameter to a value to a maximum length. - """ - - constraint_key = Constraint.MAX_LENGTH - - valid_types = (int, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(MaxLength, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('max_length must be integer.')) - - def _is_valid(self, value): - if isinstance(value, str) and len(value) <= self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('length of %(pname)s: %(pvalue)s must be no greater ' - 'than "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -class Pattern(Constraint): - """Constraint class for "pattern" - - Constrains the property or parameter to a value that is allowed by - the provided regular expression. - """ - - constraint_key = Constraint.PATTERN - - valid_types = (str, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(Pattern, self).__init__(property_name, property_type, constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('pattern must be string.')) - self.match = re.compile(self.constraint_value).match - - def _is_valid(self, value): - match = self.match(value) - return match is not None and match.end() == len(value) - - def _err_msg(self, value): - return (_('%(pname)s: "%(pvalue)s" does not match ' - 'pattern "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -constraint_mapping = { - Constraint.EQUAL: Equal, - Constraint.GREATER_THAN: GreaterThan, - Constraint.GREATER_OR_EQUAL: GreaterOrEqual, - Constraint.LESS_THAN: LessThan, - Constraint.LESS_OR_EQUAL: LessOrEqual, - Constraint.IN_RANGE: InRange, - Constraint.VALID_VALUES: ValidValues, - Constraint.LENGTH: Length, - Constraint.MIN_LENGTH: MinLength, - Constraint.MAX_LENGTH: MaxLength, - Constraint.PATTERN: Pattern - } - - -def get_constraint_class(type): - return constraint_mapping.get(type) diff --git a/IM/tosca/toscaparser/elements/datatype.py b/IM/tosca/toscaparser/elements/datatype.py deleted file mode 100644 index e66c3b79c..000000000 --- a/IM/tosca/toscaparser/elements/datatype.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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. - - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class DataType(StatefulEntityType): - '''TOSCA built-in and user defined complex data type.''' - - def __init__(self, datatypename, custom_def=None): - super(DataType, self).__init__(datatypename, self.DATATYPE_PREFIX, - custom_def) - self.custom_def = custom_def - - @property - def parent_type(self): - '''Return a datatype this datatype is derived from.''' - ptype = self.derived_from(self.defs) - if ptype: - return DataType(ptype, self.custom_def) - return None - - @property - def value_type(self): - '''Return 'type' section in the datatype schema.''' - return self.entity_value(self.defs, 'type') - - def get_all_properties_objects(self): - '''Return all properties objects defined in type and parent type.''' - props_def = self.get_properties_def_objects() - ptype = self.parent_type - while ptype: - props_def.extend(ptype.get_properties_def_objects()) - ptype = ptype.parent_type - return props_def - - def get_all_properties(self): - '''Return a dictionary of all property definition name-object pairs.''' - return {prop.name: prop - for prop in self.get_all_properties_objects()} - - def get_all_property_value(self, name): - '''Return the value of a given property name.''' - props_def = self.get_all_properties() - if props_def and name in props_def.key(): - return props_def[name].value diff --git a/IM/tosca/toscaparser/elements/entity_type.py b/IM/tosca/toscaparser/elements/entity_type.py deleted file mode 100644 index 241556093..000000000 --- a/IM/tosca/toscaparser/elements/entity_type.py +++ /dev/null @@ -1,113 +0,0 @@ -# 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. - -import logging -import os -import IM.tosca.toscaparser.utils.yamlparser - -log = logging.getLogger('tosca') - - -class EntityType(object): - '''Base class for TOSCA elements.''' - - SECTIONS = (DERIVED_FROM, PROPERTIES, ATTRIBUTES, REQUIREMENTS, - INTERFACES, CAPABILITIES, TYPE, ARTIFACTS) = \ - ('derived_from', 'properties', 'attributes', 'requirements', - 'interfaces', 'capabilities', 'type', 'artifacts') - - '''TOSCA definition file.''' - TOSCA_DEF_FILE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "TOSCA_definition_1_0.yaml") - - loader = IM.tosca.toscaparser.utils.yamlparser.load_yaml - - TOSCA_DEF = loader(TOSCA_DEF_FILE) - - RELATIONSHIP_TYPE = (DEPENDSON, HOSTEDON, CONNECTSTO, ATTACHESTO, - LINKSTO, BINDSTO) = \ - ('tosca.relationships.DependsOn', - 'tosca.relationships.HostedOn', - 'tosca.relationships.ConnectsTo', - 'tosca.relationships.AttachesTo', - 'tosca.relationships.network.LinksTo', - 'tosca.relationships.network.BindsTo') - - NODE_PREFIX = 'tosca.nodes.' - RELATIONSHIP_PREFIX = 'tosca.relationships.' - CAPABILITY_PREFIX = 'tosca.capabilities.' - INTERFACE_PREFIX = 'tosca.interfaces.' - ARTIFACT_PREFIX = 'tosca.artifacts.' - POLICY_PREFIX = 'tosca.policies.' - # currently the data types are defined only for network - # but may have changes in the future. - DATATYPE_PREFIX = 'tosca.datatypes.network.' - TOSCA = 'tosca' - - def derived_from(self, defs): - '''Return a type this type is derived from.''' - return self.entity_value(defs, 'derived_from') - - def is_derived_from(self, type_str): - '''Check if object inherits from the given type. - - Returns true if this object is derived from 'type_str'. - False otherwise. - ''' - if not self.type: - return False - elif self.type == type_str: - return True - elif self.parent_type: - return self.parent_type.is_derived_from(type_str) - else: - return False - - def entity_value(self, defs, key): - if key in defs: - return defs[key] - - def get_value(self, ndtype, defs=None, parent=None): - value = None - if defs is None: - defs = self.defs - if ndtype in defs: - value = defs[ndtype] - if parent and not value: - p = self.parent_type - while value is None: - # check parent node - if not p: - break - if p and p.type == 'tosca.nodes.Root': - break - value = p.get_value(ndtype) - p = p.parent_type - return value - - def get_definition(self, ndtype): - value = None - defs = self.defs - if ndtype in defs: - value = defs[ndtype] - p = self.parent_type - if p: - inherited = p.get_definition(ndtype) - if inherited: - inherited = dict(inherited) - if not value: - value = inherited - else: - inherited.update(value) - value.update(inherited) - return value diff --git a/IM/tosca/toscaparser/elements/interfaces.py b/IM/tosca/toscaparser/elements/interfaces.py deleted file mode 100644 index b763294f8..000000000 --- a/IM/tosca/toscaparser/elements/interfaces.py +++ /dev/null @@ -1,74 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - -SECTIONS = (LIFECYCLE, CONFIGURE, LIFECYCLE_SHORTNAME, - CONFIGURE_SHORTNAME) = \ - ('tosca.interfaces.node.lifecycle.Standard', - 'tosca.interfaces.relationship.Configure', - 'Standard', 'Configure') - -INTERFACEVALUE = (IMPLEMENTATION, INPUTS) = ('implementation', 'inputs') - - -class InterfacesDef(StatefulEntityType): - '''TOSCA built-in interfaces type.''' - - def __init__(self, node_type, interfacetype, - node_template=None, name=None, value=None): - self.ntype = node_type - self.node_template = node_template - self.type = interfacetype - self.name = name - self.value = value - self.implementation = None - self.inputs = None - self.defs = {} - if interfacetype == LIFECYCLE_SHORTNAME: - interfacetype = LIFECYCLE - if interfacetype == CONFIGURE_SHORTNAME: - interfacetype = CONFIGURE - if node_type: - self.defs = self.TOSCA_DEF[interfacetype] - if value: - if isinstance(self.value, dict): - for i, j in self.value.items(): - if i == IMPLEMENTATION: - self.implementation = j - elif i == INPUTS: - self.inputs = j - else: - what = ('Interfaces of template %s' % - self.node_template.name) - raise UnknownFieldError(what=what, field=i) - else: - self.implementation = value - - @property - def lifecycle_ops(self): - if self.defs: - if self.type == LIFECYCLE: - return self._ops() - - @property - def configure_ops(self): - if self.defs: - if self.type == CONFIGURE: - return self._ops() - - def _ops(self): - ops = [] - for name in list(self.defs.keys()): - ops.append(name) - return ops diff --git a/IM/tosca/toscaparser/elements/nodetype.py b/IM/tosca/toscaparser/elements/nodetype.py deleted file mode 100644 index f0ee53453..000000000 --- a/IM/tosca/toscaparser/elements/nodetype.py +++ /dev/null @@ -1,200 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.capabilitytype import CapabilityTypeDef -import IM.tosca.toscaparser.elements.interfaces as ifaces -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class NodeType(StatefulEntityType): - '''TOSCA built-in node type.''' - - def __init__(self, ntype, custom_def=None): - super(NodeType, self).__init__(ntype, self.NODE_PREFIX, custom_def) - self.custom_def = custom_def - - @property - def parent_type(self): - '''Return a node this node is derived from.''' - pnode = self.derived_from(self.defs) - if pnode: - return NodeType(pnode) - - @property - def relationship(self): - '''Return a dictionary of relationships to other node types. - - This method returns a dictionary of named relationships that nodes - of the current node type (self) can have to other nodes (of specific - types) in a TOSCA template. - - ''' - relationship = {} - requires = self.get_all_requirements() - if requires: - # NOTE(sdmonov): Check if requires is a dict. - # If it is a dict convert it to a list of dicts. - # This is needed because currently the code below supports only - # lists as requirements definition. The following check will - # make sure if a map (dict) was provided it will be converted to - # a list before proceeding to the parsing. - if isinstance(requires, dict): - requires = [{key: value} for key, value in requires.items()] - - keyword = None - node_type = None - for require in requires: - for key, req in require.items(): - if 'relationship' in req: - relation = req.get('relationship') - if 'type' in relation: - relation = relation.get('type') - node_type = req.get('node') - value = req - if node_type: - keyword = 'node' - else: - # If value is a dict and has a type key - # we need to lookup the node type using - # the capability type - value = req - if isinstance(value, dict): - captype = value['capability'] - value = (self. - _get_node_type_by_cap(key, captype)) - relation = self._get_relation(key, value) - keyword = key - node_type = value - rtype = RelationshipType(relation, keyword, req) - relatednode = NodeType(node_type, self.custom_def) - relationship[rtype] = relatednode - return relationship - - def _get_node_type_by_cap(self, key, cap): - '''Find the node type that has the provided capability - - This method will lookup all node types if they have the - provided capability. - ''' - - # Filter the node types - node_types = [node_type for node_type in self.TOSCA_DEF.keys() - if node_type.startswith(self.NODE_PREFIX) and - node_type != 'tosca.nodes.Root'] - - for node_type in node_types: - node_def = self.TOSCA_DEF[node_type] - if isinstance(node_def, dict) and 'capabilities' in node_def: - node_caps = node_def['capabilities'] - for value in node_caps.values(): - if isinstance(value, dict) and \ - 'type' in value and value['type'] == cap: - return node_type - - def _get_relation(self, key, ndtype): - relation = None - ntype = NodeType(ndtype) - caps = ntype.get_capabilities() - if caps and key in caps.keys(): - c = caps[key] - for r in self.RELATIONSHIP_TYPE: - rtypedef = ntype.TOSCA_DEF[r] - for properties in rtypedef.values(): - if c.type in properties: - relation = r - break - if relation: - break - else: - for properties in rtypedef.values(): - if c.parent_type in properties: - relation = r - break - return relation - - def get_capabilities_objects(self): - '''Return a list of capability objects.''' - typecapabilities = [] - caps = self.get_value(self.CAPABILITIES) - if caps is None: - caps = self.get_value(self.CAPABILITIES, None, True) - if caps: - for name, value in caps.items(): - ctype = value.get('type') - cap = CapabilityTypeDef(name, ctype, self.type, - self.custom_def) - typecapabilities.append(cap) - return typecapabilities - - def get_capabilities(self): - '''Return a dictionary of capability name-objects pairs.''' - return {cap.name: cap - for cap in self.get_capabilities_objects()} - - @property - def requirements(self): - return self.get_value(self.REQUIREMENTS) - - def get_all_requirements(self): - requires = self.requirements - parent_node = self.parent_type - if requires is None: - requires = self.get_value(self.REQUIREMENTS, None, True) - parent_node = parent_node.parent_type - if parent_node: - while parent_node.type != 'tosca.nodes.Root': - req = parent_node.get_value(self.REQUIREMENTS, None, True) - for r in req: - if r not in requires: - requires.append(r) - parent_node = parent_node.parent_type - return requires - - @property - def interfaces(self): - return self.get_value(self.INTERFACES) - - @property - def lifecycle_inputs(self): - '''Return inputs to life cycle operations if found.''' - inputs = [] - interfaces = self.interfaces - if interfaces: - for name, value in interfaces.items(): - if name == ifaces.LIFECYCLE: - for x, y in value.items(): - if x == 'inputs': - for i in y.iterkeys(): - inputs.append(i) - return inputs - - @property - def lifecycle_operations(self): - '''Return available life cycle operations if found.''' - ops = None - interfaces = self.interfaces - if interfaces: - i = InterfacesDef(self.type, ifaces.LIFECYCLE) - ops = i.lifecycle_ops - return ops - - def get_capability(self, name): - caps = self.get_capabilities() - if caps and name in caps.keys(): - return caps[name].value - - def get_capability_type(self, name): - captype = self.get_capability(name) - if captype and name in captype.keys(): - return captype[name].value diff --git a/IM/tosca/toscaparser/elements/policytype.py b/IM/tosca/toscaparser/elements/policytype.py deleted file mode 100644 index 573e04509..000000000 --- a/IM/tosca/toscaparser/elements/policytype.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class PolicyType(StatefulEntityType): - '''TOSCA built-in policies type.''' - - def __init__(self, ptype, custom_def=None): - super(PolicyType, self).__init__(ptype, self.POLICY_PREFIX, - custom_def) - self.type = ptype - self.properties = None - if self.PROPERTIES in self.defs: - self.properties = self.defs[self.PROPERTIES] - self.parent_policies = self._get_parent_policies() - - def _get_parent_policies(self): - policies = {} - parent_policy = self.parent_type - if parent_policy: - while parent_policy != 'tosca.policies.Root': - policies[parent_policy] = self.TOSCA_DEF[parent_policy] - parent_policy = policies[parent_policy]['derived_from'] - return policies - - @property - def parent_type(self): - '''Return a policy this policy is derived from.''' - return self.derived_from(self.defs) - - def get_policy(self, name): - '''Return the definition of a policy field by name.''' - if name in self.defs: - return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/property_definition.py b/IM/tosca/toscaparser/elements/property_definition.py deleted file mode 100644 index c2c7f0089..000000000 --- a/IM/tosca/toscaparser/elements/property_definition.py +++ /dev/null @@ -1,46 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import InvalidSchemaError -# Miguel: add import -from IM.tosca.toscaparser.utils.gettextutils import _ - -class PropertyDef(object): - '''TOSCA built-in Property type.''' - - def __init__(self, name, value=None, schema=None): - self.name = name - self.value = value - self.schema = schema - - try: - self.schema['type'] - except KeyError: - msg = (_("Property definition of %(pname)s must have type.") % - dict(pname=self.name)) - raise InvalidSchemaError(message=msg) - - @property - def required(self): - if self.schema: - for prop_key, prop_value in self.schema.items(): - if prop_key == 'required' and prop_value: - return True - return False - - @property - def default(self): - if self.schema: - for prop_key, prop_value in self.schema.items(): - if prop_key == 'default': - return prop_value - return None diff --git a/IM/tosca/toscaparser/elements/relationshiptype.py b/IM/tosca/toscaparser/elements/relationshiptype.py deleted file mode 100644 index e45ee3d93..000000000 --- a/IM/tosca/toscaparser/elements/relationshiptype.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class RelationshipType(StatefulEntityType): - '''TOSCA built-in relationship type.''' - def __init__(self, type, capability_name=None, custom_def=None): - super(RelationshipType, self).__init__(type, self.RELATIONSHIP_PREFIX, - custom_def) - self.capability_name = capability_name - self.custom_def = custom_def - - @property - def parent_type(self): - '''Return a relationship this reletionship is derived from.''' - prel = self.derived_from(self.defs) - if prel: - return RelationshipType(prel) - - @property - def valid_target_types(self): - return self.entity_value(self.defs, 'valid_target_types') diff --git a/IM/tosca/toscaparser/elements/scalarunit.py b/IM/tosca/toscaparser/elements/scalarunit.py deleted file mode 100644 index 836427085..000000000 --- a/IM/tosca/toscaparser/elements/scalarunit.py +++ /dev/null @@ -1,130 +0,0 @@ -# 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. - -import logging -import re - -from IM.tosca.toscaparser.utils.gettextutils import _ -from IM.tosca.toscaparser.utils import validateutils - -log = logging.getLogger('tosca') - - -class ScalarUnit(object): - '''Parent class for scalar-unit type.''' - - SCALAR_UNIT_TYPES = ( - SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME - ) = ( - 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time' - ) - - def __init__(self, value): - self.value = value - - def _check_unit_in_scalar_standard_units(self, input_unit): - """Check whether the input unit is following specified standard - - If unit is not following specified standard, convert it to standard - unit after displaying a warning message. - """ - if input_unit in self.SCALAR_UNIT_DICT.keys(): - return input_unit - else: - for key in self.SCALAR_UNIT_DICT.keys(): - if key.upper() == input_unit.upper(): - log.warning(_('Given unit %(unit)s does not follow scalar ' - 'unit standards; using %(key)s instead.') % { - 'unit': input_unit, 'key': key}) - return key - msg = (_('Provided unit "%(unit)s" is not valid. The valid units' - ' are %(valid_units)s') % {'unit': input_unit, - 'valid_units': sorted(self.SCALAR_UNIT_DICT.keys())}) - raise ValueError(msg) - - def validate_scalar_unit(self): - # Miguel: Cambios aqui - if self.value is None: - return None - regex = re.compile('([0-9.]+)\s*(\w+)') - try: - result = regex.match(str(self.value)).groups() - validateutils.str_to_num(result[0]) - scalar_unit = self._check_unit_in_scalar_standard_units(result[1]) - self.value = ' '.join([result[0], scalar_unit]) - return self.value - - except Exception: - raise ValueError(_('"%s" is not a valid scalar-unit') - % self.value) - - def get_num_from_scalar_unit(self, unit=None): - #Miguel: Cambios aqui - if self.value is None: - return None - if unit: - unit = self._check_unit_in_scalar_standard_units(unit) - else: - unit = self.SCALAR_UNIT_DEFAULT - self.validate_scalar_unit() - - regex = re.compile('([0-9.]+)\s*(\w+)') - result = regex.match(str(self.value)).groups() - converted = (float(validateutils.str_to_num(result[0])) - * self.SCALAR_UNIT_DICT[result[1]] - / self.SCALAR_UNIT_DICT[unit]) - if converted - int(converted) < 0.0000000000001: - converted = int(converted) - return converted - - -class ScalarUnit_Size(ScalarUnit): - - SCALAR_UNIT_DEFAULT = 'B' - SCALAR_UNIT_DICT = {'B': 1, 'kB': 1000, 'KiB': 1024, 'MB': 1000000, - 'MiB': 1048576, 'GB': 1000000000, - 'GiB': 1073741824, 'TB': 1000000000000, - 'TiB': 1099511627776} - - -class ScalarUnit_Time(ScalarUnit): - - SCALAR_UNIT_DEFAULT = 'ms' - SCALAR_UNIT_DICT = {'d': 86400, 'h': 3600, 'm': 60, 's': 1, - 'ms': 0.001, 'us': 0.000001, 'ns': 0.000000001} - - -class ScalarUnit_Frequency(ScalarUnit): - - SCALAR_UNIT_DEFAULT = 'GHz' - SCALAR_UNIT_DICT = {'Hz': 1, 'kHz': 1000, - 'MHz': 1000000, 'GHz': 1000000000} - - -scalarunit_mapping = { - ScalarUnit.SCALAR_UNIT_FREQUENCY: ScalarUnit_Frequency, - ScalarUnit.SCALAR_UNIT_SIZE: ScalarUnit_Size, - ScalarUnit.SCALAR_UNIT_TIME: ScalarUnit_Time, - } - - -def get_scalarunit_class(type): - return scalarunit_mapping.get(type) - - -def get_scalarunit_value(type, value, unit=None): - if type in ScalarUnit.SCALAR_UNIT_TYPES: - ScalarUnit_Class = get_scalarunit_class(type) - return (ScalarUnit_Class(value). - get_num_from_scalar_unit(unit)) - else: - raise TypeError(_('"%s" is not a valid scalar-unit type') % type) diff --git a/IM/tosca/toscaparser/elements/statefulentitytype.py b/IM/tosca/toscaparser/elements/statefulentitytype.py deleted file mode 100644 index af820bd69..000000000 --- a/IM/tosca/toscaparser/elements/statefulentitytype.py +++ /dev/null @@ -1,81 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import InvalidTypeError -from IM.tosca.toscaparser.elements.attribute_definition import AttributeDef -from IM.tosca.toscaparser.elements.entity_type import EntityType -from IM.tosca.toscaparser.elements.property_definition import PropertyDef - - -class StatefulEntityType(EntityType): - '''Class representing TOSCA states.''' - - interfaces_node_lifecycle_operations = ['create', - 'configure', 'start', - 'stop', 'delete'] - - interfaces_relationship_confiure_operations = ['post_configure_source', - 'post_configure_target', - 'add_target', - 'remove_target'] - - def __init__(self, entitytype, prefix, custom_def=None): - entire_entitytype = entitytype - if not entitytype.startswith(self.TOSCA): - entire_entitytype = prefix + entitytype - if entire_entitytype in list(self.TOSCA_DEF.keys()): - self.defs = self.TOSCA_DEF[entire_entitytype] - entitytype = entire_entitytype - elif custom_def and entitytype in list(custom_def.keys()): - self.defs = custom_def[entitytype] - else: - raise InvalidTypeError(what=entitytype) - self.type = entitytype - - def get_properties_def_objects(self): - '''Return a list of property definition objects.''' - properties = [] - props = self.get_definition(self.PROPERTIES) - if props: - for prop, schema in props.items(): - properties.append(PropertyDef(prop, None, schema)) - return properties - - def get_properties_def(self): - '''Return a dictionary of property definition name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_def_objects()} - - def get_property_def_value(self, name): - '''Return the property definition associated with a given name.''' - props_def = self.get_properties_def() - if props_def and name in props_def.keys(): - return props_def[name].value - - def get_attributes_def_objects(self): - '''Return a list of attribute definition objects.''' - attrs = self.get_value(self.ATTRIBUTES) - if attrs: - return [AttributeDef(attr, None, schema) - for attr, schema in attrs.items()] - return [] - - def get_attributes_def(self): - '''Return a dictionary of attribute definition name-object pairs.''' - return {attr.name: attr - for attr in self.get_attributes_def_objects()} - - def get_attribute_def_value(self, name): - '''Return the attribute definition associated with a given name.''' - attrs_def = self.get_attributes_def() - if attrs_def and name in attrs_def.keys(): - return attrs_def[name].value diff --git a/IM/tosca/toscaparser/entity_template.py b/IM/tosca/toscaparser/entity_template.py deleted file mode 100644 index f8f3ced25..000000000 --- a/IM/tosca/toscaparser/entity_template.py +++ /dev/null @@ -1,285 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.capabilities import Capability -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.common.exception import ValidationError -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.elements.nodetype import NodeType -from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType -from IM.tosca.toscaparser.properties import Property - - -class EntityTemplate(object): - '''Base class for TOSCA templates.''' - - SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, - INTERFACES, CAPABILITIES, TYPE, DESCRIPTION, DIRECTIVES, - ATTRIBUTES, ARTIFACTS, NODE_FILTER, COPY) = \ - ('derived_from', 'properties', 'requirements', 'interfaces', - 'capabilities', 'type', 'description', 'directives', - 'attributes', 'artifacts', 'node_filter', 'copy') - # Miguel: Add NODE_FILTER to the REQUIREMENTS_SECTION - REQUIREMENTS_SECTION = (NODE, CAPABILITY, RELATIONSHIP, OCCURRENCES, NODE_FILTER) = \ - ('node', 'capability', 'relationship', - 'occurrences','node_filter') - - def __init__(self, name, template, entity_name, custom_def=None): - self.name = name - self.entity_tpl = template - self.custom_def = custom_def - self._validate_field(self.entity_tpl) - if entity_name == 'node_type': - self.type_definition = NodeType(self.entity_tpl['type'], - custom_def) - if entity_name == 'relationship_type': - relationship = template.get('relationship') - type = None - if relationship and isinstance(relationship, dict): - type = relationship.get('type') - elif isinstance(relationship, str): - type = self.entity_tpl['relationship'] - else: - type = self.entity_tpl['type'] - self.type_definition = RelationshipType(type, - None, custom_def) - self._properties = None - self._interfaces = None - self._requirements = None - self._capabilities = None - - @property - def type(self): - return self.type_definition.type - - @property - def requirements(self): - if self._requirements is None: - self._requirements = self.type_definition.get_value( - self.REQUIREMENTS, - self.entity_tpl) or [] - return self._requirements - - def get_properties_objects(self): - '''Return properties objects for this template.''' - if self._properties is None: - self._properties = self._create_properties() - return self._properties - - def get_properties(self): - '''Return a dictionary of property name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_objects()} - - def get_property_value(self, name): - '''Return the value of a given property name.''' - props = self.get_properties() - if props and name in props.keys(): - return props[name].value - - @property - def interfaces(self): - #if self._interfaces is None: - if not self._interfaces: - self._interfaces = self._create_interfaces() - return self._interfaces - - def get_capabilities_objects(self): - '''Return capabilities objects for this template.''' - if not self._capabilities: - self._capabilities = self._create_capabilities() - return self._capabilities - - def get_capabilities(self): - '''Return a dictionary of capability name-object pairs.''' - return {cap.name: cap - for cap in self.get_capabilities_objects()} - - def is_derived_from(self, type_str): - '''Check if object inherits from the given type. - - Returns true if this object is derived from 'type_str'. - False otherwise. - ''' - if not self.type: - return False - elif self.type == type_str: - return True - elif self.parent_type: - return self.parent_type.is_derived_from(type_str) - else: - return False - - def _create_capabilities(self): - capability = [] - # Miguel: cambios aqui - caps = self.type_definition.get_value(self.CAPABILITIES, - self.entity_tpl, - self.type_definition) - if caps: - for name, props in caps.items(): - capabilities = self.type_definition.get_capabilities() - if name in capabilities.keys(): - c = capabilities[name] - if 'properties' in props: - cap = Capability(name, props['properties'], c) - else: - cap = Capability(name, [], c) - capability.append(cap) - return capability - - def _validate_properties(self, template, entitytype): - properties = entitytype.get_value(self.PROPERTIES, template) - self._common_validate_properties(entitytype, properties) - - def _validate_capabilities(self): - type_capabilities = self.type_definition.get_capabilities() - allowed_caps = \ - type_capabilities.keys() if type_capabilities else [] - capabilities = self.type_definition.get_value(self.CAPABILITIES, - self.entity_tpl) - if capabilities: - self._common_validate_field(capabilities, allowed_caps, - 'Capabilities') - self._validate_capabilities_properties(capabilities) - - def _validate_capabilities_properties(self, capabilities): - for cap, props in capabilities.items(): - capabilitydef = self.get_capability(cap).definition - self._common_validate_properties(capabilitydef, - props[self.PROPERTIES]) - - # validating capability properties values - for prop in self.get_capability(cap).get_properties_objects(): - prop.validate() - - # TODO(srinivas_tadepalli): temporary work around to validate - # default_instances until standardized in specification - if cap == "scalable" and prop.name == "default_instances": - prop_dict = props[self.PROPERTIES] - min_instances = prop_dict.get("min_instances") - max_instances = prop_dict.get("max_instances") - default_instances = prop_dict.get("default_instances") - if not (min_instances <= default_instances - <= max_instances): - err_msg = ("Properties of template %s : " - "default_instances value is not" - " between min_instances and " - "max_instances" % self.name) - raise ValidationError(message=err_msg) - - def _common_validate_properties(self, entitytype, properties): - allowed_props = [] - required_props = [] - for p in entitytype.get_properties_def_objects(): - allowed_props.append(p.name) - if p.required: - required_props.append(p.name) - if properties: - self._common_validate_field(properties, allowed_props, - 'Properties') - # make sure it's not missing any property required by a tosca type - missingprop = [] - for r in required_props: - if r not in properties.keys(): - missingprop.append(r) - if missingprop: - raise MissingRequiredFieldError( - what='Properties of template %s' % self.name, - required=missingprop) - else: - if required_props: - raise MissingRequiredFieldError( - what='Properties of template %s' % self.name, - required=missingprop) - - def _validate_field(self, template): - if not isinstance(template, dict): - raise MissingRequiredFieldError( - what='Template %s' % self.name, required=self.TYPE) - try: - relationship = template.get('relationship') - if relationship and not isinstance(relationship, str): - relationship[self.TYPE] - elif isinstance(relationship, str): - template['relationship'] - else: - template[self.TYPE] - except KeyError: - raise MissingRequiredFieldError( - what='Template %s' % self.name, required=self.TYPE) - - def _common_validate_field(self, schema, allowedlist, section): - for name in schema: - if name not in allowedlist: - raise UnknownFieldError( - what='%(section)s of template %(nodename)s' - % {'section': section, 'nodename': self.name}, - field=name) - - def _create_properties(self): - props = [] - properties = self.type_definition.get_value(self.PROPERTIES, - self.entity_tpl) or {} - for name, value in properties.items(): - props_def = self.type_definition.get_properties_def() - if props_def and name in props_def: - prop = Property(name, value, - props_def[name].schema, self.custom_def) - props.append(prop) - for p in self.type_definition.get_properties_def_objects(): - if p.default is not None and p.name not in properties.keys(): - prop = Property(p.name, p.default, p.schema, self.custom_def) - props.append(prop) - return props - - def _create_interfaces(self): - interfaces = [] - type_interfaces = None - if isinstance(self.type_definition, RelationshipType): - if isinstance(self.entity_tpl, dict): - # Miguel: cambios aqui - for key, value in self.entity_tpl.items(): - if key == 'interfaces': - type_interfaces = value - elif key != 'type': - rel = None - if isinstance(value, dict): - rel = value.get('relationship') - if rel: - if self.INTERFACES in rel: - type_interfaces = rel[self.INTERFACES] - break - else: - type_interfaces = self.type_definition.get_value(self.INTERFACES, - self.entity_tpl) - if type_interfaces: - for interface_type, value in type_interfaces.items(): - for op, op_def in value.items(): - iface = InterfacesDef(self.type_definition, - interfacetype=interface_type, - node_template=self, - name=op, - value=op_def) - interfaces.append(iface) - return interfaces - - def get_capability(self, name): - """Provide named capability - - :param name: name of capability - :return: capability object if found, None otherwise - """ - caps = self.get_capabilities() - if caps and name in caps.keys(): - return caps[name] diff --git a/IM/tosca/toscaparser/functions.py b/IM/tosca/toscaparser/functions.py deleted file mode 100644 index 5ecb905c9..000000000 --- a/IM/tosca/toscaparser/functions.py +++ /dev/null @@ -1,410 +0,0 @@ -# -# 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. - - -import abc -import six - -from IM.tosca.toscaparser.common.exception import UnknownInputError -from IM.tosca.toscaparser.utils.gettextutils import _ - - -GET_PROPERTY = 'get_property' -GET_ATTRIBUTE = 'get_attribute' -GET_INPUT = 'get_input' - -SELF = 'SELF' -HOST = 'HOST' - -HOSTED_ON = 'tosca.relationships.HostedOn' - - -@six.add_metaclass(abc.ABCMeta) -class Function(object): - """An abstract type for representing a Tosca template function.""" - - def __init__(self, tosca_tpl, context, name, args): - self.tosca_tpl = tosca_tpl - self.context = context - self.name = name - self.args = args - self.validate() - - @abc.abstractmethod - def result(self): - """Invokes the function and returns its result - - Some methods invocation may only be relevant on runtime (for example, - getting runtime properties) and therefore its the responsibility of - the orchestrator/translator to take care of such functions invocation. - - :return: Function invocation result. - """ - return {self.name: self.args} - - @abc.abstractmethod - def validate(self): - """Validates function arguments.""" - pass - - -class GetInput(Function): - """Get a property value declared within the input of the service template. - - Arguments: - - * Input name. - - Example: - - * get_input: port - """ - - def validate(self): - if len(self.args) != 1: - raise ValueError(_( - 'Expected one argument for get_input function but received: ' - '{0}.').format(self.args)) - inputs = [input.name for input in self.tosca_tpl.inputs] - if self.args[0] not in inputs: - raise UnknownInputError(input_name=self.args[0]) - - def result(self): - found_input = [input_def for input_def in self.tosca_tpl.inputs - if self.input_name == input_def.name][0] - return found_input.default - - @property - def input_name(self): - return self.args[0] - - -class GetAttribute(Function): - """Get an attribute value of an entity defined in the service template - - Node template attributes values are set in runtime and therefore its the - responsibility of the Tosca engine to implement the evaluation of - get_attribute functions. - - Arguments: - - * Node template name | HOST. - * Attribute name. - - If the HOST keyword is passed as the node template name argument the - function will search each node template along the HostedOn relationship - chain until a node which contains the attribute is found. - - Examples: - - * { get_attribute: [ server, private_address ] } - * { get_attribute: [ HOST, private_address ] } - """ - - def validate(self): - # Miguel: this is not true: - # { get_attribute: [ HOST, networks, private, addresses, 0 ] } - if len(self.args) != 2: - raise ValueError(_( - 'Illegal arguments for {0} function. Expected arguments: ' - 'node-template-name, attribute-name').format(GET_ATTRIBUTE)) - self._find_node_template_containing_attribute() - - def result(self): - return self.args - - def get_referenced_node_template(self): - """Gets the NodeTemplate instance the get_attribute function refers to. - - If HOST keyword was used as the node template argument, the node - template which contains the attribute along the HostedOn relationship - chain will be returned. - """ - return self._find_node_template_containing_attribute() - - def _find_node_template_containing_attribute(self): - if self.node_template_name == HOST: - # Currently this is the only way to tell whether the function - # is used within the outputs section of the TOSCA template. - if isinstance(self.context, list): - raise ValueError(_( - "get_attribute HOST keyword is not allowed within the " - "outputs section of the TOSCA template")) - node_tpl = self._find_host_containing_attribute() - if not node_tpl: - raise ValueError(_( - "get_attribute HOST keyword is used in '{0}' node " - "template but {1} was not found " - "in relationship chain").format(self.context.name, - HOSTED_ON)) - else: - node_tpl = self._find_node_template(self.args[0]) - if not self._attribute_exists_in_type(node_tpl.type_definition): - raise KeyError(_( - "Attribute '{0}' not found in node template: {1}.").format( - self.attribute_name, node_tpl.name)) - return node_tpl - - def _attribute_exists_in_type(self, type_definition): - attrs_def = type_definition.get_attributes_def() - found = [attrs_def[self.attribute_name]] \ - if self.attribute_name in attrs_def else [] - return len(found) == 1 - - def _find_host_containing_attribute(self, node_template_name=SELF): - node_template = self._find_node_template(node_template_name) - from IM.tosca.toscaparser.elements.entity_type import EntityType - hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] - for r in node_template.requirements: - for requirement, target_name in r.items(): - target_node = self._find_node_template(target_name) - target_type = target_node.type_definition - for capability in target_type.get_capabilities_objects(): - if capability.type in hosted_on_rel['valid_target_types']: - if self._attribute_exists_in_type(target_type): - return target_node - return self._find_host_containing_attribute( - target_name) - return None - - def _find_node_template(self, node_template_name): - name = self.context.name if node_template_name == SELF else \ - node_template_name - for node_template in self.tosca_tpl.nodetemplates: - if node_template.name == name: - return node_template - raise KeyError(_( - 'No such node template: {0}.').format(node_template_name)) - - @property - def node_template_name(self): - return self.args[0] - - @property - def attribute_name(self): - return self.args[1] - - -class GetProperty(Function): - """Get a property value of an entity defined in the same service template. - - Arguments: - - * Node template name. - * Requirement or capability name (optional). - * Property name. - - If requirement or capability name is specified, the behavior is as follows: - The req or cap name is first looked up in the specified node template's - requirements. - If found, it would search for a matching capability - of an other node template and get its property as specified in function - arguments. - Otherwise, the req or cap name would be looked up in the specified - node template's capabilities and if found, it would return the property of - the capability as specified in function arguments. - - Examples: - - * { get_property: [ mysql_server, port ] } - * { get_property: [ SELF, db_port ] } - * { get_property: [ SELF, database_endpoint, port ] } - """ - - def validate(self): - if len(self.args) < 2 or len(self.args) > 3: - raise ValueError(_( - 'Expected arguments: [node-template-name, req-or-cap ' - '(optional), property name.')) - if len(self.args) == 2: - prop = self._find_property(self.args[1]).value - if not isinstance(prop, Function): - get_function(self.tosca_tpl, self.context, prop) - elif len(self.args) == 3: - get_function(self.tosca_tpl, - self.context, - self._find_req_or_cap_property(self.args[1], - self.args[2])) - else: - raise NotImplementedError(_( - 'Nested properties are not supported.')) - - def _find_req_or_cap_property(self, req_or_cap, property_name): - node_tpl = self._find_node_template(self.args[0]) - # Find property in node template's requirements - for r in node_tpl.requirements: - for req, node_name in r.items(): - if req == req_or_cap: - node_template = self._find_node_template(node_name) - return self._get_capability_property( - node_template, - req, - property_name) - # If requirement was not found, look in node template's capabilities - return self._get_capability_property(node_tpl, - req_or_cap, - property_name) - - def _get_capability_property(self, - node_template, - capability_name, - property_name): - """Gets a node template capability property.""" - caps = node_template.get_capabilities() - if caps and capability_name in caps.keys(): - cap = caps[capability_name] - # Miguel: Cambios aqui - property = None - props = cap.get_properties() - if props and property_name in props.keys(): - property = props[property_name] - if not property: - raise KeyError(_( - "Property '{0}' not found in capability '{1}' of node" - " template '{2}' referenced from node template" - " '{3}'.").format(property_name, - capability_name, - node_template.name, - self.context.name)) - if property.value: - return property.value - else: - return property.default - msg = _("Requirement/Capability '{0}' referenced from '{1}' node " - "template not found in '{2}' node template.").format( - capability_name, - self.context.name, - node_template.name) - raise KeyError(msg) - - def _find_property(self, property_name): - node_tpl = self._find_node_template(self.args[0]) - props = node_tpl.get_properties() - found = [props[property_name]] if property_name in props else [] - if len(found) == 0: - raise KeyError(_( - "Property: '{0}' not found in node template: {1}.").format( - property_name, node_tpl.name)) - return found[0] - - def _find_node_template(self, node_template_name): - if node_template_name == SELF: - return self.context - # Miguel: cambios aqui - elif node_template_name == HOST: - return self._find_host_containing_property() - for node_template in self.tosca_tpl.nodetemplates: - if node_template.name == node_template_name: - return node_template - raise KeyError(_( - 'No such node template: {0}.').format(node_template_name)) - - # Miguel: anyado esto - def _find_host_containing_property(self, node_template_name=SELF): - node_template = self._find_node_template(node_template_name) - from IM.tosca.toscaparser.elements.entity_type import EntityType - hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] - for r in node_template.requirements: - for requirement, target_name in r.items(): - target_node = self._find_node_template(target_name) - target_type = target_node.type_definition - for capability in target_type.get_capabilities_objects(): - if capability.type in hosted_on_rel['valid_target_types']: - if self._property_exists_in_type(target_type): - return target_node - return self._find_host_containing_attribute( - target_name) - return None - - def _property_exists_in_type(self, type_definition): - props_def = type_definition.get_properties_def() - found = [props_def[self.args[1]]] \ - if self.args[1] in props_def else [] - return len(found) == 1 - - def result(self): - if len(self.args) == 3: - property_value = self._find_req_or_cap_property(self.args[1], - self.args[2]) - else: - property_value = self._find_property(self.args[1]).value - if isinstance(property_value, Function): - return property_value - return get_function(self.tosca_tpl, - self.context, - property_value) - - @property - def node_template_name(self): - return self.args[0] - - @property - def property_name(self): - if len(self.args) > 2: - return self.args[2] - return self.args[1] - - @property - def req_or_cap(self): - if len(self.args) > 2: - return self.args[1] - return None - - -function_mappings = { - GET_PROPERTY: GetProperty, - GET_INPUT: GetInput, - GET_ATTRIBUTE: GetAttribute -} - - -def is_function(function): - """Returns True if the provided function is a Tosca intrinsic function. - - Examples: - - * "{ get_property: { SELF, port } }" - * "{ get_input: db_name }" - * Function instance - - :param function: Function as string or a Function instance. - :return: True if function is a Tosca intrinsic function, otherwise False. - """ - if isinstance(function, dict) and len(function) == 1: - func_name = list(function.keys())[0] - return func_name in function_mappings - return isinstance(function, Function) - - -def get_function(tosca_tpl, node_template, raw_function): - """Gets a Function instance representing the provided template function. - - If the format provided raw_function format is not relevant for template - functions or if the function name doesn't exist in function mapping the - method returns the provided raw_function. - - :param tosca_tpl: The tosca template. - :param node_template: The node template the function is specified for. - :param raw_function: The raw function as dict. - :return: Template function as Function instance or the raw_function if - parsing was unsuccessful. - """ - if is_function(raw_function): - func_name = list(raw_function.keys())[0] - if func_name in function_mappings: - func = function_mappings[func_name] - func_args = list(raw_function.values())[0] - if not isinstance(func_args, list): - func_args = [func_args] - return func(tosca_tpl, node_template, func_name, func_args) - return raw_function diff --git a/IM/tosca/toscaparser/groups.py b/IM/tosca/toscaparser/groups.py deleted file mode 100644 index 40ebcf548..000000000 --- a/IM/tosca/toscaparser/groups.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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. - - -class NodeGroup(object): - - def __init__(self, name, group_templates, member_nodes): - self.name = name - self.tpl = group_templates - self.members = member_nodes - - @property - def member_names(self): - return self.tpl.get('members') - - @property - def policies(self): - return self.tpl.get('policies') diff --git a/IM/tosca/toscaparser/nodetemplate.py b/IM/tosca/toscaparser/nodetemplate.py deleted file mode 100644 index 9288571b1..000000000 --- a/IM/tosca/toscaparser/nodetemplate.py +++ /dev/null @@ -1,242 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.common.exception import InvalidPropertyValueError -from IM.tosca.toscaparser.common.exception import TypeMismatchError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.dataentity import DataEntity -from IM.tosca.toscaparser.elements.interfaces import CONFIGURE -from IM.tosca.toscaparser.elements.interfaces import CONFIGURE_SHORTNAME -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE -from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE_SHORTNAME -from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType -from IM.tosca.toscaparser.entity_template import EntityTemplate -from IM.tosca.toscaparser.relationship_template import RelationshipTemplate -from IM.tosca.toscaparser.utils.gettextutils import _ - -log = logging.getLogger('tosca') - - -class NodeTemplate(EntityTemplate): - '''Node template from a Tosca profile.''' - def __init__(self, name, node_templates, custom_def=None, - available_rel_tpls=None, available_rel_types=None): - super(NodeTemplate, self).__init__(name, node_templates[name], - 'node_type', - custom_def) - self.templates = node_templates - self._validate_fields(node_templates[name]) - self.custom_def = custom_def - self.related = {} - self.relationship_tpl = [] - self.available_rel_tpls = available_rel_tpls - self.available_rel_types = available_rel_types - self._relationships = {} - - @property - def relationships(self): - if not self._relationships: - requires = self.requirements - if requires: - for r in requires: - for _, value in r.items(): - explicit = self._get_explicit_relationship(r, value) - if explicit: - for key, value in explicit.items(): - self._relationships[key] = value - return self._relationships - - def _get_explicit_relationship(self, req, value): - """Handle explicit relationship - - For example, - - req: - node: DBMS - relationship: tosca.relationships.HostedOn - """ - explicit_relation = {} - node = value.get('node') if isinstance(value, dict) else value - - if node: - # TODO(spzala) implement look up once Glance meta data is available - # to find a matching TOSCA node using the TOSCA types - msg = _('Lookup by TOSCA types are not supported. ' - 'Requirement for %s can not be full-filled.') % self.name - if (node in list(self.type_definition.TOSCA_DEF.keys()) - or node in self.custom_def): - raise NotImplementedError(msg) - related_tpl = NodeTemplate(node, self.templates, self.custom_def) - relationship = value.get('relationship') \ - if isinstance(value, dict) else None - # check if it's type has relationship defined - if not relationship: - parent_reqs = self.type_definition.get_all_requirements() - for key in req.keys(): - for req_dict in parent_reqs: - if key in req_dict.keys(): - relationship = (req_dict.get(key). - get('relationship')) - break - if relationship: - found_relationship_tpl = False - # apply available relationship templates if found - # Miguel: add this if - if self.available_rel_tpls: - for tpl in self.available_rel_tpls: - if tpl.name == relationship: - rtype = RelationshipType(tpl.type, None, - self.custom_def) - explicit_relation[rtype] = related_tpl - self.relationship_tpl.append(tpl) - found_relationship_tpl = True - - # create relationship template object. - rel_prfx = self.type_definition.RELATIONSHIP_PREFIX - if not found_relationship_tpl: - if isinstance(relationship, dict): - relationship = relationship.get('type') - if self.available_rel_types and \ - relationship in self.available_rel_types.keys(): - pass - elif not relationship.startswith(rel_prfx): - relationship = rel_prfx + relationship - for rtype in self.type_definition.relationship.keys(): - if rtype.type == relationship: - explicit_relation[rtype] = related_tpl - related_tpl._add_relationship_template(req, - rtype.type) - elif self.available_rel_types: - if relationship in self.available_rel_types.keys(): - rel_type_def = self.available_rel_types.\ - get(relationship) - if 'derived_from' in rel_type_def: - super_type = \ - rel_type_def.get('derived_from') - if not super_type.startswith(rel_prfx): - super_type = rel_prfx + super_type - if rtype.type == super_type: - explicit_relation[rtype] = related_tpl - related_tpl.\ - _add_relationship_template( - req, rtype.type) - return explicit_relation - - def _add_relationship_template(self, requirement, rtype): - req = requirement.copy() - req['type'] = rtype - tpl = RelationshipTemplate(req, rtype, None) - self.relationship_tpl.append(tpl) - - def get_relationship_template(self): - return self.relationship_tpl - - def _add_next(self, nodetpl, relationship): - self.related[nodetpl] = relationship - - @property - def related_nodes(self): - if not self.related: - for relation, node in self.type_definition.relationship.items(): - for tpl in self.templates: - if tpl == node.type: - self.related[NodeTemplate(tpl)] = relation - return self.related.keys() - - def validate(self, tosca_tpl=None): - self._validate_capabilities() - self._validate_requirements() - self._validate_properties(self.entity_tpl, self.type_definition) - self._validate_interfaces() - for prop in self.get_properties_objects(): - prop.validate() - - def _validate_requirements(self): - type_requires = self.type_definition.get_all_requirements() - allowed_reqs = ["template"] - if type_requires: - for treq in type_requires: - for key, value in treq.items(): - allowed_reqs.append(key) - if isinstance(value, dict): - for key in value: - allowed_reqs.append(key) - - requires = self.type_definition.get_value(self.REQUIREMENTS, - self.entity_tpl) - if requires: - if not isinstance(requires, list): - raise TypeMismatchError( - what='Requirements of template %s' % self.name, - type='list') - for req in requires: - for r1, value in req.items(): - if isinstance(value, dict): - self._validate_requirements_keys(value) - self._validate_requirements_properties(value) - allowed_reqs.append(r1) - self._common_validate_field(req, allowed_reqs, 'Requirements') - - def _validate_requirements_properties(self, requirements): - # TODO(anyone): Only occurences property of the requirements is - # validated here. Validation of other requirement properties are being - # validated in different files. Better to keep all the requirements - # properties validation here. - for key, value in requirements.items(): - if key == 'occurrences': - self._validate_occurrences(value) - break - - def _validate_occurrences(self, occurrences): - DataEntity.validate_datatype('list', occurrences) - for value in occurrences: - DataEntity.validate_datatype('integer', value) - if len(occurrences) != 2 or not (0 <= occurrences[0] <= occurrences[1]) \ - or occurrences[1] == 0: - raise InvalidPropertyValueError(what=(occurrences)) - - def _validate_requirements_keys(self, requirement): - for key in requirement.keys(): - if key not in self.REQUIREMENTS_SECTION: - raise UnknownFieldError( - what='Requirements of template %s' % self.name, - field=key) - - def _validate_interfaces(self): - ifaces = self.type_definition.get_value(self.INTERFACES, - self.entity_tpl) - if ifaces: - for i in ifaces: - for name, value in ifaces.items(): - if name in (LIFECYCLE, LIFECYCLE_SHORTNAME): - self._common_validate_field( - value, InterfacesDef. - interfaces_node_lifecycle_operations, - 'Interfaces') - elif name in (CONFIGURE, CONFIGURE_SHORTNAME): - self._common_validate_field( - value, InterfacesDef. - interfaces_relationship_confiure_operations, - 'Interfaces') - else: - raise UnknownFieldError( - what='Interfaces of template %s' % self.name, - field=name) - - def _validate_fields(self, nodetemplate): - for name in nodetemplate.keys(): - if name not in self.SECTIONS: - raise UnknownFieldError(what='Node template %s' - % self.name, field=name) diff --git a/IM/tosca/toscaparser/parameters.py b/IM/tosca/toscaparser/parameters.py deleted file mode 100644 index a8a3f76e4..000000000 --- a/IM/tosca/toscaparser/parameters.py +++ /dev/null @@ -1,110 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.dataentity import DataEntity -from IM.tosca.toscaparser.elements.constraints import Schema -from IM.tosca.toscaparser.elements.entity_type import EntityType -from IM.tosca.toscaparser.utils.gettextutils import _ - - -log = logging.getLogger('tosca') - - -class Input(object): - - INPUTFIELD = (TYPE, DESCRIPTION, DEFAULT, CONSTRAINTS) = \ - ('type', 'description', 'default', 'constraints') - - def __init__(self, name, schema_dict): - self.name = name - self.schema = Schema(name, schema_dict) - - @property - def type(self): - return self.schema.type - - @property - def description(self): - return self.schema.description - - @property - def default(self): - return self.schema.default - - @property - def constraints(self): - return self.schema.constraints - - def validate(self, value=None): - self._validate_field() - self.validate_type(self.type) - if value: - self._validate_value(value) - - def _validate_field(self): - for name in self.schema: - if name not in self.INPUTFIELD: - raise UnknownFieldError(what='Input %s' % self.name, - field=name) - - def validate_type(self, input_type): - if input_type not in Schema.PROPERTY_TYPES: - raise ValueError(_('Invalid type %s') % type) - - def _validate_value(self, value): - tosca = EntityType.TOSCA_DEF - datatype = None - if self.type in tosca: - datatype = tosca[self.type] - elif EntityType.DATATYPE_PREFIX + self.type in tosca: - datatype = tosca[EntityType.DATATYPE_PREFIX + self.type] - - DataEntity.validate_datatype(self.type, value, None, datatype) - - -class Output(object): - - OUTPUTFIELD = (DESCRIPTION, VALUE) = ('description', 'value') - - def __init__(self, name, attrs): - self.name = name - self.attrs = attrs - - @property - def description(self): - return self.attrs[self.DESCRIPTION] - - @property - def value(self): - return self.attrs[self.VALUE] - - def validate(self): - self._validate_field() - - def _validate_field(self): - if not isinstance(self.attrs, dict): - raise MissingRequiredFieldError(what='Output %s' % self.name, - required=self.VALUE) - try: - self.value - except KeyError: - raise MissingRequiredFieldError(what='Output %s' % self.name, - required=self.VALUE) - for name in self.attrs: - if name not in self.OUTPUTFIELD: - raise UnknownFieldError(what='Output %s' % self.name, - field=name) diff --git a/IM/tosca/toscaparser/prereq/__init__.py b/IM/tosca/toscaparser/prereq/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/prereq/csar.py b/IM/tosca/toscaparser/prereq/csar.py deleted file mode 100644 index 9f17b902c..000000000 --- a/IM/tosca/toscaparser/prereq/csar.py +++ /dev/null @@ -1,122 +0,0 @@ -# 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. - -import os.path -import yaml -import zipfile - -from IM.tosca.toscaparser.common.exception import ValidationError -from IM.tosca.toscaparser.utils.gettextutils import _ - - -class CSAR(object): - - def __init__(self, csar_file): - self.csar_file = csar_file - self.is_validated = False - - def validate(self): - """Validate the provided CSAR file.""" - - self.is_validated = True - - # validate that the file exists - if not os.path.isfile(self.csar_file): - err_msg = (_('The file %s does not exist.') % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that it is a valid zip file - if not zipfile.is_zipfile(self.csar_file): - err_msg = (_('The file %s is not a valid zip file.') - % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that it contains the metadata file in the correct location - self.zfile = zipfile.ZipFile(self.csar_file, 'r') - filelist = self.zfile.namelist() - if 'TOSCA-Metadata/TOSCA.meta' not in filelist: - err_msg = (_('The file %s is not a valid CSAR as it does not ' - 'contain the required file "TOSCA.meta" in the ' - 'folder "TOSCA-Metadata".') % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that 'Entry-Definitions' property exists in TOSCA.meta - data = self.zfile.read('TOSCA-Metadata/TOSCA.meta') - invalid_yaml_err_msg = (_('The file "TOSCA-Metadata/TOSCA.meta" in %s ' - 'does not contain valid YAML content.') % - self.csar_file) - try: - meta = yaml.load(data) - if type(meta) is not dict: - raise ValidationError(message=invalid_yaml_err_msg) - self.metadata = meta - except yaml.YAMLError: - raise ValidationError(message=invalid_yaml_err_msg) - - if 'Entry-Definitions' not in self.metadata: - err_msg = (_('The CSAR file "%s" is missing the required metadata ' - '"Entry-Definitions" in "TOSCA-Metadata/TOSCA.meta".') - % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that 'Entry-Definitions' metadata value points to an - # existing file in the CSAR - entry = self.metadata['Entry-Definitions'] - if entry not in filelist: - err_msg = (_('The "Entry-Definitions" file defined in the CSAR ' - '"%s" does not exist.') % self.csar_file) - raise ValidationError(message=err_msg) - - def get_metadata(self): - """Return the metadata dictionary.""" - - # validate the csar if not already validated - if not self.is_validated: - self.validate() - - # return a copy to avoid changes overwrite the original - return dict(self.metadata) if self.metadata else None - - def _get_metadata(self, key): - if not self.is_validated: - self.validate() - return self.metadata[key] if key in self.metadata else None - - def get_author(self): - return self._get_metadata('Created-By') - - def get_version(self): - return self._get_metadata('CSAR-Version') - - def get_main_template(self): - return self._get_metadata('Entry-Definitions') - - def get_description(self): - desc = self._get_metadata('Description') - if desc is not None: - return desc - - main_template = self.get_main_template() - # extract the description from the main template - data = self.zfile.read(main_template) - invalid_tosca_yaml_err_msg = ( - _('The file %(template)s in %(csar)s does not contain valid TOSCA ' - 'YAML content.') % {'template': main_template, - 'csar': self.csar_file}) - try: - tosca_yaml = yaml.load(data) - if type(tosca_yaml) is not dict: - raise ValidationError(message=invalid_tosca_yaml_err_msg) - self.metadata['Description'] = tosca_yaml['description'] - except Exception: - raise ValidationError(message=invalid_tosca_yaml_err_msg) - return self.metadata['Description'] diff --git a/IM/tosca/toscaparser/properties.py b/IM/tosca/toscaparser/properties.py deleted file mode 100644 index f35c4394a..000000000 --- a/IM/tosca/toscaparser/properties.py +++ /dev/null @@ -1,79 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.dataentity import DataEntity -from IM.tosca.toscaparser.elements.constraints import Schema -from IM.tosca.toscaparser.functions import is_function - - -class Property(object): - '''TOSCA built-in Property type.''' - - PROPERTY_KEYS = ( - TYPE, REQUIRED, DESCRIPTION, DEFAULT, CONSTRAINTS - ) = ( - 'type', 'required', 'description', 'default', 'constraints' - ) - - ENTRY_SCHEMA_KEYS = ( - ENTRYTYPE, ENTRYPROPERTIES - ) = ( - 'type', 'properties' - ) - - def __init__(self, property_name, value, schema_dict, custom_def=None): - self.name = property_name - self.value = value - self.custom_def = custom_def - self.schema = Schema(property_name, schema_dict) - - @property - def type(self): - return self.schema.type - - @property - def required(self): - return self.schema.required - - @property - def description(self): - return self.schema.description - - @property - def default(self): - return self.schema.default - - @property - def constraints(self): - return self.schema.constraints - - @property - def entry_schema(self): - return self.schema.entry_schema - - def validate(self): - '''Validate if not a reference property.''' - # Miguel: Cambios aqui - if not is_function(self.value): - if self.value is not None: - if self.type == Schema.STRING: - self.value = str(self.value) - self.value = DataEntity.validate_datatype(self.type, self.value, - self.entry_schema, - self.custom_def) - self._validate_constraints() - - def _validate_constraints(self): - # Miguel: Cambios aqui - if self.value and self.constraints: - for constraint in self.constraints: - constraint.validate(self.value) diff --git a/IM/tosca/toscaparser/relationship_template.py b/IM/tosca/toscaparser/relationship_template.py deleted file mode 100644 index a213595ca..000000000 --- a/IM/tosca/toscaparser/relationship_template.py +++ /dev/null @@ -1,68 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.entity_template import EntityTemplate -from IM.tosca.toscaparser.properties import Property - -SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, - INTERFACES, CAPABILITIES, TYPE) = \ - ('derived_from', 'properties', 'requirements', 'interfaces', - 'capabilities', 'type') - -log = logging.getLogger('tosca') - - -class RelationshipTemplate(EntityTemplate): - '''Relationship template.''' - def __init__(self, relationship_template, name, custom_def=None): - super(RelationshipTemplate, self).__init__(name, - relationship_template, - 'relationship_type', - custom_def) - self.name = name.lower() - - def get_properties_objects(self): - '''Return properties objects for this template.''' - if self._properties is None: - self._properties = self._create_relationship_properties() - return self._properties - - def _create_relationship_properties(self): - props = [] - properties = {} - relationship = self.entity_tpl.get('relationship') - if relationship: - properties = self.type_definition.get_value(self.PROPERTIES, - relationship) or {} - if not properties: - properties = self.entity_tpl.get(self.PROPERTIES) or {} - - if properties: - for name, value in properties.items(): - props_def = self.type_definition.get_properties_def() - if props_def and name in props_def: - if name in properties.keys(): - value = properties.get(name) - prop = Property(name, value, - props_def[name].schema, self.custom_def) - props.append(prop) - for p in self.type_definition.get_properties_def_objects(): - if p.default is not None and p.name not in properties.keys(): - prop = Property(p.name, p.default, p.schema, self.custom_def) - props.append(prop) - return props - - def validate(self): - self._validate_properties(self.entity_tpl, self.type_definition) diff --git a/IM/tosca/toscaparser/topology_template.py b/IM/tosca/toscaparser/topology_template.py deleted file mode 100644 index 6822189fa..000000000 --- a/IM/tosca/toscaparser/topology_template.py +++ /dev/null @@ -1,213 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.common import exception -from IM.tosca.toscaparser import functions -from IM.tosca.toscaparser.groups import NodeGroup -from IM.tosca.toscaparser.nodetemplate import NodeTemplate -from IM.tosca.toscaparser.parameters import Input -from IM.tosca.toscaparser.parameters import Output -from IM.tosca.toscaparser.relationship_template import RelationshipTemplate -from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph - - -# Topology template key names -SECTIONS = (DESCRIPTION, INPUTS, NODE_TEMPLATES, - RELATIONSHIP_TEMPLATES, OUTPUTS, GROUPS, - SUBSTITUION_MAPPINGS) = \ - ('description', 'inputs', 'node_templates', - 'relationship_templates', 'outputs', 'groups', - 'substitution_mappings') - -log = logging.getLogger("tosca.model") - - -class TopologyTemplate(object): - - '''Load the template data.''' - def __init__(self, template, custom_defs, - rel_types=None, parsed_params=None): - self.tpl = template - self.custom_defs = custom_defs - self.rel_types = rel_types - self.parsed_params = parsed_params - self._validate_field() - self.description = self._tpl_description() - self.inputs = self._inputs() - self.relationship_templates = self._relationship_templates() - self.nodetemplates = self._nodetemplates() - self.outputs = self._outputs() - self.graph = ToscaGraph(self.nodetemplates) - self.groups = self._groups() - self._process_intrinsic_functions() - - def _inputs(self): - inputs = [] - for name, attrs in self._tpl_inputs().items(): - input = Input(name, attrs) - if self.parsed_params and name in self.parsed_params: - input.validate(self.parsed_params[name]) - inputs.append(input) - return inputs - - def _nodetemplates(self): - nodetemplates = [] - tpls = self._tpl_nodetemplates() - for name in tpls: - tpl = NodeTemplate(name, tpls, self.custom_defs, - self.relationship_templates, - self.rel_types) - tpl.validate(self) - nodetemplates.append(tpl) - return nodetemplates - - def _relationship_templates(self): - rel_templates = [] - tpls = self._tpl_relationship_templates() - for name in tpls: - tpl = RelationshipTemplate(tpls[name], name, self.custom_defs) - rel_templates.append(tpl) - return rel_templates - - def _outputs(self): - outputs = [] - for name, attrs in self._tpl_outputs().items(): - output = Output(name, attrs) - output.validate() - outputs.append(output) - return outputs - - def _substitution_mappings(self): - pass - - def _groups(self): - groups = [] - for group_name, group_tpl in self._tpl_groups().items(): - member_names = group_tpl.get('members') - if member_names and len(member_names) > 1: - group = NodeGroup(group_name, group_tpl, - self._get_group_memerbs(member_names)) - groups.append(group) - else: - raise ValueError - return groups - - def _get_group_memerbs(self, member_names): - member_nodes = [] - for member in member_names: - for node in self.nodetemplates: - if node.name == member: - member_nodes.append(node) - return member_nodes - - # topology template can act like node template - # it is exposed by substitution_mappings. - def nodetype(self): - pass - - def capabilities(self): - pass - - def requirements(self): - pass - - def _tpl_description(self): - description = self.tpl.get(DESCRIPTION) - if description: - description = description.rstrip() - return description - - def _tpl_inputs(self): - return self.tpl.get(INPUTS) or {} - - def _tpl_nodetemplates(self): - return self.tpl[NODE_TEMPLATES] - - def _tpl_relationship_templates(self): - return self.tpl.get(RELATIONSHIP_TEMPLATES) or {} - - def _tpl_outputs(self): - return self.tpl.get(OUTPUTS) or {} - - def _tpl_substitution_mappings(self): - return self.tpl.get(SUBSTITUION_MAPPINGS) or {} - - def _tpl_groups(self): - return self.tpl.get(GROUPS) or {} - - def _validate_field(self): - for name in self.tpl: - if name not in SECTIONS: - raise exception.UnknownFieldError(what='Template', field=name) - - def _process_intrinsic_functions(self): - """Process intrinsic functions - - Current implementation processes functions within node template - properties, requirements, interfaces inputs and template outputs. - """ - for node_template in self.nodetemplates: - for prop in node_template.get_properties_objects(): - prop.value = functions.get_function(self, - node_template, - prop.value) - for interface in node_template.interfaces: - if interface.inputs: - for name, value in interface.inputs.items(): - interface.inputs[name] = functions.get_function( - self, - node_template, - value) - if node_template.requirements: - for req in node_template.requirements: - rel = req - for req_name, req_item in req.items(): - if isinstance(req_item, dict): - rel = req_item.get('relationship') - break - if rel and 'properties' in rel: - for key, value in rel['properties'].items(): - rel['properties'][key] = functions.get_function( - self, - req, - value) - if node_template.get_capabilities_objects(): - for cap in node_template.get_capabilities_objects(): - if cap.get_properties_objects(): - for prop in cap.get_properties_objects(): - propvalue = functions.get_function( - self, - node_template, - prop.value) - if isinstance(propvalue, functions.GetInput): - propvalue = propvalue.result() - for p, v in cap._properties.items(): - if p == prop.name: - cap._properties[p] = propvalue - for rel, node in node_template.relationships.items(): - rel_tpls = node.relationship_tpl - if rel_tpls: - for rel_tpl in rel_tpls: - for interface in rel_tpl.interfaces: - if interface.inputs: - for name, value in interface.inputs.items(): - interface.inputs[name] = \ - functions.get_function(self, - rel_tpl, - value) - for output in self.outputs: - func = functions.get_function(self, self.outputs, output.value) - if isinstance(func, functions.GetAttribute): - output.attrs[output.VALUE] = func diff --git a/IM/tosca/toscaparser/tosca_template.py b/IM/tosca/toscaparser/tosca_template.py deleted file mode 100644 index 0c7589fec..000000000 --- a/IM/tosca/toscaparser/tosca_template.py +++ /dev/null @@ -1,190 +0,0 @@ -# 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. - - -import logging -import os - -from IM.tosca.toscaparser.common.exception import InvalidTemplateVersion -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.topology_template import TopologyTemplate -from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph -from IM.tosca.toscaparser.utils.gettextutils import _ -import IM.tosca.toscaparser.utils.urlutils -import IM.tosca.toscaparser.utils.yamlparser - - -# TOSCA template key names -SECTIONS = (DEFINITION_VERSION, DEFAULT_NAMESPACE, TEMPLATE_NAME, - TOPOLOGY_TEMPLATE, TEMPLATE_AUTHOR, TEMPLATE_VERSION, - DESCRIPTION, IMPORTS, DSL_DEFINITIONS, NODE_TYPES, - RELATIONSHIP_TYPES, RELATIONSHIP_TEMPLATES, - CAPABILITY_TYPES, ARTIFACT_TYPES, DATATYPE_DEFINITIONS) = \ - ('tosca_definitions_version', 'tosca_default_namespace', - 'template_name', 'topology_template', 'template_author', - 'template_version', 'description', 'imports', 'dsl_definitions', - 'node_types', 'relationship_types', 'relationship_templates', - 'capability_types', 'artifact_types', 'datatype_definitions') - -log = logging.getLogger("tosca.model") - -YAML_LOADER = IM.tosca.toscaparser.utils.yamlparser.load_yaml - - -class ToscaTemplate(object): - - VALID_TEMPLATE_VERSIONS = ['tosca_simple_yaml_1_0'] - - '''Load the template data.''' - def __init__(self, path, a_file=True, parsed_params=None): - self.tpl = YAML_LOADER(path, a_file) - self.path = path - self.a_file = a_file - self.parsed_params = parsed_params - self._validate_field() - self.version = self._tpl_version() - self.relationship_types = self._tpl_relationship_types() - self.description = self._tpl_description() - self.topology_template = self._topology_template() - self.inputs = self._inputs() - self.relationship_templates = self._relationship_templates() - self.nodetemplates = self._nodetemplates() - self.outputs = self._outputs() - self.graph = ToscaGraph(self.nodetemplates) - - def _topology_template(self): - return TopologyTemplate(self._tpl_topology_template(), - self._get_all_custom_defs(), - self.relationship_types, - self.parsed_params) - - def _inputs(self): - return self.topology_template.inputs - - def _nodetemplates(self): - return self.topology_template.nodetemplates - - def _relationship_templates(self): - return self.topology_template.relationship_templates - - def _outputs(self): - return self.topology_template.outputs - - def _tpl_version(self): - return self.tpl[DEFINITION_VERSION] - - def _tpl_description(self): - return self.tpl[DESCRIPTION].rstrip() - - def _tpl_imports(self): - if IMPORTS in self.tpl: - return self.tpl[IMPORTS] - - def _tpl_relationship_types(self): - return self._get_custom_types(RELATIONSHIP_TYPES) - - def _tpl_relationship_templates(self): - topology_template = self._tpl_topology_template() - if RELATIONSHIP_TEMPLATES in topology_template.keys(): - return topology_template[RELATIONSHIP_TEMPLATES] - else: - return None - - def _tpl_topology_template(self): - return self.tpl.get(TOPOLOGY_TEMPLATE) - - def _get_all_custom_defs(self): - types = [NODE_TYPES, CAPABILITY_TYPES, RELATIONSHIP_TYPES, - DATATYPE_DEFINITIONS] - custom_defs = {} - for type in types: - custom_def = self._get_custom_types(type) - if custom_def: - custom_defs.update(custom_def) - return custom_defs - - def _get_custom_types(self, type_definition): - """Handle custom types defined in imported template files - - This method loads the custom type definitions referenced in "imports" - section of the TOSCA YAML template by determining whether each import - is specified via a file reference (by relative or absolute path) or a - URL reference. It then assigns the correct value to "def_file" variable - so the YAML content of those imports can be loaded. - - Possibilities: - +----------+--------+------------------------------+ - | template | import | comment | - +----------+--------+------------------------------+ - | file | file | OK | - | file | URL | OK | - | URL | file | file must be a relative path | - | URL | URL | OK | - +----------+--------+------------------------------+ - """ - - custom_defs = {} - imports = self._tpl_imports() - if imports: - main_a_file = os.path.isfile(self.path) - for definition in imports: - def_file = definition - a_file = False - if main_a_file: - if os.path.isfile(definition): - a_file = True - else: - full_path = os.path.join( - os.path.dirname(os.path.abspath(self.path)), - definition) - if os.path.isfile(full_path): - a_file = True - def_file = full_path - else: # main_a_url - a_url = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ - validate_url(definition) - if not a_url: - if os.path.isabs(definition): - raise ImportError(_("Absolute file name cannot be " - "used for a URL-based input " - "template.")) - def_file = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ - join_url(self.path, definition) - - custom_type = YAML_LOADER(def_file, a_file) - outer_custom_types = custom_type.get(type_definition) - if outer_custom_types: - custom_defs.update(outer_custom_types) - - # Handle custom types defined in current template file - inner_custom_types = self.tpl.get(type_definition) or {} - if inner_custom_types: - custom_defs.update(inner_custom_types) - return custom_defs - - def _validate_field(self): - try: - version = self._tpl_version() - self._validate_version(version) - except KeyError: - raise MissingRequiredFieldError(what='Template', - required=DEFINITION_VERSION) - for name in self.tpl: - if name not in SECTIONS: - raise UnknownFieldError(what='Template', field=name) - - def _validate_version(self, version): - if version not in self.VALID_TEMPLATE_VERSIONS: - raise InvalidTemplateVersion( - what=version, - valid_versions=', '. join(self.VALID_TEMPLATE_VERSIONS)) diff --git a/IM/tosca/toscaparser/tpl_relationship_graph.py b/IM/tosca/toscaparser/tpl_relationship_graph.py deleted file mode 100644 index 1a5ea7b66..000000000 --- a/IM/tosca/toscaparser/tpl_relationship_graph.py +++ /dev/null @@ -1,46 +0,0 @@ -# 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. - - -class ToscaGraph(object): - '''Graph of Tosca Node Templates.''' - def __init__(self, nodetemplates): - self.nodetemplates = nodetemplates - self.vertices = {} - self._create() - - def _create_vertex(self, node): - if node not in self.vertices: - self.vertices[node.name] = node - - def _create_edge(self, node1, node2, relationship): - if node1 not in self.vertices: - self._create_vertex(node1) - self.vertices[node1.name]._add_next(node2, - relationship) - - def vertex(self, node): - if node in self.vertices: - return self.vertices[node] - - def __iter__(self): - return iter(self.vertices.values()) - - def _create(self): - for node in self.nodetemplates: - relation = node.relationships - if relation: - for rel, nodetpls in relation.items(): - for tpl in self.nodetemplates: - if tpl.name == nodetpls.name: - self._create_edge(node, tpl, rel) - self._create_vertex(node) diff --git a/IM/tosca/toscaparser/utils/__init__.py b/IM/tosca/toscaparser/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/utils/gettextutils.py b/IM/tosca/toscaparser/utils/gettextutils.py deleted file mode 100644 index f5562e2d7..000000000 --- a/IM/tosca/toscaparser/utils/gettextutils.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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. - -import gettext -import os - -_localedir = os.environ.get('tosca-parser'.upper() + '_LOCALEDIR') -_t = gettext.translation('tosca-parser', localedir=_localedir, - fallback=True) - - -def _(msg): - return _t.gettext(msg) diff --git a/IM/tosca/toscaparser/utils/urlutils.py b/IM/tosca/toscaparser/utils/urlutils.py deleted file mode 100644 index 628314cdf..000000000 --- a/IM/tosca/toscaparser/utils/urlutils.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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. - - -from six.moves.urllib.parse import urljoin -from six.moves.urllib.parse import urlparse -from IM.tosca.toscaparser.utils.gettextutils import _ - - -class UrlUtils(object): - - @staticmethod - def validate_url(path): - """Validates whether the given path is a URL or not. - - If the given path includes a scheme (http, https, ftp, ...) and a net - location (a domain name such as www.github.com) it is validated as a - URL. - """ - parsed = urlparse(path) - return bool(parsed.scheme) and bool(parsed.netloc) - - @staticmethod - def join_url(url, relative_path): - """Builds a new URL from the given URL and the relative path. - - Example: - url: http://www.githib.com/openstack/heat - relative_path: heat-translator - - joined: http://www.githib.com/openstack/heat-translator - """ - if not UrlUtils.validate_url(url): - raise ValueError(_("Provided URL is invalid.")) - return urljoin(url, relative_path) diff --git a/IM/tosca/toscaparser/utils/validateutils.py b/IM/tosca/toscaparser/utils/validateutils.py deleted file mode 100644 index 42bfc4664..000000000 --- a/IM/tosca/toscaparser/utils/validateutils.py +++ /dev/null @@ -1,154 +0,0 @@ -# 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. - -import collections -import dateutil.parser -import logging -import numbers -import re -import six - -from IM.tosca.toscaparser.common.exception import ( - InvalidTOSCAVersionPropertyException) -from IM.tosca.toscaparser.utils.gettextutils import _ -log = logging.getLogger('tosca') - - -def str_to_num(value): - '''Convert a string representation of a number into a numeric type.''' - if isinstance(value, numbers.Number): - return value - try: - return int(value) - except ValueError: - return float(value) - - -def validate_number(value): - return str_to_num(value) - - -def validate_integer(value): - if not isinstance(value, int): - try: - value = int(value) - except Exception: - raise ValueError(_('"%s" is not an integer') % value) - return value - - -def validate_float(value): - if not isinstance(value, float): - raise ValueError(_('"%s" is not a float') % value) - return validate_number(value) - - -def validate_string(value): - if not isinstance(value, six.string_types): - raise ValueError(_('"%s" is not a string') % value) - return value - - -def validate_list(value): - if not isinstance(value, list): - raise ValueError(_('"%s" is not a list') % value) - return value - - -def validate_map(value): - if not isinstance(value, collections.Mapping): - raise ValueError(_('"%s" is not a map') % value) - return value - - -def validate_boolean(value): - if isinstance(value, bool): - return value - - if isinstance(value, str): - normalised = value.lower() - if normalised in ['true', 'false']: - return normalised == 'true' - raise ValueError(_('"%s" is not a boolean') % value) - - -def validate_timestamp(value): - return dateutil.parser.parse(value) - - -class TOSCAVersionProperty(object): - - VERSION_RE = re.compile('^(?P([0-9][0-9]*))' - '(\.(?P([0-9][0-9]*)))?' - '(\.(?P([0-9][0-9]*)))?' - '(\.(?P([0-9A-Za-z]+)))?' - '(\-(?P[0-9])*)?$') - - def __init__(self, version): - self.version = str(version) - match = self.VERSION_RE.match(self.version) - if not match: - raise InvalidTOSCAVersionPropertyException(what=(self.version)) - ver = match.groupdict() - if self.version in ['0', '0.0', '0.0.0']: - log.warning(_('Version assumed as not provided')) - self.version = None - self.minor_version = ver['minor_version'] - self.major_version = ver['major_version'] - self.fix_version = ver['fix_version'] - self.qualifier = self._validate_qualifier(ver['qualifier']) - self.build_version = self._validate_build(ver['build_version']) - self._validate_major_version(self.major_version) - - def _validate_major_version(self, value): - """Validate major version - - Checks if only major version is provided and assumes - minor version as 0. - Eg: If version = 18, then it returns version = '18.0' - """ - - if self.minor_version is None and self.build_version is None and \ - value != '0': - log.warning(_('Minor version assumed "0"')) - self.version = '.'.join([value, '0']) - return value - - def _validate_qualifier(self, value): - """Validate qualifier - - TOSCA version is invalid if a qualifier is present without the - fix version or with all of major, minor and fix version 0s. - - For example, the following versions are invalid - 18.0.abc - 0.0.0.abc - """ - if (self.fix_version is None and value) or \ - (self.minor_version == self.major_version == - self.fix_version == '0' and value): - raise InvalidTOSCAVersionPropertyException(what=(self.version)) - return value - - def _validate_build(self, value): - """Validate build version - - TOSCA version is invalid if build version is present without the - qualifier. - Eg: version = 18.0.0-1 is invalid. - """ - if not self.qualifier and value: - raise InvalidTOSCAVersionPropertyException(what=(self.version)) - return value - - def get_version(self): - return self.version diff --git a/IM/tosca/toscaparser/utils/yamlparser.py b/IM/tosca/toscaparser/utils/yamlparser.py deleted file mode 100644 index cd6c5b224..000000000 --- a/IM/tosca/toscaparser/utils/yamlparser.py +++ /dev/null @@ -1,73 +0,0 @@ -# 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. - -import codecs -from collections import OrderedDict -import yaml - -try: - # Python 3.x - import urllib.request as urllib2 -except ImportError: - # Python 2.x - import urllib2 - -if hasattr(yaml, 'CSafeLoader'): - yaml_loader = yaml.CSafeLoader -else: - yaml_loader = yaml.SafeLoader - - -def load_yaml(path, a_file=True): - # Miguel: enable to load also a TOSCA string - if path.find("\n") == -1: - f = codecs.open(path, encoding='utf-8', errors='strict') if a_file \ - else urllib2.urlopen(path) - return yaml.load(f.read(), Loader=yaml_loader) - else: - return yaml.load(path, Loader=yaml_loader) - - -def simple_parse(tmpl_str): - try: - tpl = yaml.load(tmpl_str, Loader=yaml_loader) - except yaml.YAMLError as yea: - raise ValueError(yea) - else: - if tpl is None: - tpl = {} - return tpl - - -def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict): - class OrderedLoader(Loader): - pass - - def construct_mapping(loader, node): - loader.flatten_mapping(node) - return object_pairs_hook(loader.construct_pairs(node)) - - OrderedLoader.add_constructor( - yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, - construct_mapping) - return yaml.load(stream, OrderedLoader) - - -def simple_ordered_parse(tmpl_str): - try: - tpl = ordered_load(tmpl_str) - except yaml.YAMLError as yea: - raise ValueError(yea) - else: - if tpl is None: - tpl = {} - return tpl From 89766fd0564c745792d91f5caef7efa12f7394cb Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:17:51 +0100 Subject: [PATCH 008/509] Bugfix when stopping the im service --- IM/InfrastructureInfo.py | 11 +- IM/InfrastructureManager.py | 8 +- IM/tosca/toscaparser/__init__.py | 19 - IM/tosca/toscaparser/capabilities.py | 57 -- IM/tosca/toscaparser/common/__init__.py | 0 IM/tosca/toscaparser/common/exception.py | 100 -- IM/tosca/toscaparser/dataentity.py | 159 ---- .../elements/TOSCA_definition_1_0.yaml | 893 ------------------ IM/tosca/toscaparser/elements/__init__.py | 0 IM/tosca/toscaparser/elements/artifacttype.py | 45 - .../elements/attribute_definition.py | 20 - .../toscaparser/elements/capabilitytype.py | 71 -- IM/tosca/toscaparser/elements/constraints.py | 569 ----------- IM/tosca/toscaparser/elements/datatype.py | 56 -- IM/tosca/toscaparser/elements/entity_type.py | 113 --- IM/tosca/toscaparser/elements/interfaces.py | 74 -- IM/tosca/toscaparser/elements/nodetype.py | 200 ---- IM/tosca/toscaparser/elements/policytype.py | 45 - .../elements/property_definition.py | 46 - .../toscaparser/elements/relationshiptype.py | 33 - IM/tosca/toscaparser/elements/scalarunit.py | 130 --- .../elements/statefulentitytype.py | 81 -- IM/tosca/toscaparser/entity_template.py | 285 ------ IM/tosca/toscaparser/functions.py | 410 -------- IM/tosca/toscaparser/groups.py | 27 - IM/tosca/toscaparser/nodetemplate.py | 242 ----- IM/tosca/toscaparser/parameters.py | 110 --- IM/tosca/toscaparser/prereq/__init__.py | 0 IM/tosca/toscaparser/prereq/csar.py | 122 --- IM/tosca/toscaparser/properties.py | 79 -- IM/tosca/toscaparser/relationship_template.py | 68 -- IM/tosca/toscaparser/topology_template.py | 213 ----- IM/tosca/toscaparser/tosca_template.py | 190 ---- .../toscaparser/tpl_relationship_graph.py | 46 - IM/tosca/toscaparser/utils/__init__.py | 0 IM/tosca/toscaparser/utils/gettextutils.py | 22 - IM/tosca/toscaparser/utils/urlutils.py | 43 - IM/tosca/toscaparser/utils/validateutils.py | 154 --- IM/tosca/toscaparser/utils/yamlparser.py | 73 -- 39 files changed, 12 insertions(+), 4802 deletions(-) delete mode 100644 IM/tosca/toscaparser/__init__.py delete mode 100644 IM/tosca/toscaparser/capabilities.py delete mode 100644 IM/tosca/toscaparser/common/__init__.py delete mode 100644 IM/tosca/toscaparser/common/exception.py delete mode 100644 IM/tosca/toscaparser/dataentity.py delete mode 100644 IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml delete mode 100644 IM/tosca/toscaparser/elements/__init__.py delete mode 100644 IM/tosca/toscaparser/elements/artifacttype.py delete mode 100644 IM/tosca/toscaparser/elements/attribute_definition.py delete mode 100644 IM/tosca/toscaparser/elements/capabilitytype.py delete mode 100644 IM/tosca/toscaparser/elements/constraints.py delete mode 100644 IM/tosca/toscaparser/elements/datatype.py delete mode 100644 IM/tosca/toscaparser/elements/entity_type.py delete mode 100644 IM/tosca/toscaparser/elements/interfaces.py delete mode 100644 IM/tosca/toscaparser/elements/nodetype.py delete mode 100644 IM/tosca/toscaparser/elements/policytype.py delete mode 100644 IM/tosca/toscaparser/elements/property_definition.py delete mode 100644 IM/tosca/toscaparser/elements/relationshiptype.py delete mode 100644 IM/tosca/toscaparser/elements/scalarunit.py delete mode 100644 IM/tosca/toscaparser/elements/statefulentitytype.py delete mode 100644 IM/tosca/toscaparser/entity_template.py delete mode 100644 IM/tosca/toscaparser/functions.py delete mode 100644 IM/tosca/toscaparser/groups.py delete mode 100644 IM/tosca/toscaparser/nodetemplate.py delete mode 100644 IM/tosca/toscaparser/parameters.py delete mode 100644 IM/tosca/toscaparser/prereq/__init__.py delete mode 100644 IM/tosca/toscaparser/prereq/csar.py delete mode 100644 IM/tosca/toscaparser/properties.py delete mode 100644 IM/tosca/toscaparser/relationship_template.py delete mode 100644 IM/tosca/toscaparser/topology_template.py delete mode 100644 IM/tosca/toscaparser/tosca_template.py delete mode 100644 IM/tosca/toscaparser/tpl_relationship_graph.py delete mode 100644 IM/tosca/toscaparser/utils/__init__.py delete mode 100644 IM/tosca/toscaparser/utils/gettextutils.py delete mode 100644 IM/tosca/toscaparser/utils/urlutils.py delete mode 100644 IM/tosca/toscaparser/utils/validateutils.py delete mode 100644 IM/tosca/toscaparser/utils/yamlparser.py diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 9d24078af..d18ed58d6 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -119,16 +119,21 @@ def delete(self): """ Set this Inf as deleted """ - self.stop_cm_thread() + self.stop() self.deleted = True - def stop_cm_thread(self): + def stop(self): """ - Stop the Ctxt thread if is is alive. + Stop all the Ctxt threads """ + # Stop the Ctxt thread if it is alive. if self.cm and self.cm.isAlive(): self.cm.stop() + # kill all the ctxt processes in the VMs + for vm in self.get_vm_list(): + vm.kill_check_ctxt_process() + def get_cont_out(self): """ Returns the contextualization message diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 533054268..a6578e513 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1052,7 +1052,7 @@ def CreateInfrastructure(radl, auth): InfrastructureManager.logger.exception("Error Creating Inf id " + str(inf.id)) inf.delete() InfrastructureManager.save_data(inf.id) - InfrastructureManager.remove_inf(inf.id) + InfrastructureManager.remove_inf(inf) raise e InfrastructureManager.logger.info("Infrastructure id " + str(inf.id) + " successfully created") @@ -1094,7 +1094,7 @@ def ExportInfrastructure(inf_id, delete, auth_data): if delete: sel_inf.delete() InfrastructureManager.save_data(sel_inf.id) - InfrastructureManager.remove_inf(sel_inf.id) + InfrastructureManager.remove_inf(sel_inf) return str_inf @staticmethod @@ -1202,6 +1202,6 @@ def stop(): # Acquire the lock to avoid writing data to the DATA_FILE with InfrastructureManager._lock: InfrastructureManager._exiting = True - # Stop all the Ctxt threads of the + # Stop all the Ctxt threads of the Infrastructure for inf in InfrastructureManager.infrastructure_list.values(): - inf.stop_cm_thread() \ No newline at end of file + inf.stop() \ No newline at end of file diff --git a/IM/tosca/toscaparser/__init__.py b/IM/tosca/toscaparser/__init__.py deleted file mode 100644 index f418d00a9..000000000 --- a/IM/tosca/toscaparser/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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. - -import pbr.version - -__version__ = "1.0.0" -#__version__ = pbr.version.VersionInfo( -# 'tosca-parser').version_string() diff --git a/IM/tosca/toscaparser/capabilities.py b/IM/tosca/toscaparser/capabilities.py deleted file mode 100644 index 5af77f862..000000000 --- a/IM/tosca/toscaparser/capabilities.py +++ /dev/null @@ -1,57 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.properties import Property - - -class Capability(object): - '''TOSCA built-in capabilities type.''' - - def __init__(self, name, properties, definition): - self.name = name - self._properties = properties - self.definition = definition - - def get_properties_objects(self): - '''Return a list of property objects.''' - properties = [] - # Miguel: cambios aqui - props_def = self.definition.get_properties_def() - if props_def: - props_name = props_def.keys() - - for name in props_name: - value = None - if name in self._properties: - value = self._properties[name] - properties.append(Property(name, value, props_def[name].schema)) - -# props = self._properties -# -# if props: -# for name, value in props.items(): -# props_def = self.definition.get_properties_def() -# if props_def and name in props_def: -# properties.append(Property(name, value, -# props_def[name].schema)) - return properties - - def get_properties(self): - '''Return a dictionary of property name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_objects()} - - def get_property_value(self, name): - '''Return the value of a given property name.''' - props = self.get_properties() - if props and name in props: - return props[name].value diff --git a/IM/tosca/toscaparser/common/__init__.py b/IM/tosca/toscaparser/common/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/common/exception.py b/IM/tosca/toscaparser/common/exception.py deleted file mode 100644 index fb2eea9e6..000000000 --- a/IM/tosca/toscaparser/common/exception.py +++ /dev/null @@ -1,100 +0,0 @@ -# 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. - -''' -TOSCA exception classes -''' -import logging -import sys - -from IM.tosca.toscaparser.utils.gettextutils import _ - - -log = logging.getLogger(__name__) - - -class TOSCAException(Exception): - '''Base exception class for TOSCA - - To correctly use this class, inherit from it and define - a 'msg_fmt' property. - - ''' - - _FATAL_EXCEPTION_FORMAT_ERRORS = False - - message = _('An unknown exception occurred.') - - def __init__(self, **kwargs): - try: - self.message = self.msg_fmt % kwargs - except KeyError: - exc_info = sys.exc_info() - log.exception(_('Exception in string format operation: %s') - % exc_info[1]) - - if TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS: - raise exc_info[0] - - def __str__(self): - return self.message - - @staticmethod - def set_fatal_format_exception(flag): - if isinstance(flag, bool): - TOSCAException._FATAL_EXCEPTION_FORMAT_ERRORS = flag - - -class MissingRequiredFieldError(TOSCAException): - msg_fmt = _('%(what)s is missing required field: "%(required)s".') - - -class UnknownFieldError(TOSCAException): - msg_fmt = _('%(what)s contain(s) unknown field: "%(field)s", ' - 'refer to the definition to verify valid values.') - - -class TypeMismatchError(TOSCAException): - msg_fmt = _('%(what)s must be of type: "%(type)s".') - - -class InvalidNodeTypeError(TOSCAException): - msg_fmt = _('Node type "%(what)s" is not a valid type.') - - -class InvalidTypeError(TOSCAException): - msg_fmt = _('Type "%(what)s" is not a valid type.') - - -class InvalidSchemaError(TOSCAException): - msg_fmt = _("%(message)s") - - -class ValidationError(TOSCAException): - msg_fmt = _("%(message)s") - - -class UnknownInputError(TOSCAException): - msg_fmt = _('Unknown input: %(input_name)s') - - -class InvalidPropertyValueError(TOSCAException): - msg_fmt = _('Value of property "%(what)s" is invalid.') - - -class InvalidTemplateVersion(TOSCAException): - msg_fmt = _('The template version "%(what)s" is invalid. ' - 'The valid versions are: "%(valid_versions)s"') - - -class InvalidTOSCAVersionPropertyException(TOSCAException): - msg_fmt = _('Value of TOSCA version property "%(what)s" is invalid.') diff --git a/IM/tosca/toscaparser/dataentity.py b/IM/tosca/toscaparser/dataentity.py deleted file mode 100644 index eb67e63a4..000000000 --- a/IM/tosca/toscaparser/dataentity.py +++ /dev/null @@ -1,159 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import TypeMismatchError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.elements.constraints import Schema -from IM.tosca.toscaparser.elements.datatype import DataType -from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Frequency -from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Size -from IM.tosca.toscaparser.elements.scalarunit import ScalarUnit_Time - -from IM.tosca.toscaparser.utils.gettextutils import _ -from IM.tosca.toscaparser.utils import validateutils - - -class DataEntity(object): - '''A complex data value entity.''' - - def __init__(self, datatypename, value_dict, custom_def=None): - self.custom_def = custom_def - self.datatype = DataType(datatypename, custom_def) - self.schema = self.datatype.get_all_properties() - self.value = value_dict - - def validate(self): - '''Validate the value by the definition of the datatype.''' - - # A datatype can not have both 'type' and 'properties' definitions. - # If the datatype has 'type' definition - if self.datatype.value_type: - self.value = DataEntity.validate_datatype(self.datatype.value_type, - self.value, - None, - self.custom_def) - schema = Schema(None, self.datatype.defs) - for constraint in schema.constraints: - constraint.validate(self.value) - # If the datatype has 'properties' definition - else: - if not isinstance(self.value, dict): - raise TypeMismatchError(what=self.value, - type=self.datatype.type) - allowed_props = [] - required_props = [] - default_props = {} - if self.schema: - allowed_props = self.schema.keys() - for name, prop_def in self.schema.items(): - if prop_def.required: - required_props.append(name) - if prop_def.default: - default_props[name] = prop_def.default - - # check allowed field - for value_key in list(self.value.keys()): - if value_key not in allowed_props: - raise UnknownFieldError(what=_('Data value of type %s') - % self.datatype.type, - field=value_key) - - # check default field - for def_key, def_value in list(default_props.items()): - if def_key not in list(self.value.keys()): - self.value[def_key] = def_value - - # check missing field - missingprop = [] - for req_key in required_props: - if req_key not in list(self.value.keys()): - missingprop.append(req_key) - if missingprop: - raise MissingRequiredFieldError(what=_('Data value of type %s') - % self.datatype.type, - required=missingprop) - - # check every field - for name, value in list(self.value.items()): - prop_schema = Schema(name, self._find_schema(name)) - # check if field value meets type defined - DataEntity.validate_datatype(prop_schema.type, value, - prop_schema.entry_schema, - self.custom_def) - # check if field value meets constraints defined - if prop_schema.constraints: - for constraint in prop_schema.constraints: - constraint.validate(value) - - return self.value - - def _find_schema(self, name): - if self.schema and name in self.schema.keys(): - return self.schema[name].schema - - @staticmethod - def validate_datatype(type, value, entry_schema=None, custom_def=None): - '''Validate value with given type. - - If type is list or map, validate its entry by entry_schema(if defined) - If type is a user-defined complex datatype, custom_def is required. - ''' - if type == Schema.STRING: - return validateutils.validate_string(value) - elif type == Schema.INTEGER: - return validateutils.validate_integer(value) - elif type == Schema.FLOAT: - return validateutils.validate_float(value) - elif type == Schema.NUMBER: - return validateutils.validate_number(value) - elif type == Schema.BOOLEAN: - return validateutils.validate_boolean(value) - elif type == Schema.TIMESTAMP: - validateutils.validate_timestamp(value) - return value - elif type == Schema.LIST: - validateutils.validate_list(value) - if entry_schema: - DataEntity.validate_entry(value, entry_schema, custom_def) - return value - elif type == Schema.SCALAR_UNIT_SIZE: - return ScalarUnit_Size(value).validate_scalar_unit() - elif type == Schema.SCALAR_UNIT_FREQUENCY: - return ScalarUnit_Frequency(value).validate_scalar_unit() - elif type == Schema.SCALAR_UNIT_TIME: - return ScalarUnit_Time(value).validate_scalar_unit() - elif type == Schema.VERSION: - return validateutils.TOSCAVersionProperty(value).get_version() - elif type == Schema.MAP: - validateutils.validate_map(value) - if entry_schema: - DataEntity.validate_entry(value, entry_schema, custom_def) - return value - else: - data = DataEntity(type, value, custom_def) - return data.validate() - - @staticmethod - def validate_entry(value, entry_schema, custom_def=None): - '''Validate entries for map and list.''' - schema = Schema(None, entry_schema) - valuelist = value - if isinstance(value, dict): - valuelist = list(value.values()) - for v in valuelist: - DataEntity.validate_datatype(schema.type, v, schema.entry_schema, - custom_def) - if schema.constraints: - for constraint in schema.constraints: - constraint.validate(v) - return value diff --git a/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml b/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml deleted file mode 100644 index b819c02b4..000000000 --- a/IM/tosca/toscaparser/elements/TOSCA_definition_1_0.yaml +++ /dev/null @@ -1,893 +0,0 @@ -# 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. - -########################################################################## -# The content of this file reflects TOSCA Simple Profile in YAML version -# 1.0.0. It describes the definition for TOSCA types including Node Type, -# Relationship Type, Capability Type and Interfaces. -########################################################################## -tosca_definitions_version: tosca_simple_yaml_1_0 - -########################################################################## -# Node Type. -# A Node Type is a reusable entity that defines the type of one or more -# Node Templates. -########################################################################## -tosca.nodes.Root: - description: > - The TOSCA root node all other TOSCA base node types derive from. - attributes: - tosca_id: - type: string - tosca_name: - type: string - state: - type: string - capabilities: - feature: - type: tosca.capabilities.Node - requirements: - - dependency: - capability: tosca.capabilities.Node - node: tosca.nodes.Root - relationship: tosca.relationships.DependsOn - occurrences: [ 0, UNBOUNDED ] - interfaces: - Standard: - type: tosca.interfaces.node.lifecycle.Standard - -tosca.nodes.Compute: - derived_from: tosca.nodes.Root - attributes: - private_address: - type: string - public_address: - type: string - capabilities: - host: - type: tosca.capabilities.Container - binding: - type: tosca.capabilities.network.Bindable - os: - type: tosca.capabilities.OperatingSystem - scalable: - type: tosca.capabilities.Scalable - requirements: - - local_storage: - capability: tosca.capabilities.Attachment - node: tosca.nodes.BlockStorage - relationship: tosca.relationships.AttachesTo - occurrences: [0, UNBOUNDED] - -tosca.nodes.SoftwareComponent: - derived_from: tosca.nodes.Root - properties: - # domain-specific software component version - component_version: - type: version - required: false - description: > - Software component version. - admin_credential: - type: tosca.datatypes.Credential - required: false - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.Compute - relationship: tosca.relationships.HostedOn - -tosca.nodes.DBMS: - derived_from: tosca.nodes.SoftwareComponent - properties: - port: - required: no - type: integer - description: > - The port the DBMS service will listen to for data and requests. - root_password: - required: no - type: string - description: > - The root password for the DBMS service. - capabilities: - host: - type: tosca.capabilities.Container - valid_source_types: [tosca.nodes.Database] - -tosca.nodes.Database: - derived_from: tosca.nodes.Root - properties: - user: - required: no - type: string - description: > - User account name for DB administration - name: - required: no - type: string - description: > - The name of the database. - password: - required: no - type: string - description: > - The password for the DB user account - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.DBMS - relationship: tosca.relationships.HostedOn - capabilities: - database_endpoint: - type: tosca.capabilities.Endpoint.Database - -tosca.nodes.WebServer: - derived_from: tosca.nodes.SoftwareComponent - capabilities: - data_endpoint: - type: tosca.capabilities.Endpoint - admin_endpoint: - type: tosca.capabilities.Endpoint.Admin - host: - type: tosca.capabilities.Container - valid_source_types: [tosca.nodes.WebApplication] - -tosca.nodes.WebApplication: - derived_from: tosca.nodes.Root - properties: - context_root: - type: string - required: false - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.WebServer - relationship: tosca.relationships.HostedOn - capabilities: - app_endpoint: - type: tosca.capabilities.Endpoint - -tosca.nodes.BlockStorage: - derived_from: tosca.nodes.Root - properties: - size: - type: scalar-unit.size - constraints: - - greater_or_equal: 1 MB - volume_id: - type: string - required: false - snapshot_id: - type: string - required: false - attributes: - volume_id: - type: string - capabilities: - attachment: - type: tosca.capabilities.Attachment - -tosca.nodes.network.Network: - derived_from: tosca.nodes.Root - description: > - The TOSCA Network node represents a simple, logical network service. - properties: - ip_version: - type: integer - required: no - default: 4 - constraints: - - valid_values: [ 4, 6 ] - description: > - The IP version of the requested network. Valid values are 4 for ipv4 - or 6 for ipv6. - cidr: - type: string - required: no - description: > - The cidr block of the requested network. - start_ip: - type: string - required: no - description: > - The IP address to be used as the start of a pool of addresses within - the full IP range derived from the cidr block. - end_ip: - type: string - required: no - description: > - The IP address to be used as the end of a pool of addresses within - the full IP range derived from the cidr block. - gateway_ip: - type: string - required: no - description: > - The gateway IP address. - network_name: - type: string - required: no - description: > - An identifier that represents an existing Network instance in the - underlying cloud infrastructure or can be used as the name of the - newly created network. If network_name is provided and no other - properties are provided (with exception of network_id), then an - existing network instance will be used. If network_name is provided - alongside with more properties then a new network with this name will - be created. - network_id: - type: string - required: no - description: > - An identifier that represents an existing Network instance in the - underlying cloud infrastructure. This property is mutually exclusive - with all other properties except network_name. This can be used alone - or together with network_name to identify an existing network. - network_type: - type: string - required: no - description: > - It specifies the nature of the physical network in the underlying - cloud infrastructure. Examples are flat, vlan, gre or vxlan. F - segmentation_id: - type: string - required: no - description: > - A segmentation identifier in the underlying cloud infrastructure. - E.g. VLAN ID, GRE tunnel ID, etc.. - dhcp_enabled: - type: boolean - required: no - default: true - description: > - Indicates should DHCP service be enabled on the network or not. - capabilities: - link: - type: tosca.capabilities.network.Linkable - -tosca.nodes.network.Port: - derived_from: tosca.nodes.Root - description: > - The TOSCA Port node represents a logical entity that associates between - Compute and Network normative types. The Port node type effectively - represents a single virtual NIC on the Compute node instance. - properties: - ip_address: - type: string - required: no - description: > - Allow the user to set a static IP. - order: - type: integer - required: no - default: 0 - constraints: - - greater_or_equal: 0 - description: > - The order of the NIC on the compute instance (e.g. eth2). - is_default: - type: boolean - required: no - default: false - description: > - If is_default=true this port will be used for the default gateway - route. Only one port that is associated to single compute node can - set as is_default=true. - ip_range_start: - type: string - required: no - description: > - Defines the starting IP of a range to be allocated for the compute - instances that are associated with this Port. - ip_range_end: - type: string - required: no - description: > - Defines the ending IP of a range to be allocated for the compute - instances that are associated with this Port. - attributes: - ip_address: - type: string - requirements: - - binding: - description: > - Binding requirement expresses the relationship between Port and - Compute nodes. Effectevely it indicates that the Port will be - attached to specific Compute node instance - capability: tosca.capabilities.network.Bindable - relationship: tosca.relationships.network.BindsTo - - link: - description: > - Link requirement expresses the relationship between Port and Network - nodes. It indicates which network this port will connect to. - capability: tosca.capabilities.network.Linkable - relationship: tosca.relationships.network.LinksTo - -tosca.nodes.ObjectStorage: - derived_from: tosca.nodes.Root - description: > - The TOSCA ObjectStorage node represents storage that provides the ability - to store data as objects (or BLOBs of data) without consideration for the - underlying filesystem or devices - properties: - name: - type: string - required: yes - description: > - The logical name of the object store (or container). - size: - type: scalar-unit.size - required: no - constraints: - - greater_or_equal: 0 GB - description: > - The requested initial storage size. - maxsize: - type: scalar-unit.size - required: no - constraints: - - greater_or_equal: 0 GB - description: > - The requested maximum storage size. - capabilities: - storage_endpoint: - type: tosca.capabilities.Endpoint - -########################################################################## -# Relationship Type. -# A Relationship Type is a reusable entity that defines the type of one -# or more relationships between Node Types or Node Templates. -########################################################################## -tosca.relationships.Root: - description: > - The TOSCA root Relationship Type all other TOSCA base Relationship Types - derive from. - attributes: - tosca_id: - type: string - tosca_name: - type: string - interfaces: - Configure: - type: tosca.interfaces.relationship.Configure - -tosca.relationships.DependsOn: - derived_from: tosca.relationships.Root - -tosca.relationships.HostedOn: - derived_from: tosca.relationships.Root - valid_target_types: [ tosca.capabilities.Container ] - -tosca.relationships.ConnectsTo: - derived_from: tosca.relationships.Root - valid_target_types: [ tosca.capabilities.Endpoint ] - credential: - type: tosca.datatypes.Credential - required: false - -tosca.relationships.AttachesTo: - derived_from: tosca.relationships.Root - valid_target_types: [ tosca.capabilities.Attachment ] - properties: - location: - required: true - type: string - constraints: - - min_length: 1 - device: - required: false - type: string - -tosca.relationships.network.LinksTo: - derived_from: tosca.relationships.DependsOn - valid_target_types: [ tosca.capabilities.network.Linkable ] - -tosca.relationships.network.BindsTo: - derived_from: tosca.relationships.DependsOn - valid_target_types: [ tosca.capabilities.network.Bindable ] - -########################################################################## -# Capability Type. -# A Capability Type is a reusable entity that describes a kind of -# capability that a Node Type can declare to expose. -########################################################################## -tosca.capabilities.Root: - description: > - The TOSCA root Capability Type all other TOSCA base Capability Types - derive from. - -tosca.capabilities.Node: - derived_from: tosca.capabilities.Root - -tosca.capabilities.Container: - derived_from: tosca.capabilities.Root - properties: - num_cpus: - required: no - type: integer - constraints: - - greater_or_equal: 1 - cpu_frequency: - required: no - type: scalar-unit.frequency - constraints: - - greater_or_equal: 0.1 GHz - disk_size: - required: no - type: scalar-unit.size - constraints: - - greater_or_equal: 0 MB - mem_size: - required: no - type: scalar-unit.size - constraints: - - greater_or_equal: 0 MB - -tosca.capabilities.Endpoint: - derived_from: tosca.capabilities.Root - properties: - protocol: - type: string - default: tcp - port: - type: tosca.datatypes.network.PortDef - required: false - secure: - type: boolean - default: false - url_path: - type: string - required: false - port_name: - type: string - required: false - network_name: - type: string - required: false - initiator: - type: string - default: source - constraints: - - valid_values: [source, target, peer] - ports: - type: map - required: false - constraints: - - min_length: 1 - entry_schema: - type: tosca.datatypes.network.PortDef - attributes: - ip_address: - type: string - -tosca.capabilities.Endpoint.Admin: - derived_from: tosca.capabilities.Endpoint - properties: - secure: true - -tosca.capabilities.Scalable: - derived_from: tosca.capabilities.Root - properties: - min_instances: - type: integer - required: yes - default: 1 - description: > - This property is used to indicate the minimum number of instances - that should be created for the associated TOSCA Node Template by - a TOSCA orchestrator. - max_instances: - type: integer - required: yes - default: 1 - description: > - This property is used to indicate the maximum number of instances - that should be created for the associated TOSCA Node Template by - a TOSCA orchestrator. - default_instances: - type: integer - required: no - description: > - An optional property that indicates the requested default number - of instances that should be the starting number of instances a - TOSCA orchestrator should attempt to allocate. - The value for this property MUST be in the range between the values - set for min_instances and max_instances properties. - -tosca.capabilities.Endpoint.Database: - derived_from: tosca.capabilities.Endpoint - -tosca.capabilities.Attachment: - derived_from: tosca.capabilities.Root - -tosca.capabilities.network.Linkable: - derived_from: tosca.capabilities.Root - description: > - A node type that includes the Linkable capability indicates that it can - be pointed by tosca.relationships.network.LinksTo relationship type, which - represents an association relationship between Port and Network node types. - -tosca.capabilities.network.Bindable: - derived_from: tosca.capabilities.Root - description: > - A node type that includes the Bindable capability indicates that it can - be pointed by tosca.relationships.network.BindsTo relationship type, which - represents a network association relationship between Port and Compute node - types. - -tosca.capabilities.OperatingSystem: - derived_from: tosca.capabilities.Root - properties: - architecture: - required: false - type: string - description: > - The host Operating System (OS) architecture. - type: - required: false - type: string - description: > - The host Operating System (OS) type. - distribution: - required: false - type: string - description: > - The host Operating System (OS) distribution. Examples of valid values - for an “type” of “Linux” would include: - debian, fedora, rhel and ubuntu. - version: - required: false - type: version - description: > - The host Operating System version. - -########################################################################## - # Interfaces Type. - # The Interfaces element describes a list of one or more interface - # definitions for a modelable entity (e.g., a Node or Relationship Type) - # as defined within the TOSCA Simple Profile specification. -########################################################################## -tosca.interfaces.node.lifecycle.Standard: - create: - description: Standard lifecycle create operation. - configure: - description: Standard lifecycle configure operation. - start: - description: Standard lifecycle start operation. - stop: - description: Standard lifecycle stop operation. - delete: - description: Standard lifecycle delete operation. - -tosca.interfaces.relationship.Configure: - pre_configure_source: - description: Operation to pre-configure the source endpoint. - pre_configure_target: - description: Operation to pre-configure the target endpoint. - post_configure_source: - description: Operation to post-configure the source endpoint. - post_configure_target: - description: Operation to post-configure the target endpoint. - add_target: - description: Operation to add a target node. - remove_target: - description: Operation to remove a target node. - add_source: > - description: Operation to notify the target node of a source node which - is now available via a relationship. - description: - target_changed: > - description: Operation to notify source some property or attribute of the - target changed - -########################################################################## - # Data Type. - # A Datatype is a complex data type declaration which contains other - # complex or simple data types. -########################################################################## -tosca.datatypes.network.NetworkInfo: - properties: - network_name: - type: string - network_id: - type: string - addresses: - type: list - entry_schema: - type: string - -tosca.datatypes.network.PortInfo: - properties: - port_name: - type: string - port_id: - type: string - network_id: - type: string - mac_address: - type: string - addresses: - type: list - entry_schema: - type: string - -tosca.datatypes.network.PortDef: - type: integer - constraints: - - in_range: [ 1, 65535 ] - -tosca.datatypes.network.PortSpec: - properties: - protocol: - type: string - required: true - default: tcp - constraints: - - valid_values: [ udp, tcp, igmp ] - target: - type: list - entry_schema: - type: PortDef - target_range: - type: range - constraints: - - in_range: [ 1, 65535 ] - source: - type: list - entry_schema: - type: PortDef - source_range: - type: range - constraints: - - in_range: [ 1, 65535 ] - -tosca.datatypes.Credential: - properties: - protocol: - type: string - token_type: - type: string - token: - type: string - keys: - type: map - entry_schema: - type: string - user: - type: string - required: false - -########################################################################## - # Artifact Type. - # An Artifact Type is a reusable entity that defines the type of one or more - # files which Node Types or Node Templates can have dependent relationships - # and used during operations such as during installation or deployment. -########################################################################## -tosca.artifacts.Root: - description: > - The TOSCA Artifact Type all other TOSCA Artifact Types derive from - properties: - version: version - -tosca.artifacts.File: - derived_from: tosca.artifacts.Root - -tosca.artifacts.Deployment: - derived_from: tosca.artifacts.Root - description: TOSCA base type for deployment artifacts - -tosca.artifacts.Deployment.Image: - derived_from: tosca.artifacts.Deployment - -tosca.artifacts.Deployment.Image.VM: - derived_from: tosca.artifacts.Deployment.Image - -tosca.artifacts.Implementation: - derived_from: tosca.artifacts.Root - description: TOSCA base type for implementation artifacts - -tosca.artifacts.Implementation.Bash: - derived_from: tosca.artifacts.Implementation - description: Script artifact for the Unix Bash shell - mime_type: application/x-sh - file_ext: [ sh ] - -tosca.artifacts.Implementation.Python: - derived_from: tosca.artifacts.Implementation - description: Artifact for the interpreted Python language - mime_type: application/x-python - file_ext: [ py ] - -tosca.artifacts.Deployment.Image.Container.Docker: - derived_from: tosca.artifacts.Deployment.Image - description: Docker container image - -tosca.artifacts.Deployment.Image.VM.ISO: - derived_from: tosca.artifacts.Deployment.Image - description: Virtual Machine (VM) image in ISO disk format - mime_type: application/octet-stream - file_ext: [ iso ] - -tosca.artifacts.Deployment.Image.VM.QCOW2: - derived_from: tosca.artifacts.Deployment.Image - description: Virtual Machine (VM) image in QCOW v2 standard disk format - mime_type: application/octet-stream - file_ext: [ qcow2 ] - -########################################################################## - # Policy Type. - # TOSCA Policy Types represent logical grouping of TOSCA nodes that have - # an implied relationship and need to be orchestrated or managed together - # to achieve some result. -########################################################################## -tosca.policies.Root: - description: The TOSCA Policy Type all other TOSCA Policy Types derive from. - -tosca.policies.Placement: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to govern - placement of TOSCA nodes or groups of nodes. - -tosca.policies.Scaling: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to govern - scaling of TOSCA nodes or groups of nodes. - -tosca.policies.Update: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to govern - update of TOSCA nodes or groups of nodes. - -tosca.policies.Performance: - derived_from: tosca.policies.Root - description: The TOSCA Policy Type definition that is used to declare - performance requirements for TOSCA nodes or groups of nodes. - -# Miguel: new types - -tosca.nodes.Database.MySQL: - derived_from: tosca.nodes.Database - properties: - password: - type: string - required: true - name: - type: string - required: true - user: - type: string - required: true - root_password: - type: string - required: true - requirements: - - host: - capability: tosca.capabilities.Container - relationship: tosca.relationships.HostedOn - node: tosca.nodes.DBMS.MySQL - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_configure.yml - inputs: - password: { get_property: [ SELF, password ] } - name: { get_property: [ SELF, name ] } - user: { get_property: [ SELF, user ] } - root_password: { get_property: [ SELF, root_password ] } - - -tosca.nodes.DBMS.MySQL: - derived_from: tosca.nodes.DBMS - properties: - port: - type: integer - description: reflect the default MySQL server port - default: 3306 - root_password: - type: string - # MySQL requires a root_password for configuration - required: true - capabilities: - # Further constrain the ‘host’ capability to only allow MySQL databases - host: - type: tosca.capabilities.Container - valid_source_types: [ tosca.nodes.Database.MySQL ] - interfaces: - Standard: - create: mysql/mysql_install.yml - configure: - implementation: mysql/mysql_configure.yml - inputs: - root_password: { get_property: [ SELF, root_password ] } - port: { get_property: [ SELF, port ] } - -tosca.nodes.WebServer.Apache: - derived_from: tosca.nodes.WebServer - interfaces: - Standard: - create: apache/apache_install.yml - -# INDIGO non normative types - -tosca.nodes.indigo.GalaxyPortal: - derived_from: tosca.nodes.WebServer - properties: - admin: - type: string - description: email of the admin user - default: admin@admin.com - required: false - admin_api_key: - type: string - description: key to access the API with admin role - default: not_very_secret_api_key - required: false - user: - type: string - description: username to launch the galaxy daemon - default: galaxy - required: false - install_path: - type: string - description: path to install the galaxy tool - default: /home/galaxy/galaxy - required: false - interfaces: - Standard: - create: - implementation: galaxy/galaxy_install.yml - inputs: - galaxy_install_path: { get_property: [ SELF, install_path ] } - configure: - implementation: galaxy/galaxy_configure.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - galaxy_admin: { get_property: [ SELF, admin ] } - galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } - start: - implementation: galaxy/galaxy_start.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - - -tosca.nodes.indigo.GalaxyTool: - derived_from: tosca.nodes.WebApplication - properties: - name: - type: string - description: name of the tool - required: true - owner: - type: string - description: developer of the tool - required: true - tool_panel_section_id: - type: string - description: panel section to install the tool - required: true - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.indigo.GalaxyPortal - relationship: tosca.relationships.HostedOn - interfaces: - Standard: - create: - implementation: galaxy/galaxy_tools_configure.yml - inputs: - galaxy_install_path: { get_property: [ HOST, install_path ] } - galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } - galaxy_tool_name: { get_property: [ SELF, name ] } - galaxy_tool_owner: { get_property: [ SELF, owner ] } - galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } diff --git a/IM/tosca/toscaparser/elements/__init__.py b/IM/tosca/toscaparser/elements/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/elements/artifacttype.py b/IM/tosca/toscaparser/elements/artifacttype.py deleted file mode 100644 index e0897b3d7..000000000 --- a/IM/tosca/toscaparser/elements/artifacttype.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class ArtifactTypeDef(StatefulEntityType): - '''TOSCA built-in artifacts type.''' - - def __init__(self, atype, custom_def=None): - super(ArtifactTypeDef, self).__init__(atype, self.ARTIFACT_PREFIX, - custom_def) - self.type = atype - self.properties = None - if self.PROPERTIES in self.defs: - self.properties = self.defs[self.PROPERTIES] - self.parent_artifacts = self._get_parent_artifacts() - - def _get_parent_artifacts(self): - artifacts = {} - parent_artif = self.parent_type - if parent_artif: - while parent_artif != 'tosca.artifacts.Root': - artifacts[parent_artif] = self.TOSCA_DEF[parent_artif] - parent_artif = artifacts[parent_artif]['derived_from'] - return artifacts - - @property - def parent_type(self): - '''Return an artifact this artifact is derived from.''' - return self.derived_from(self.defs) - - def get_artifact(self, name): - '''Return the definition of an artifact field by name.''' - if name in self.defs: - return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/attribute_definition.py b/IM/tosca/toscaparser/elements/attribute_definition.py deleted file mode 100644 index 35ba27f22..000000000 --- a/IM/tosca/toscaparser/elements/attribute_definition.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - - -class AttributeDef(object): - '''TOSCA built-in Attribute type.''' - - def __init__(self, name, value=None, schema=None): - self.name = name - self.value = value - self.schema = schema diff --git a/IM/tosca/toscaparser/elements/capabilitytype.py b/IM/tosca/toscaparser/elements/capabilitytype.py deleted file mode 100644 index b1bd7d767..000000000 --- a/IM/tosca/toscaparser/elements/capabilitytype.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.property_definition import PropertyDef -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class CapabilityTypeDef(StatefulEntityType): - '''TOSCA built-in capabilities type.''' - - def __init__(self, name, ctype, ntype, custom_def=None): - self.name = name - super(CapabilityTypeDef, self).__init__(ctype, self.CAPABILITY_PREFIX, - custom_def) - self.nodetype = ntype - self.properties = None - if self.PROPERTIES in self.defs: - self.properties = self.defs[self.PROPERTIES] - self.parent_capabilities = self._get_parent_capabilities() - - def get_properties_def_objects(self): - '''Return a list of property definition objects.''' - properties = [] - parent_properties = {} - if self.parent_capabilities: - for type, value in self.parent_capabilities.items(): - parent_properties[type] = value.get('properties') - if self.properties: - for prop, schema in self.properties.items(): - # Miguel: Cambios aqui - if isinstance(schema, dict): - properties.append(PropertyDef(prop, None, schema)) - if parent_properties: - for parent, props in parent_properties.items(): - for prop, schema in props.items(): - properties.append(PropertyDef(prop, None, schema)) - return properties - - def get_properties_def(self): - '''Return a dictionary of property definition name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_def_objects()} - - def get_property_def_value(self, name): - '''Return the definition of a given property name.''' - props_def = self.get_properties_def() - if props_def and name in props_def: - return props_def[name].value - - def _get_parent_capabilities(self): - capabilities = {} - parent_cap = self.parent_type - if parent_cap: - while parent_cap != 'tosca.capabilities.Root': - capabilities[parent_cap] = self.TOSCA_DEF[parent_cap] - parent_cap = capabilities[parent_cap]['derived_from'] - return capabilities - - @property - def parent_type(self): - '''Return a capability this capability is derived from.''' - return self.derived_from(self.defs) diff --git a/IM/tosca/toscaparser/elements/constraints.py b/IM/tosca/toscaparser/elements/constraints.py deleted file mode 100644 index 2f38eeffd..000000000 --- a/IM/tosca/toscaparser/elements/constraints.py +++ /dev/null @@ -1,569 +0,0 @@ -# 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. - -import collections -import datetime -import re - -from IM.tosca.toscaparser.common.exception import InvalidSchemaError -from IM.tosca.toscaparser.common.exception import ValidationError -from IM.tosca.toscaparser.elements import scalarunit -from IM.tosca.toscaparser.functions import is_function -from IM.tosca.toscaparser.utils.gettextutils import _ - - -class Schema(collections.Mapping): - - KEYS = ( - TYPE, REQUIRED, DESCRIPTION, - DEFAULT, CONSTRAINTS, ENTRYSCHEMA - ) = ( - 'type', 'required', 'description', - 'default', 'constraints', 'entry_schema' - ) - - PROPERTY_TYPES = ( - INTEGER, STRING, BOOLEAN, FLOAT, - NUMBER, TIMESTAMP, LIST, MAP, - SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME, - PORTDEF, VERSION - ) = ( - 'integer', 'string', 'boolean', 'float', - 'number', 'timestamp', 'list', 'map', - 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time', - 'PortDef', 'version' - ) - - SCALAR_UNIT_SIZE_DEFAULT = 'B' - SCALAR_UNIT_SIZE_DICT = {'B': 1, 'KB': 1000, 'KIB': 1024, 'MB': 1000000, - 'MIB': 1048576, 'GB': 1000000000, - 'GIB': 1073741824, 'TB': 1000000000000, - 'TIB': 1099511627776} - - def __init__(self, name, schema_dict): - self.name = name - if not isinstance(schema_dict, collections.Mapping): - msg = _("Schema %(pname)s must be a dict.") % dict(pname=name) - raise InvalidSchemaError(message=msg) - - try: - schema_dict['type'] - except KeyError: - msg = _("Schema %(pname)s must have type.") % dict(pname=name) - raise InvalidSchemaError(message=msg) - - self.schema = schema_dict - self._len = None - self.constraints_list = [] - - @property - def type(self): - return self.schema[self.TYPE] - - @property - def required(self): - return self.schema.get(self.REQUIRED, True) - - @property - def description(self): - return self.schema.get(self.DESCRIPTION, '') - - @property - def default(self): - return self.schema.get(self.DEFAULT) - - @property - def constraints(self): - if not self.constraints_list: - constraint_schemata = self.schema.get(self.CONSTRAINTS) - if constraint_schemata: - self.constraints_list = [Constraint(self.name, - self.type, - cschema) - for cschema in constraint_schemata] - return self.constraints_list - - @property - def entry_schema(self): - return self.schema.get(self.ENTRYSCHEMA) - - def __getitem__(self, key): - return self.schema[key] - - def __iter__(self): - for k in self.KEYS: - try: - self.schema[k] - except KeyError: - pass - else: - yield k - - def __len__(self): - if self._len is None: - self._len = len(list(iter(self))) - return self._len - - -class Constraint(object): - '''Parent class for constraints for a Property or Input.''' - - CONSTRAINTS = (EQUAL, GREATER_THAN, - GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_RANGE, - VALID_VALUES, LENGTH, MIN_LENGTH, MAX_LENGTH, PATTERN) = \ - ('equal', 'greater_than', 'greater_or_equal', 'less_than', - 'less_or_equal', 'in_range', 'valid_values', 'length', - 'min_length', 'max_length', 'pattern') - - def __new__(cls, property_name, property_type, constraint): - if cls is not Constraint: - return super(Constraint, cls).__new__(cls) - - if(not isinstance(constraint, collections.Mapping) or - len(constraint) != 1): - raise InvalidSchemaError(message=_('Invalid constraint schema.')) - - for type in constraint.keys(): - ConstraintClass = get_constraint_class(type) - if not ConstraintClass: - msg = _('Invalid constraint type "%s".') % type - raise InvalidSchemaError(message=msg) - - return ConstraintClass(property_name, property_type, constraint) - - def __init__(self, property_name, property_type, constraint): - self.property_name = property_name - self.property_type = property_type - self.constraint_value = constraint[self.constraint_key] - self.constraint_value_msg = self.constraint_value - if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: - self.constraint_value = self._get_scalarunit_constraint_value() - # check if constraint is valid for property type - if property_type not in self.valid_prop_types: - msg = _('Constraint type "%(ctype)s" is not valid ' - 'for data type "%(dtype)s".') % dict( - ctype=self.constraint_key, - dtype=property_type) - raise InvalidSchemaError(message=msg) - - def _get_scalarunit_constraint_value(self): - if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: - ScalarUnit_Class = (scalarunit. - get_scalarunit_class(self.property_type)) - if isinstance(self.constraint_value, list): - return [ScalarUnit_Class(v).get_num_from_scalar_unit() - for v in self.constraint_value] - else: - return (ScalarUnit_Class(self.constraint_value). - get_num_from_scalar_unit()) - - def _err_msg(self, value): - return _('Property %s could not be validated.') % self.property_name - - def validate(self, value): - self.value_msg = value - if self.property_type in scalarunit.ScalarUnit.SCALAR_UNIT_TYPES: - value = scalarunit.get_scalarunit_value(self.property_type, value) - if not self._is_valid(value): - err_msg = self._err_msg(value) - raise ValidationError(message=err_msg) - - -class Equal(Constraint): - """Constraint class for "equal" - - Constrains a property or parameter to a value equal to ('=') - the value declared. - """ - - constraint_key = Constraint.EQUAL - - valid_prop_types = Schema.PROPERTY_TYPES - - def _is_valid(self, value): - if value == self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s is not equal to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class GreaterThan(Constraint): - """Constraint class for "greater_than" - - Constrains a property or parameter to a value greater than ('>') - the value declared. - """ - - constraint_key = Constraint.GREATER_THAN - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(GreaterThan, self).__init__(property_name, property_type, - constraint) - if not isinstance(constraint[self.GREATER_THAN], self.valid_types): - raise InvalidSchemaError(message=_('greater_than must ' - 'be comparable.')) - - def _is_valid(self, value): - if value > self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be greater than "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class GreaterOrEqual(Constraint): - """Constraint class for "greater_or_equal" - - Constrains a property or parameter to a value greater than or equal - to ('>=') the value declared. - """ - - constraint_key = Constraint.GREATER_OR_EQUAL - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(GreaterOrEqual, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('greater_or_equal must ' - 'be comparable.')) - - def _is_valid(self, value): - if is_function(value) or value >= self.constraint_value: - return True - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be greater or equal ' - 'to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class LessThan(Constraint): - """Constraint class for "less_than" - - Constrains a property or parameter to a value less than ('<') - the value declared. - """ - - constraint_key = Constraint.LESS_THAN - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(LessThan, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('less_than must ' - 'be comparable.')) - - def _is_valid(self, value): - if value < self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be less than "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class LessOrEqual(Constraint): - """Constraint class for "less_or_equal" - - Constrains a property or parameter to a value less than or equal - to ('<=') the value declared. - """ - - constraint_key = Constraint.LESS_OR_EQUAL - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(LessOrEqual, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('less_or_equal must ' - 'be comparable.')) - - def _is_valid(self, value): - if value <= self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s must be less or ' - 'equal to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=self.value_msg, - cvalue=self.constraint_value_msg)) - - -class InRange(Constraint): - """Constraint class for "in_range" - - Constrains a property or parameter to a value in range of (inclusive) - the two values declared. - """ - - constraint_key = Constraint.IN_RANGE - - valid_types = (int, float, datetime.date, - datetime.time, datetime.datetime) - - valid_prop_types = (Schema.INTEGER, Schema.FLOAT, Schema.TIMESTAMP, - Schema.SCALAR_UNIT_SIZE, Schema.SCALAR_UNIT_FREQUENCY, - Schema.SCALAR_UNIT_TIME) - - def __init__(self, property_name, property_type, constraint): - super(InRange, self).__init__(property_name, property_type, constraint) - if(not isinstance(self.constraint_value, collections.Sequence) or - (len(constraint[self.IN_RANGE]) != 2)): - raise InvalidSchemaError(message=_('in_range must be a list.')) - - for value in self.constraint_value: - if not isinstance(value, self.valid_types): - raise InvalidSchemaError(_('in_range value must ' - 'be comparable.')) - - self.min = self.constraint_value[0] - self.max = self.constraint_value[1] - - def _is_valid(self, value): - if value < self.min: - return False - if value > self.max: - return False - - return True - - def _err_msg(self, value): - return (_('%(pname)s: %(pvalue)s is out of range ' - '(min:%(vmin)s, max:%(vmax)s).') % - dict(pname=self.property_name, - pvalue=self.value_msg, - vmin=self.constraint_value_msg[0], - vmax=self.constraint_value_msg[1])) - - -class ValidValues(Constraint): - """Constraint class for "valid_values" - - Constrains a property or parameter to a value that is in the list of - declared values. - """ - constraint_key = Constraint.VALID_VALUES - - valid_prop_types = Schema.PROPERTY_TYPES - - def __init__(self, property_name, property_type, constraint): - super(ValidValues, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, collections.Sequence): - raise InvalidSchemaError(message=_('valid_values must be a list.')) - - def _is_valid(self, value): - if isinstance(value, list): - return all(v in self.constraint_value for v in value) - return value in self.constraint_value - - def _err_msg(self, value): - allowed = '[%s]' % ', '.join(str(a) for a in self.constraint_value) - return (_('%(pname)s: %(pvalue)s is not an valid ' - 'value "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=allowed)) - - -class Length(Constraint): - """Constraint class for "length" - - Constrains the property or parameter to a value of a given length. - """ - - constraint_key = Constraint.LENGTH - - valid_types = (int, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(Length, self).__init__(property_name, property_type, constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('length must be integer.')) - - def _is_valid(self, value): - if isinstance(value, str) and len(value) == self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('length of %(pname)s: %(pvalue)s must be equal ' - 'to "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -class MinLength(Constraint): - """Constraint class for "min_length" - - Constrains the property or parameter to a value to a minimum length. - """ - - constraint_key = Constraint.MIN_LENGTH - - valid_types = (int, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(MinLength, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('min_length must be integer.')) - - def _is_valid(self, value): - if isinstance(value, str) and len(value) >= self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('length of %(pname)s: %(pvalue)s must be ' - 'at least "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -class MaxLength(Constraint): - """Constraint class for "max_length" - - Constrains the property or parameter to a value to a maximum length. - """ - - constraint_key = Constraint.MAX_LENGTH - - valid_types = (int, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(MaxLength, self).__init__(property_name, property_type, - constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('max_length must be integer.')) - - def _is_valid(self, value): - if isinstance(value, str) and len(value) <= self.constraint_value: - return True - - return False - - def _err_msg(self, value): - return (_('length of %(pname)s: %(pvalue)s must be no greater ' - 'than "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -class Pattern(Constraint): - """Constraint class for "pattern" - - Constrains the property or parameter to a value that is allowed by - the provided regular expression. - """ - - constraint_key = Constraint.PATTERN - - valid_types = (str, ) - - valid_prop_types = (Schema.STRING, ) - - def __init__(self, property_name, property_type, constraint): - super(Pattern, self).__init__(property_name, property_type, constraint) - if not isinstance(self.constraint_value, self.valid_types): - raise InvalidSchemaError(message=_('pattern must be string.')) - self.match = re.compile(self.constraint_value).match - - def _is_valid(self, value): - match = self.match(value) - return match is not None and match.end() == len(value) - - def _err_msg(self, value): - return (_('%(pname)s: "%(pvalue)s" does not match ' - 'pattern "%(cvalue)s".') % - dict(pname=self.property_name, - pvalue=value, - cvalue=self.constraint_value)) - - -constraint_mapping = { - Constraint.EQUAL: Equal, - Constraint.GREATER_THAN: GreaterThan, - Constraint.GREATER_OR_EQUAL: GreaterOrEqual, - Constraint.LESS_THAN: LessThan, - Constraint.LESS_OR_EQUAL: LessOrEqual, - Constraint.IN_RANGE: InRange, - Constraint.VALID_VALUES: ValidValues, - Constraint.LENGTH: Length, - Constraint.MIN_LENGTH: MinLength, - Constraint.MAX_LENGTH: MaxLength, - Constraint.PATTERN: Pattern - } - - -def get_constraint_class(type): - return constraint_mapping.get(type) diff --git a/IM/tosca/toscaparser/elements/datatype.py b/IM/tosca/toscaparser/elements/datatype.py deleted file mode 100644 index e66c3b79c..000000000 --- a/IM/tosca/toscaparser/elements/datatype.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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. - - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class DataType(StatefulEntityType): - '''TOSCA built-in and user defined complex data type.''' - - def __init__(self, datatypename, custom_def=None): - super(DataType, self).__init__(datatypename, self.DATATYPE_PREFIX, - custom_def) - self.custom_def = custom_def - - @property - def parent_type(self): - '''Return a datatype this datatype is derived from.''' - ptype = self.derived_from(self.defs) - if ptype: - return DataType(ptype, self.custom_def) - return None - - @property - def value_type(self): - '''Return 'type' section in the datatype schema.''' - return self.entity_value(self.defs, 'type') - - def get_all_properties_objects(self): - '''Return all properties objects defined in type and parent type.''' - props_def = self.get_properties_def_objects() - ptype = self.parent_type - while ptype: - props_def.extend(ptype.get_properties_def_objects()) - ptype = ptype.parent_type - return props_def - - def get_all_properties(self): - '''Return a dictionary of all property definition name-object pairs.''' - return {prop.name: prop - for prop in self.get_all_properties_objects()} - - def get_all_property_value(self, name): - '''Return the value of a given property name.''' - props_def = self.get_all_properties() - if props_def and name in props_def.key(): - return props_def[name].value diff --git a/IM/tosca/toscaparser/elements/entity_type.py b/IM/tosca/toscaparser/elements/entity_type.py deleted file mode 100644 index 241556093..000000000 --- a/IM/tosca/toscaparser/elements/entity_type.py +++ /dev/null @@ -1,113 +0,0 @@ -# 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. - -import logging -import os -import IM.tosca.toscaparser.utils.yamlparser - -log = logging.getLogger('tosca') - - -class EntityType(object): - '''Base class for TOSCA elements.''' - - SECTIONS = (DERIVED_FROM, PROPERTIES, ATTRIBUTES, REQUIREMENTS, - INTERFACES, CAPABILITIES, TYPE, ARTIFACTS) = \ - ('derived_from', 'properties', 'attributes', 'requirements', - 'interfaces', 'capabilities', 'type', 'artifacts') - - '''TOSCA definition file.''' - TOSCA_DEF_FILE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "TOSCA_definition_1_0.yaml") - - loader = IM.tosca.toscaparser.utils.yamlparser.load_yaml - - TOSCA_DEF = loader(TOSCA_DEF_FILE) - - RELATIONSHIP_TYPE = (DEPENDSON, HOSTEDON, CONNECTSTO, ATTACHESTO, - LINKSTO, BINDSTO) = \ - ('tosca.relationships.DependsOn', - 'tosca.relationships.HostedOn', - 'tosca.relationships.ConnectsTo', - 'tosca.relationships.AttachesTo', - 'tosca.relationships.network.LinksTo', - 'tosca.relationships.network.BindsTo') - - NODE_PREFIX = 'tosca.nodes.' - RELATIONSHIP_PREFIX = 'tosca.relationships.' - CAPABILITY_PREFIX = 'tosca.capabilities.' - INTERFACE_PREFIX = 'tosca.interfaces.' - ARTIFACT_PREFIX = 'tosca.artifacts.' - POLICY_PREFIX = 'tosca.policies.' - # currently the data types are defined only for network - # but may have changes in the future. - DATATYPE_PREFIX = 'tosca.datatypes.network.' - TOSCA = 'tosca' - - def derived_from(self, defs): - '''Return a type this type is derived from.''' - return self.entity_value(defs, 'derived_from') - - def is_derived_from(self, type_str): - '''Check if object inherits from the given type. - - Returns true if this object is derived from 'type_str'. - False otherwise. - ''' - if not self.type: - return False - elif self.type == type_str: - return True - elif self.parent_type: - return self.parent_type.is_derived_from(type_str) - else: - return False - - def entity_value(self, defs, key): - if key in defs: - return defs[key] - - def get_value(self, ndtype, defs=None, parent=None): - value = None - if defs is None: - defs = self.defs - if ndtype in defs: - value = defs[ndtype] - if parent and not value: - p = self.parent_type - while value is None: - # check parent node - if not p: - break - if p and p.type == 'tosca.nodes.Root': - break - value = p.get_value(ndtype) - p = p.parent_type - return value - - def get_definition(self, ndtype): - value = None - defs = self.defs - if ndtype in defs: - value = defs[ndtype] - p = self.parent_type - if p: - inherited = p.get_definition(ndtype) - if inherited: - inherited = dict(inherited) - if not value: - value = inherited - else: - inherited.update(value) - value.update(inherited) - return value diff --git a/IM/tosca/toscaparser/elements/interfaces.py b/IM/tosca/toscaparser/elements/interfaces.py deleted file mode 100644 index b763294f8..000000000 --- a/IM/tosca/toscaparser/elements/interfaces.py +++ /dev/null @@ -1,74 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - -SECTIONS = (LIFECYCLE, CONFIGURE, LIFECYCLE_SHORTNAME, - CONFIGURE_SHORTNAME) = \ - ('tosca.interfaces.node.lifecycle.Standard', - 'tosca.interfaces.relationship.Configure', - 'Standard', 'Configure') - -INTERFACEVALUE = (IMPLEMENTATION, INPUTS) = ('implementation', 'inputs') - - -class InterfacesDef(StatefulEntityType): - '''TOSCA built-in interfaces type.''' - - def __init__(self, node_type, interfacetype, - node_template=None, name=None, value=None): - self.ntype = node_type - self.node_template = node_template - self.type = interfacetype - self.name = name - self.value = value - self.implementation = None - self.inputs = None - self.defs = {} - if interfacetype == LIFECYCLE_SHORTNAME: - interfacetype = LIFECYCLE - if interfacetype == CONFIGURE_SHORTNAME: - interfacetype = CONFIGURE - if node_type: - self.defs = self.TOSCA_DEF[interfacetype] - if value: - if isinstance(self.value, dict): - for i, j in self.value.items(): - if i == IMPLEMENTATION: - self.implementation = j - elif i == INPUTS: - self.inputs = j - else: - what = ('Interfaces of template %s' % - self.node_template.name) - raise UnknownFieldError(what=what, field=i) - else: - self.implementation = value - - @property - def lifecycle_ops(self): - if self.defs: - if self.type == LIFECYCLE: - return self._ops() - - @property - def configure_ops(self): - if self.defs: - if self.type == CONFIGURE: - return self._ops() - - def _ops(self): - ops = [] - for name in list(self.defs.keys()): - ops.append(name) - return ops diff --git a/IM/tosca/toscaparser/elements/nodetype.py b/IM/tosca/toscaparser/elements/nodetype.py deleted file mode 100644 index f0ee53453..000000000 --- a/IM/tosca/toscaparser/elements/nodetype.py +++ /dev/null @@ -1,200 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.capabilitytype import CapabilityTypeDef -import IM.tosca.toscaparser.elements.interfaces as ifaces -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class NodeType(StatefulEntityType): - '''TOSCA built-in node type.''' - - def __init__(self, ntype, custom_def=None): - super(NodeType, self).__init__(ntype, self.NODE_PREFIX, custom_def) - self.custom_def = custom_def - - @property - def parent_type(self): - '''Return a node this node is derived from.''' - pnode = self.derived_from(self.defs) - if pnode: - return NodeType(pnode) - - @property - def relationship(self): - '''Return a dictionary of relationships to other node types. - - This method returns a dictionary of named relationships that nodes - of the current node type (self) can have to other nodes (of specific - types) in a TOSCA template. - - ''' - relationship = {} - requires = self.get_all_requirements() - if requires: - # NOTE(sdmonov): Check if requires is a dict. - # If it is a dict convert it to a list of dicts. - # This is needed because currently the code below supports only - # lists as requirements definition. The following check will - # make sure if a map (dict) was provided it will be converted to - # a list before proceeding to the parsing. - if isinstance(requires, dict): - requires = [{key: value} for key, value in requires.items()] - - keyword = None - node_type = None - for require in requires: - for key, req in require.items(): - if 'relationship' in req: - relation = req.get('relationship') - if 'type' in relation: - relation = relation.get('type') - node_type = req.get('node') - value = req - if node_type: - keyword = 'node' - else: - # If value is a dict and has a type key - # we need to lookup the node type using - # the capability type - value = req - if isinstance(value, dict): - captype = value['capability'] - value = (self. - _get_node_type_by_cap(key, captype)) - relation = self._get_relation(key, value) - keyword = key - node_type = value - rtype = RelationshipType(relation, keyword, req) - relatednode = NodeType(node_type, self.custom_def) - relationship[rtype] = relatednode - return relationship - - def _get_node_type_by_cap(self, key, cap): - '''Find the node type that has the provided capability - - This method will lookup all node types if they have the - provided capability. - ''' - - # Filter the node types - node_types = [node_type for node_type in self.TOSCA_DEF.keys() - if node_type.startswith(self.NODE_PREFIX) and - node_type != 'tosca.nodes.Root'] - - for node_type in node_types: - node_def = self.TOSCA_DEF[node_type] - if isinstance(node_def, dict) and 'capabilities' in node_def: - node_caps = node_def['capabilities'] - for value in node_caps.values(): - if isinstance(value, dict) and \ - 'type' in value and value['type'] == cap: - return node_type - - def _get_relation(self, key, ndtype): - relation = None - ntype = NodeType(ndtype) - caps = ntype.get_capabilities() - if caps and key in caps.keys(): - c = caps[key] - for r in self.RELATIONSHIP_TYPE: - rtypedef = ntype.TOSCA_DEF[r] - for properties in rtypedef.values(): - if c.type in properties: - relation = r - break - if relation: - break - else: - for properties in rtypedef.values(): - if c.parent_type in properties: - relation = r - break - return relation - - def get_capabilities_objects(self): - '''Return a list of capability objects.''' - typecapabilities = [] - caps = self.get_value(self.CAPABILITIES) - if caps is None: - caps = self.get_value(self.CAPABILITIES, None, True) - if caps: - for name, value in caps.items(): - ctype = value.get('type') - cap = CapabilityTypeDef(name, ctype, self.type, - self.custom_def) - typecapabilities.append(cap) - return typecapabilities - - def get_capabilities(self): - '''Return a dictionary of capability name-objects pairs.''' - return {cap.name: cap - for cap in self.get_capabilities_objects()} - - @property - def requirements(self): - return self.get_value(self.REQUIREMENTS) - - def get_all_requirements(self): - requires = self.requirements - parent_node = self.parent_type - if requires is None: - requires = self.get_value(self.REQUIREMENTS, None, True) - parent_node = parent_node.parent_type - if parent_node: - while parent_node.type != 'tosca.nodes.Root': - req = parent_node.get_value(self.REQUIREMENTS, None, True) - for r in req: - if r not in requires: - requires.append(r) - parent_node = parent_node.parent_type - return requires - - @property - def interfaces(self): - return self.get_value(self.INTERFACES) - - @property - def lifecycle_inputs(self): - '''Return inputs to life cycle operations if found.''' - inputs = [] - interfaces = self.interfaces - if interfaces: - for name, value in interfaces.items(): - if name == ifaces.LIFECYCLE: - for x, y in value.items(): - if x == 'inputs': - for i in y.iterkeys(): - inputs.append(i) - return inputs - - @property - def lifecycle_operations(self): - '''Return available life cycle operations if found.''' - ops = None - interfaces = self.interfaces - if interfaces: - i = InterfacesDef(self.type, ifaces.LIFECYCLE) - ops = i.lifecycle_ops - return ops - - def get_capability(self, name): - caps = self.get_capabilities() - if caps and name in caps.keys(): - return caps[name].value - - def get_capability_type(self, name): - captype = self.get_capability(name) - if captype and name in captype.keys(): - return captype[name].value diff --git a/IM/tosca/toscaparser/elements/policytype.py b/IM/tosca/toscaparser/elements/policytype.py deleted file mode 100644 index 573e04509..000000000 --- a/IM/tosca/toscaparser/elements/policytype.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class PolicyType(StatefulEntityType): - '''TOSCA built-in policies type.''' - - def __init__(self, ptype, custom_def=None): - super(PolicyType, self).__init__(ptype, self.POLICY_PREFIX, - custom_def) - self.type = ptype - self.properties = None - if self.PROPERTIES in self.defs: - self.properties = self.defs[self.PROPERTIES] - self.parent_policies = self._get_parent_policies() - - def _get_parent_policies(self): - policies = {} - parent_policy = self.parent_type - if parent_policy: - while parent_policy != 'tosca.policies.Root': - policies[parent_policy] = self.TOSCA_DEF[parent_policy] - parent_policy = policies[parent_policy]['derived_from'] - return policies - - @property - def parent_type(self): - '''Return a policy this policy is derived from.''' - return self.derived_from(self.defs) - - def get_policy(self, name): - '''Return the definition of a policy field by name.''' - if name in self.defs: - return self.defs[name] diff --git a/IM/tosca/toscaparser/elements/property_definition.py b/IM/tosca/toscaparser/elements/property_definition.py deleted file mode 100644 index c2c7f0089..000000000 --- a/IM/tosca/toscaparser/elements/property_definition.py +++ /dev/null @@ -1,46 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import InvalidSchemaError -# Miguel: add import -from IM.tosca.toscaparser.utils.gettextutils import _ - -class PropertyDef(object): - '''TOSCA built-in Property type.''' - - def __init__(self, name, value=None, schema=None): - self.name = name - self.value = value - self.schema = schema - - try: - self.schema['type'] - except KeyError: - msg = (_("Property definition of %(pname)s must have type.") % - dict(pname=self.name)) - raise InvalidSchemaError(message=msg) - - @property - def required(self): - if self.schema: - for prop_key, prop_value in self.schema.items(): - if prop_key == 'required' and prop_value: - return True - return False - - @property - def default(self): - if self.schema: - for prop_key, prop_value in self.schema.items(): - if prop_key == 'default': - return prop_value - return None diff --git a/IM/tosca/toscaparser/elements/relationshiptype.py b/IM/tosca/toscaparser/elements/relationshiptype.py deleted file mode 100644 index e45ee3d93..000000000 --- a/IM/tosca/toscaparser/elements/relationshiptype.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.elements.statefulentitytype import StatefulEntityType - - -class RelationshipType(StatefulEntityType): - '''TOSCA built-in relationship type.''' - def __init__(self, type, capability_name=None, custom_def=None): - super(RelationshipType, self).__init__(type, self.RELATIONSHIP_PREFIX, - custom_def) - self.capability_name = capability_name - self.custom_def = custom_def - - @property - def parent_type(self): - '''Return a relationship this reletionship is derived from.''' - prel = self.derived_from(self.defs) - if prel: - return RelationshipType(prel) - - @property - def valid_target_types(self): - return self.entity_value(self.defs, 'valid_target_types') diff --git a/IM/tosca/toscaparser/elements/scalarunit.py b/IM/tosca/toscaparser/elements/scalarunit.py deleted file mode 100644 index 836427085..000000000 --- a/IM/tosca/toscaparser/elements/scalarunit.py +++ /dev/null @@ -1,130 +0,0 @@ -# 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. - -import logging -import re - -from IM.tosca.toscaparser.utils.gettextutils import _ -from IM.tosca.toscaparser.utils import validateutils - -log = logging.getLogger('tosca') - - -class ScalarUnit(object): - '''Parent class for scalar-unit type.''' - - SCALAR_UNIT_TYPES = ( - SCALAR_UNIT_SIZE, SCALAR_UNIT_FREQUENCY, SCALAR_UNIT_TIME - ) = ( - 'scalar-unit.size', 'scalar-unit.frequency', 'scalar-unit.time' - ) - - def __init__(self, value): - self.value = value - - def _check_unit_in_scalar_standard_units(self, input_unit): - """Check whether the input unit is following specified standard - - If unit is not following specified standard, convert it to standard - unit after displaying a warning message. - """ - if input_unit in self.SCALAR_UNIT_DICT.keys(): - return input_unit - else: - for key in self.SCALAR_UNIT_DICT.keys(): - if key.upper() == input_unit.upper(): - log.warning(_('Given unit %(unit)s does not follow scalar ' - 'unit standards; using %(key)s instead.') % { - 'unit': input_unit, 'key': key}) - return key - msg = (_('Provided unit "%(unit)s" is not valid. The valid units' - ' are %(valid_units)s') % {'unit': input_unit, - 'valid_units': sorted(self.SCALAR_UNIT_DICT.keys())}) - raise ValueError(msg) - - def validate_scalar_unit(self): - # Miguel: Cambios aqui - if self.value is None: - return None - regex = re.compile('([0-9.]+)\s*(\w+)') - try: - result = regex.match(str(self.value)).groups() - validateutils.str_to_num(result[0]) - scalar_unit = self._check_unit_in_scalar_standard_units(result[1]) - self.value = ' '.join([result[0], scalar_unit]) - return self.value - - except Exception: - raise ValueError(_('"%s" is not a valid scalar-unit') - % self.value) - - def get_num_from_scalar_unit(self, unit=None): - #Miguel: Cambios aqui - if self.value is None: - return None - if unit: - unit = self._check_unit_in_scalar_standard_units(unit) - else: - unit = self.SCALAR_UNIT_DEFAULT - self.validate_scalar_unit() - - regex = re.compile('([0-9.]+)\s*(\w+)') - result = regex.match(str(self.value)).groups() - converted = (float(validateutils.str_to_num(result[0])) - * self.SCALAR_UNIT_DICT[result[1]] - / self.SCALAR_UNIT_DICT[unit]) - if converted - int(converted) < 0.0000000000001: - converted = int(converted) - return converted - - -class ScalarUnit_Size(ScalarUnit): - - SCALAR_UNIT_DEFAULT = 'B' - SCALAR_UNIT_DICT = {'B': 1, 'kB': 1000, 'KiB': 1024, 'MB': 1000000, - 'MiB': 1048576, 'GB': 1000000000, - 'GiB': 1073741824, 'TB': 1000000000000, - 'TiB': 1099511627776} - - -class ScalarUnit_Time(ScalarUnit): - - SCALAR_UNIT_DEFAULT = 'ms' - SCALAR_UNIT_DICT = {'d': 86400, 'h': 3600, 'm': 60, 's': 1, - 'ms': 0.001, 'us': 0.000001, 'ns': 0.000000001} - - -class ScalarUnit_Frequency(ScalarUnit): - - SCALAR_UNIT_DEFAULT = 'GHz' - SCALAR_UNIT_DICT = {'Hz': 1, 'kHz': 1000, - 'MHz': 1000000, 'GHz': 1000000000} - - -scalarunit_mapping = { - ScalarUnit.SCALAR_UNIT_FREQUENCY: ScalarUnit_Frequency, - ScalarUnit.SCALAR_UNIT_SIZE: ScalarUnit_Size, - ScalarUnit.SCALAR_UNIT_TIME: ScalarUnit_Time, - } - - -def get_scalarunit_class(type): - return scalarunit_mapping.get(type) - - -def get_scalarunit_value(type, value, unit=None): - if type in ScalarUnit.SCALAR_UNIT_TYPES: - ScalarUnit_Class = get_scalarunit_class(type) - return (ScalarUnit_Class(value). - get_num_from_scalar_unit(unit)) - else: - raise TypeError(_('"%s" is not a valid scalar-unit type') % type) diff --git a/IM/tosca/toscaparser/elements/statefulentitytype.py b/IM/tosca/toscaparser/elements/statefulentitytype.py deleted file mode 100644 index af820bd69..000000000 --- a/IM/tosca/toscaparser/elements/statefulentitytype.py +++ /dev/null @@ -1,81 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.common.exception import InvalidTypeError -from IM.tosca.toscaparser.elements.attribute_definition import AttributeDef -from IM.tosca.toscaparser.elements.entity_type import EntityType -from IM.tosca.toscaparser.elements.property_definition import PropertyDef - - -class StatefulEntityType(EntityType): - '''Class representing TOSCA states.''' - - interfaces_node_lifecycle_operations = ['create', - 'configure', 'start', - 'stop', 'delete'] - - interfaces_relationship_confiure_operations = ['post_configure_source', - 'post_configure_target', - 'add_target', - 'remove_target'] - - def __init__(self, entitytype, prefix, custom_def=None): - entire_entitytype = entitytype - if not entitytype.startswith(self.TOSCA): - entire_entitytype = prefix + entitytype - if entire_entitytype in list(self.TOSCA_DEF.keys()): - self.defs = self.TOSCA_DEF[entire_entitytype] - entitytype = entire_entitytype - elif custom_def and entitytype in list(custom_def.keys()): - self.defs = custom_def[entitytype] - else: - raise InvalidTypeError(what=entitytype) - self.type = entitytype - - def get_properties_def_objects(self): - '''Return a list of property definition objects.''' - properties = [] - props = self.get_definition(self.PROPERTIES) - if props: - for prop, schema in props.items(): - properties.append(PropertyDef(prop, None, schema)) - return properties - - def get_properties_def(self): - '''Return a dictionary of property definition name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_def_objects()} - - def get_property_def_value(self, name): - '''Return the property definition associated with a given name.''' - props_def = self.get_properties_def() - if props_def and name in props_def.keys(): - return props_def[name].value - - def get_attributes_def_objects(self): - '''Return a list of attribute definition objects.''' - attrs = self.get_value(self.ATTRIBUTES) - if attrs: - return [AttributeDef(attr, None, schema) - for attr, schema in attrs.items()] - return [] - - def get_attributes_def(self): - '''Return a dictionary of attribute definition name-object pairs.''' - return {attr.name: attr - for attr in self.get_attributes_def_objects()} - - def get_attribute_def_value(self, name): - '''Return the attribute definition associated with a given name.''' - attrs_def = self.get_attributes_def() - if attrs_def and name in attrs_def.keys(): - return attrs_def[name].value diff --git a/IM/tosca/toscaparser/entity_template.py b/IM/tosca/toscaparser/entity_template.py deleted file mode 100644 index f8f3ced25..000000000 --- a/IM/tosca/toscaparser/entity_template.py +++ /dev/null @@ -1,285 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.capabilities import Capability -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.common.exception import ValidationError -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.elements.nodetype import NodeType -from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType -from IM.tosca.toscaparser.properties import Property - - -class EntityTemplate(object): - '''Base class for TOSCA templates.''' - - SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, - INTERFACES, CAPABILITIES, TYPE, DESCRIPTION, DIRECTIVES, - ATTRIBUTES, ARTIFACTS, NODE_FILTER, COPY) = \ - ('derived_from', 'properties', 'requirements', 'interfaces', - 'capabilities', 'type', 'description', 'directives', - 'attributes', 'artifacts', 'node_filter', 'copy') - # Miguel: Add NODE_FILTER to the REQUIREMENTS_SECTION - REQUIREMENTS_SECTION = (NODE, CAPABILITY, RELATIONSHIP, OCCURRENCES, NODE_FILTER) = \ - ('node', 'capability', 'relationship', - 'occurrences','node_filter') - - def __init__(self, name, template, entity_name, custom_def=None): - self.name = name - self.entity_tpl = template - self.custom_def = custom_def - self._validate_field(self.entity_tpl) - if entity_name == 'node_type': - self.type_definition = NodeType(self.entity_tpl['type'], - custom_def) - if entity_name == 'relationship_type': - relationship = template.get('relationship') - type = None - if relationship and isinstance(relationship, dict): - type = relationship.get('type') - elif isinstance(relationship, str): - type = self.entity_tpl['relationship'] - else: - type = self.entity_tpl['type'] - self.type_definition = RelationshipType(type, - None, custom_def) - self._properties = None - self._interfaces = None - self._requirements = None - self._capabilities = None - - @property - def type(self): - return self.type_definition.type - - @property - def requirements(self): - if self._requirements is None: - self._requirements = self.type_definition.get_value( - self.REQUIREMENTS, - self.entity_tpl) or [] - return self._requirements - - def get_properties_objects(self): - '''Return properties objects for this template.''' - if self._properties is None: - self._properties = self._create_properties() - return self._properties - - def get_properties(self): - '''Return a dictionary of property name-object pairs.''' - return {prop.name: prop - for prop in self.get_properties_objects()} - - def get_property_value(self, name): - '''Return the value of a given property name.''' - props = self.get_properties() - if props and name in props.keys(): - return props[name].value - - @property - def interfaces(self): - #if self._interfaces is None: - if not self._interfaces: - self._interfaces = self._create_interfaces() - return self._interfaces - - def get_capabilities_objects(self): - '''Return capabilities objects for this template.''' - if not self._capabilities: - self._capabilities = self._create_capabilities() - return self._capabilities - - def get_capabilities(self): - '''Return a dictionary of capability name-object pairs.''' - return {cap.name: cap - for cap in self.get_capabilities_objects()} - - def is_derived_from(self, type_str): - '''Check if object inherits from the given type. - - Returns true if this object is derived from 'type_str'. - False otherwise. - ''' - if not self.type: - return False - elif self.type == type_str: - return True - elif self.parent_type: - return self.parent_type.is_derived_from(type_str) - else: - return False - - def _create_capabilities(self): - capability = [] - # Miguel: cambios aqui - caps = self.type_definition.get_value(self.CAPABILITIES, - self.entity_tpl, - self.type_definition) - if caps: - for name, props in caps.items(): - capabilities = self.type_definition.get_capabilities() - if name in capabilities.keys(): - c = capabilities[name] - if 'properties' in props: - cap = Capability(name, props['properties'], c) - else: - cap = Capability(name, [], c) - capability.append(cap) - return capability - - def _validate_properties(self, template, entitytype): - properties = entitytype.get_value(self.PROPERTIES, template) - self._common_validate_properties(entitytype, properties) - - def _validate_capabilities(self): - type_capabilities = self.type_definition.get_capabilities() - allowed_caps = \ - type_capabilities.keys() if type_capabilities else [] - capabilities = self.type_definition.get_value(self.CAPABILITIES, - self.entity_tpl) - if capabilities: - self._common_validate_field(capabilities, allowed_caps, - 'Capabilities') - self._validate_capabilities_properties(capabilities) - - def _validate_capabilities_properties(self, capabilities): - for cap, props in capabilities.items(): - capabilitydef = self.get_capability(cap).definition - self._common_validate_properties(capabilitydef, - props[self.PROPERTIES]) - - # validating capability properties values - for prop in self.get_capability(cap).get_properties_objects(): - prop.validate() - - # TODO(srinivas_tadepalli): temporary work around to validate - # default_instances until standardized in specification - if cap == "scalable" and prop.name == "default_instances": - prop_dict = props[self.PROPERTIES] - min_instances = prop_dict.get("min_instances") - max_instances = prop_dict.get("max_instances") - default_instances = prop_dict.get("default_instances") - if not (min_instances <= default_instances - <= max_instances): - err_msg = ("Properties of template %s : " - "default_instances value is not" - " between min_instances and " - "max_instances" % self.name) - raise ValidationError(message=err_msg) - - def _common_validate_properties(self, entitytype, properties): - allowed_props = [] - required_props = [] - for p in entitytype.get_properties_def_objects(): - allowed_props.append(p.name) - if p.required: - required_props.append(p.name) - if properties: - self._common_validate_field(properties, allowed_props, - 'Properties') - # make sure it's not missing any property required by a tosca type - missingprop = [] - for r in required_props: - if r not in properties.keys(): - missingprop.append(r) - if missingprop: - raise MissingRequiredFieldError( - what='Properties of template %s' % self.name, - required=missingprop) - else: - if required_props: - raise MissingRequiredFieldError( - what='Properties of template %s' % self.name, - required=missingprop) - - def _validate_field(self, template): - if not isinstance(template, dict): - raise MissingRequiredFieldError( - what='Template %s' % self.name, required=self.TYPE) - try: - relationship = template.get('relationship') - if relationship and not isinstance(relationship, str): - relationship[self.TYPE] - elif isinstance(relationship, str): - template['relationship'] - else: - template[self.TYPE] - except KeyError: - raise MissingRequiredFieldError( - what='Template %s' % self.name, required=self.TYPE) - - def _common_validate_field(self, schema, allowedlist, section): - for name in schema: - if name not in allowedlist: - raise UnknownFieldError( - what='%(section)s of template %(nodename)s' - % {'section': section, 'nodename': self.name}, - field=name) - - def _create_properties(self): - props = [] - properties = self.type_definition.get_value(self.PROPERTIES, - self.entity_tpl) or {} - for name, value in properties.items(): - props_def = self.type_definition.get_properties_def() - if props_def and name in props_def: - prop = Property(name, value, - props_def[name].schema, self.custom_def) - props.append(prop) - for p in self.type_definition.get_properties_def_objects(): - if p.default is not None and p.name not in properties.keys(): - prop = Property(p.name, p.default, p.schema, self.custom_def) - props.append(prop) - return props - - def _create_interfaces(self): - interfaces = [] - type_interfaces = None - if isinstance(self.type_definition, RelationshipType): - if isinstance(self.entity_tpl, dict): - # Miguel: cambios aqui - for key, value in self.entity_tpl.items(): - if key == 'interfaces': - type_interfaces = value - elif key != 'type': - rel = None - if isinstance(value, dict): - rel = value.get('relationship') - if rel: - if self.INTERFACES in rel: - type_interfaces = rel[self.INTERFACES] - break - else: - type_interfaces = self.type_definition.get_value(self.INTERFACES, - self.entity_tpl) - if type_interfaces: - for interface_type, value in type_interfaces.items(): - for op, op_def in value.items(): - iface = InterfacesDef(self.type_definition, - interfacetype=interface_type, - node_template=self, - name=op, - value=op_def) - interfaces.append(iface) - return interfaces - - def get_capability(self, name): - """Provide named capability - - :param name: name of capability - :return: capability object if found, None otherwise - """ - caps = self.get_capabilities() - if caps and name in caps.keys(): - return caps[name] diff --git a/IM/tosca/toscaparser/functions.py b/IM/tosca/toscaparser/functions.py deleted file mode 100644 index 5ecb905c9..000000000 --- a/IM/tosca/toscaparser/functions.py +++ /dev/null @@ -1,410 +0,0 @@ -# -# 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. - - -import abc -import six - -from IM.tosca.toscaparser.common.exception import UnknownInputError -from IM.tosca.toscaparser.utils.gettextutils import _ - - -GET_PROPERTY = 'get_property' -GET_ATTRIBUTE = 'get_attribute' -GET_INPUT = 'get_input' - -SELF = 'SELF' -HOST = 'HOST' - -HOSTED_ON = 'tosca.relationships.HostedOn' - - -@six.add_metaclass(abc.ABCMeta) -class Function(object): - """An abstract type for representing a Tosca template function.""" - - def __init__(self, tosca_tpl, context, name, args): - self.tosca_tpl = tosca_tpl - self.context = context - self.name = name - self.args = args - self.validate() - - @abc.abstractmethod - def result(self): - """Invokes the function and returns its result - - Some methods invocation may only be relevant on runtime (for example, - getting runtime properties) and therefore its the responsibility of - the orchestrator/translator to take care of such functions invocation. - - :return: Function invocation result. - """ - return {self.name: self.args} - - @abc.abstractmethod - def validate(self): - """Validates function arguments.""" - pass - - -class GetInput(Function): - """Get a property value declared within the input of the service template. - - Arguments: - - * Input name. - - Example: - - * get_input: port - """ - - def validate(self): - if len(self.args) != 1: - raise ValueError(_( - 'Expected one argument for get_input function but received: ' - '{0}.').format(self.args)) - inputs = [input.name for input in self.tosca_tpl.inputs] - if self.args[0] not in inputs: - raise UnknownInputError(input_name=self.args[0]) - - def result(self): - found_input = [input_def for input_def in self.tosca_tpl.inputs - if self.input_name == input_def.name][0] - return found_input.default - - @property - def input_name(self): - return self.args[0] - - -class GetAttribute(Function): - """Get an attribute value of an entity defined in the service template - - Node template attributes values are set in runtime and therefore its the - responsibility of the Tosca engine to implement the evaluation of - get_attribute functions. - - Arguments: - - * Node template name | HOST. - * Attribute name. - - If the HOST keyword is passed as the node template name argument the - function will search each node template along the HostedOn relationship - chain until a node which contains the attribute is found. - - Examples: - - * { get_attribute: [ server, private_address ] } - * { get_attribute: [ HOST, private_address ] } - """ - - def validate(self): - # Miguel: this is not true: - # { get_attribute: [ HOST, networks, private, addresses, 0 ] } - if len(self.args) != 2: - raise ValueError(_( - 'Illegal arguments for {0} function. Expected arguments: ' - 'node-template-name, attribute-name').format(GET_ATTRIBUTE)) - self._find_node_template_containing_attribute() - - def result(self): - return self.args - - def get_referenced_node_template(self): - """Gets the NodeTemplate instance the get_attribute function refers to. - - If HOST keyword was used as the node template argument, the node - template which contains the attribute along the HostedOn relationship - chain will be returned. - """ - return self._find_node_template_containing_attribute() - - def _find_node_template_containing_attribute(self): - if self.node_template_name == HOST: - # Currently this is the only way to tell whether the function - # is used within the outputs section of the TOSCA template. - if isinstance(self.context, list): - raise ValueError(_( - "get_attribute HOST keyword is not allowed within the " - "outputs section of the TOSCA template")) - node_tpl = self._find_host_containing_attribute() - if not node_tpl: - raise ValueError(_( - "get_attribute HOST keyword is used in '{0}' node " - "template but {1} was not found " - "in relationship chain").format(self.context.name, - HOSTED_ON)) - else: - node_tpl = self._find_node_template(self.args[0]) - if not self._attribute_exists_in_type(node_tpl.type_definition): - raise KeyError(_( - "Attribute '{0}' not found in node template: {1}.").format( - self.attribute_name, node_tpl.name)) - return node_tpl - - def _attribute_exists_in_type(self, type_definition): - attrs_def = type_definition.get_attributes_def() - found = [attrs_def[self.attribute_name]] \ - if self.attribute_name in attrs_def else [] - return len(found) == 1 - - def _find_host_containing_attribute(self, node_template_name=SELF): - node_template = self._find_node_template(node_template_name) - from IM.tosca.toscaparser.elements.entity_type import EntityType - hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] - for r in node_template.requirements: - for requirement, target_name in r.items(): - target_node = self._find_node_template(target_name) - target_type = target_node.type_definition - for capability in target_type.get_capabilities_objects(): - if capability.type in hosted_on_rel['valid_target_types']: - if self._attribute_exists_in_type(target_type): - return target_node - return self._find_host_containing_attribute( - target_name) - return None - - def _find_node_template(self, node_template_name): - name = self.context.name if node_template_name == SELF else \ - node_template_name - for node_template in self.tosca_tpl.nodetemplates: - if node_template.name == name: - return node_template - raise KeyError(_( - 'No such node template: {0}.').format(node_template_name)) - - @property - def node_template_name(self): - return self.args[0] - - @property - def attribute_name(self): - return self.args[1] - - -class GetProperty(Function): - """Get a property value of an entity defined in the same service template. - - Arguments: - - * Node template name. - * Requirement or capability name (optional). - * Property name. - - If requirement or capability name is specified, the behavior is as follows: - The req or cap name is first looked up in the specified node template's - requirements. - If found, it would search for a matching capability - of an other node template and get its property as specified in function - arguments. - Otherwise, the req or cap name would be looked up in the specified - node template's capabilities and if found, it would return the property of - the capability as specified in function arguments. - - Examples: - - * { get_property: [ mysql_server, port ] } - * { get_property: [ SELF, db_port ] } - * { get_property: [ SELF, database_endpoint, port ] } - """ - - def validate(self): - if len(self.args) < 2 or len(self.args) > 3: - raise ValueError(_( - 'Expected arguments: [node-template-name, req-or-cap ' - '(optional), property name.')) - if len(self.args) == 2: - prop = self._find_property(self.args[1]).value - if not isinstance(prop, Function): - get_function(self.tosca_tpl, self.context, prop) - elif len(self.args) == 3: - get_function(self.tosca_tpl, - self.context, - self._find_req_or_cap_property(self.args[1], - self.args[2])) - else: - raise NotImplementedError(_( - 'Nested properties are not supported.')) - - def _find_req_or_cap_property(self, req_or_cap, property_name): - node_tpl = self._find_node_template(self.args[0]) - # Find property in node template's requirements - for r in node_tpl.requirements: - for req, node_name in r.items(): - if req == req_or_cap: - node_template = self._find_node_template(node_name) - return self._get_capability_property( - node_template, - req, - property_name) - # If requirement was not found, look in node template's capabilities - return self._get_capability_property(node_tpl, - req_or_cap, - property_name) - - def _get_capability_property(self, - node_template, - capability_name, - property_name): - """Gets a node template capability property.""" - caps = node_template.get_capabilities() - if caps and capability_name in caps.keys(): - cap = caps[capability_name] - # Miguel: Cambios aqui - property = None - props = cap.get_properties() - if props and property_name in props.keys(): - property = props[property_name] - if not property: - raise KeyError(_( - "Property '{0}' not found in capability '{1}' of node" - " template '{2}' referenced from node template" - " '{3}'.").format(property_name, - capability_name, - node_template.name, - self.context.name)) - if property.value: - return property.value - else: - return property.default - msg = _("Requirement/Capability '{0}' referenced from '{1}' node " - "template not found in '{2}' node template.").format( - capability_name, - self.context.name, - node_template.name) - raise KeyError(msg) - - def _find_property(self, property_name): - node_tpl = self._find_node_template(self.args[0]) - props = node_tpl.get_properties() - found = [props[property_name]] if property_name in props else [] - if len(found) == 0: - raise KeyError(_( - "Property: '{0}' not found in node template: {1}.").format( - property_name, node_tpl.name)) - return found[0] - - def _find_node_template(self, node_template_name): - if node_template_name == SELF: - return self.context - # Miguel: cambios aqui - elif node_template_name == HOST: - return self._find_host_containing_property() - for node_template in self.tosca_tpl.nodetemplates: - if node_template.name == node_template_name: - return node_template - raise KeyError(_( - 'No such node template: {0}.').format(node_template_name)) - - # Miguel: anyado esto - def _find_host_containing_property(self, node_template_name=SELF): - node_template = self._find_node_template(node_template_name) - from IM.tosca.toscaparser.elements.entity_type import EntityType - hosted_on_rel = EntityType.TOSCA_DEF[HOSTED_ON] - for r in node_template.requirements: - for requirement, target_name in r.items(): - target_node = self._find_node_template(target_name) - target_type = target_node.type_definition - for capability in target_type.get_capabilities_objects(): - if capability.type in hosted_on_rel['valid_target_types']: - if self._property_exists_in_type(target_type): - return target_node - return self._find_host_containing_attribute( - target_name) - return None - - def _property_exists_in_type(self, type_definition): - props_def = type_definition.get_properties_def() - found = [props_def[self.args[1]]] \ - if self.args[1] in props_def else [] - return len(found) == 1 - - def result(self): - if len(self.args) == 3: - property_value = self._find_req_or_cap_property(self.args[1], - self.args[2]) - else: - property_value = self._find_property(self.args[1]).value - if isinstance(property_value, Function): - return property_value - return get_function(self.tosca_tpl, - self.context, - property_value) - - @property - def node_template_name(self): - return self.args[0] - - @property - def property_name(self): - if len(self.args) > 2: - return self.args[2] - return self.args[1] - - @property - def req_or_cap(self): - if len(self.args) > 2: - return self.args[1] - return None - - -function_mappings = { - GET_PROPERTY: GetProperty, - GET_INPUT: GetInput, - GET_ATTRIBUTE: GetAttribute -} - - -def is_function(function): - """Returns True if the provided function is a Tosca intrinsic function. - - Examples: - - * "{ get_property: { SELF, port } }" - * "{ get_input: db_name }" - * Function instance - - :param function: Function as string or a Function instance. - :return: True if function is a Tosca intrinsic function, otherwise False. - """ - if isinstance(function, dict) and len(function) == 1: - func_name = list(function.keys())[0] - return func_name in function_mappings - return isinstance(function, Function) - - -def get_function(tosca_tpl, node_template, raw_function): - """Gets a Function instance representing the provided template function. - - If the format provided raw_function format is not relevant for template - functions or if the function name doesn't exist in function mapping the - method returns the provided raw_function. - - :param tosca_tpl: The tosca template. - :param node_template: The node template the function is specified for. - :param raw_function: The raw function as dict. - :return: Template function as Function instance or the raw_function if - parsing was unsuccessful. - """ - if is_function(raw_function): - func_name = list(raw_function.keys())[0] - if func_name in function_mappings: - func = function_mappings[func_name] - func_args = list(raw_function.values())[0] - if not isinstance(func_args, list): - func_args = [func_args] - return func(tosca_tpl, node_template, func_name, func_args) - return raw_function diff --git a/IM/tosca/toscaparser/groups.py b/IM/tosca/toscaparser/groups.py deleted file mode 100644 index 40ebcf548..000000000 --- a/IM/tosca/toscaparser/groups.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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. - - -class NodeGroup(object): - - def __init__(self, name, group_templates, member_nodes): - self.name = name - self.tpl = group_templates - self.members = member_nodes - - @property - def member_names(self): - return self.tpl.get('members') - - @property - def policies(self): - return self.tpl.get('policies') diff --git a/IM/tosca/toscaparser/nodetemplate.py b/IM/tosca/toscaparser/nodetemplate.py deleted file mode 100644 index 9288571b1..000000000 --- a/IM/tosca/toscaparser/nodetemplate.py +++ /dev/null @@ -1,242 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.common.exception import InvalidPropertyValueError -from IM.tosca.toscaparser.common.exception import TypeMismatchError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.dataentity import DataEntity -from IM.tosca.toscaparser.elements.interfaces import CONFIGURE -from IM.tosca.toscaparser.elements.interfaces import CONFIGURE_SHORTNAME -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE -from IM.tosca.toscaparser.elements.interfaces import LIFECYCLE_SHORTNAME -from IM.tosca.toscaparser.elements.relationshiptype import RelationshipType -from IM.tosca.toscaparser.entity_template import EntityTemplate -from IM.tosca.toscaparser.relationship_template import RelationshipTemplate -from IM.tosca.toscaparser.utils.gettextutils import _ - -log = logging.getLogger('tosca') - - -class NodeTemplate(EntityTemplate): - '''Node template from a Tosca profile.''' - def __init__(self, name, node_templates, custom_def=None, - available_rel_tpls=None, available_rel_types=None): - super(NodeTemplate, self).__init__(name, node_templates[name], - 'node_type', - custom_def) - self.templates = node_templates - self._validate_fields(node_templates[name]) - self.custom_def = custom_def - self.related = {} - self.relationship_tpl = [] - self.available_rel_tpls = available_rel_tpls - self.available_rel_types = available_rel_types - self._relationships = {} - - @property - def relationships(self): - if not self._relationships: - requires = self.requirements - if requires: - for r in requires: - for _, value in r.items(): - explicit = self._get_explicit_relationship(r, value) - if explicit: - for key, value in explicit.items(): - self._relationships[key] = value - return self._relationships - - def _get_explicit_relationship(self, req, value): - """Handle explicit relationship - - For example, - - req: - node: DBMS - relationship: tosca.relationships.HostedOn - """ - explicit_relation = {} - node = value.get('node') if isinstance(value, dict) else value - - if node: - # TODO(spzala) implement look up once Glance meta data is available - # to find a matching TOSCA node using the TOSCA types - msg = _('Lookup by TOSCA types are not supported. ' - 'Requirement for %s can not be full-filled.') % self.name - if (node in list(self.type_definition.TOSCA_DEF.keys()) - or node in self.custom_def): - raise NotImplementedError(msg) - related_tpl = NodeTemplate(node, self.templates, self.custom_def) - relationship = value.get('relationship') \ - if isinstance(value, dict) else None - # check if it's type has relationship defined - if not relationship: - parent_reqs = self.type_definition.get_all_requirements() - for key in req.keys(): - for req_dict in parent_reqs: - if key in req_dict.keys(): - relationship = (req_dict.get(key). - get('relationship')) - break - if relationship: - found_relationship_tpl = False - # apply available relationship templates if found - # Miguel: add this if - if self.available_rel_tpls: - for tpl in self.available_rel_tpls: - if tpl.name == relationship: - rtype = RelationshipType(tpl.type, None, - self.custom_def) - explicit_relation[rtype] = related_tpl - self.relationship_tpl.append(tpl) - found_relationship_tpl = True - - # create relationship template object. - rel_prfx = self.type_definition.RELATIONSHIP_PREFIX - if not found_relationship_tpl: - if isinstance(relationship, dict): - relationship = relationship.get('type') - if self.available_rel_types and \ - relationship in self.available_rel_types.keys(): - pass - elif not relationship.startswith(rel_prfx): - relationship = rel_prfx + relationship - for rtype in self.type_definition.relationship.keys(): - if rtype.type == relationship: - explicit_relation[rtype] = related_tpl - related_tpl._add_relationship_template(req, - rtype.type) - elif self.available_rel_types: - if relationship in self.available_rel_types.keys(): - rel_type_def = self.available_rel_types.\ - get(relationship) - if 'derived_from' in rel_type_def: - super_type = \ - rel_type_def.get('derived_from') - if not super_type.startswith(rel_prfx): - super_type = rel_prfx + super_type - if rtype.type == super_type: - explicit_relation[rtype] = related_tpl - related_tpl.\ - _add_relationship_template( - req, rtype.type) - return explicit_relation - - def _add_relationship_template(self, requirement, rtype): - req = requirement.copy() - req['type'] = rtype - tpl = RelationshipTemplate(req, rtype, None) - self.relationship_tpl.append(tpl) - - def get_relationship_template(self): - return self.relationship_tpl - - def _add_next(self, nodetpl, relationship): - self.related[nodetpl] = relationship - - @property - def related_nodes(self): - if not self.related: - for relation, node in self.type_definition.relationship.items(): - for tpl in self.templates: - if tpl == node.type: - self.related[NodeTemplate(tpl)] = relation - return self.related.keys() - - def validate(self, tosca_tpl=None): - self._validate_capabilities() - self._validate_requirements() - self._validate_properties(self.entity_tpl, self.type_definition) - self._validate_interfaces() - for prop in self.get_properties_objects(): - prop.validate() - - def _validate_requirements(self): - type_requires = self.type_definition.get_all_requirements() - allowed_reqs = ["template"] - if type_requires: - for treq in type_requires: - for key, value in treq.items(): - allowed_reqs.append(key) - if isinstance(value, dict): - for key in value: - allowed_reqs.append(key) - - requires = self.type_definition.get_value(self.REQUIREMENTS, - self.entity_tpl) - if requires: - if not isinstance(requires, list): - raise TypeMismatchError( - what='Requirements of template %s' % self.name, - type='list') - for req in requires: - for r1, value in req.items(): - if isinstance(value, dict): - self._validate_requirements_keys(value) - self._validate_requirements_properties(value) - allowed_reqs.append(r1) - self._common_validate_field(req, allowed_reqs, 'Requirements') - - def _validate_requirements_properties(self, requirements): - # TODO(anyone): Only occurences property of the requirements is - # validated here. Validation of other requirement properties are being - # validated in different files. Better to keep all the requirements - # properties validation here. - for key, value in requirements.items(): - if key == 'occurrences': - self._validate_occurrences(value) - break - - def _validate_occurrences(self, occurrences): - DataEntity.validate_datatype('list', occurrences) - for value in occurrences: - DataEntity.validate_datatype('integer', value) - if len(occurrences) != 2 or not (0 <= occurrences[0] <= occurrences[1]) \ - or occurrences[1] == 0: - raise InvalidPropertyValueError(what=(occurrences)) - - def _validate_requirements_keys(self, requirement): - for key in requirement.keys(): - if key not in self.REQUIREMENTS_SECTION: - raise UnknownFieldError( - what='Requirements of template %s' % self.name, - field=key) - - def _validate_interfaces(self): - ifaces = self.type_definition.get_value(self.INTERFACES, - self.entity_tpl) - if ifaces: - for i in ifaces: - for name, value in ifaces.items(): - if name in (LIFECYCLE, LIFECYCLE_SHORTNAME): - self._common_validate_field( - value, InterfacesDef. - interfaces_node_lifecycle_operations, - 'Interfaces') - elif name in (CONFIGURE, CONFIGURE_SHORTNAME): - self._common_validate_field( - value, InterfacesDef. - interfaces_relationship_confiure_operations, - 'Interfaces') - else: - raise UnknownFieldError( - what='Interfaces of template %s' % self.name, - field=name) - - def _validate_fields(self, nodetemplate): - for name in nodetemplate.keys(): - if name not in self.SECTIONS: - raise UnknownFieldError(what='Node template %s' - % self.name, field=name) diff --git a/IM/tosca/toscaparser/parameters.py b/IM/tosca/toscaparser/parameters.py deleted file mode 100644 index a8a3f76e4..000000000 --- a/IM/tosca/toscaparser/parameters.py +++ /dev/null @@ -1,110 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.dataentity import DataEntity -from IM.tosca.toscaparser.elements.constraints import Schema -from IM.tosca.toscaparser.elements.entity_type import EntityType -from IM.tosca.toscaparser.utils.gettextutils import _ - - -log = logging.getLogger('tosca') - - -class Input(object): - - INPUTFIELD = (TYPE, DESCRIPTION, DEFAULT, CONSTRAINTS) = \ - ('type', 'description', 'default', 'constraints') - - def __init__(self, name, schema_dict): - self.name = name - self.schema = Schema(name, schema_dict) - - @property - def type(self): - return self.schema.type - - @property - def description(self): - return self.schema.description - - @property - def default(self): - return self.schema.default - - @property - def constraints(self): - return self.schema.constraints - - def validate(self, value=None): - self._validate_field() - self.validate_type(self.type) - if value: - self._validate_value(value) - - def _validate_field(self): - for name in self.schema: - if name not in self.INPUTFIELD: - raise UnknownFieldError(what='Input %s' % self.name, - field=name) - - def validate_type(self, input_type): - if input_type not in Schema.PROPERTY_TYPES: - raise ValueError(_('Invalid type %s') % type) - - def _validate_value(self, value): - tosca = EntityType.TOSCA_DEF - datatype = None - if self.type in tosca: - datatype = tosca[self.type] - elif EntityType.DATATYPE_PREFIX + self.type in tosca: - datatype = tosca[EntityType.DATATYPE_PREFIX + self.type] - - DataEntity.validate_datatype(self.type, value, None, datatype) - - -class Output(object): - - OUTPUTFIELD = (DESCRIPTION, VALUE) = ('description', 'value') - - def __init__(self, name, attrs): - self.name = name - self.attrs = attrs - - @property - def description(self): - return self.attrs[self.DESCRIPTION] - - @property - def value(self): - return self.attrs[self.VALUE] - - def validate(self): - self._validate_field() - - def _validate_field(self): - if not isinstance(self.attrs, dict): - raise MissingRequiredFieldError(what='Output %s' % self.name, - required=self.VALUE) - try: - self.value - except KeyError: - raise MissingRequiredFieldError(what='Output %s' % self.name, - required=self.VALUE) - for name in self.attrs: - if name not in self.OUTPUTFIELD: - raise UnknownFieldError(what='Output %s' % self.name, - field=name) diff --git a/IM/tosca/toscaparser/prereq/__init__.py b/IM/tosca/toscaparser/prereq/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/prereq/csar.py b/IM/tosca/toscaparser/prereq/csar.py deleted file mode 100644 index 9f17b902c..000000000 --- a/IM/tosca/toscaparser/prereq/csar.py +++ /dev/null @@ -1,122 +0,0 @@ -# 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. - -import os.path -import yaml -import zipfile - -from IM.tosca.toscaparser.common.exception import ValidationError -from IM.tosca.toscaparser.utils.gettextutils import _ - - -class CSAR(object): - - def __init__(self, csar_file): - self.csar_file = csar_file - self.is_validated = False - - def validate(self): - """Validate the provided CSAR file.""" - - self.is_validated = True - - # validate that the file exists - if not os.path.isfile(self.csar_file): - err_msg = (_('The file %s does not exist.') % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that it is a valid zip file - if not zipfile.is_zipfile(self.csar_file): - err_msg = (_('The file %s is not a valid zip file.') - % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that it contains the metadata file in the correct location - self.zfile = zipfile.ZipFile(self.csar_file, 'r') - filelist = self.zfile.namelist() - if 'TOSCA-Metadata/TOSCA.meta' not in filelist: - err_msg = (_('The file %s is not a valid CSAR as it does not ' - 'contain the required file "TOSCA.meta" in the ' - 'folder "TOSCA-Metadata".') % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that 'Entry-Definitions' property exists in TOSCA.meta - data = self.zfile.read('TOSCA-Metadata/TOSCA.meta') - invalid_yaml_err_msg = (_('The file "TOSCA-Metadata/TOSCA.meta" in %s ' - 'does not contain valid YAML content.') % - self.csar_file) - try: - meta = yaml.load(data) - if type(meta) is not dict: - raise ValidationError(message=invalid_yaml_err_msg) - self.metadata = meta - except yaml.YAMLError: - raise ValidationError(message=invalid_yaml_err_msg) - - if 'Entry-Definitions' not in self.metadata: - err_msg = (_('The CSAR file "%s" is missing the required metadata ' - '"Entry-Definitions" in "TOSCA-Metadata/TOSCA.meta".') - % self.csar_file) - raise ValidationError(message=err_msg) - - # validate that 'Entry-Definitions' metadata value points to an - # existing file in the CSAR - entry = self.metadata['Entry-Definitions'] - if entry not in filelist: - err_msg = (_('The "Entry-Definitions" file defined in the CSAR ' - '"%s" does not exist.') % self.csar_file) - raise ValidationError(message=err_msg) - - def get_metadata(self): - """Return the metadata dictionary.""" - - # validate the csar if not already validated - if not self.is_validated: - self.validate() - - # return a copy to avoid changes overwrite the original - return dict(self.metadata) if self.metadata else None - - def _get_metadata(self, key): - if not self.is_validated: - self.validate() - return self.metadata[key] if key in self.metadata else None - - def get_author(self): - return self._get_metadata('Created-By') - - def get_version(self): - return self._get_metadata('CSAR-Version') - - def get_main_template(self): - return self._get_metadata('Entry-Definitions') - - def get_description(self): - desc = self._get_metadata('Description') - if desc is not None: - return desc - - main_template = self.get_main_template() - # extract the description from the main template - data = self.zfile.read(main_template) - invalid_tosca_yaml_err_msg = ( - _('The file %(template)s in %(csar)s does not contain valid TOSCA ' - 'YAML content.') % {'template': main_template, - 'csar': self.csar_file}) - try: - tosca_yaml = yaml.load(data) - if type(tosca_yaml) is not dict: - raise ValidationError(message=invalid_tosca_yaml_err_msg) - self.metadata['Description'] = tosca_yaml['description'] - except Exception: - raise ValidationError(message=invalid_tosca_yaml_err_msg) - return self.metadata['Description'] diff --git a/IM/tosca/toscaparser/properties.py b/IM/tosca/toscaparser/properties.py deleted file mode 100644 index f35c4394a..000000000 --- a/IM/tosca/toscaparser/properties.py +++ /dev/null @@ -1,79 +0,0 @@ -# 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. - -from IM.tosca.toscaparser.dataentity import DataEntity -from IM.tosca.toscaparser.elements.constraints import Schema -from IM.tosca.toscaparser.functions import is_function - - -class Property(object): - '''TOSCA built-in Property type.''' - - PROPERTY_KEYS = ( - TYPE, REQUIRED, DESCRIPTION, DEFAULT, CONSTRAINTS - ) = ( - 'type', 'required', 'description', 'default', 'constraints' - ) - - ENTRY_SCHEMA_KEYS = ( - ENTRYTYPE, ENTRYPROPERTIES - ) = ( - 'type', 'properties' - ) - - def __init__(self, property_name, value, schema_dict, custom_def=None): - self.name = property_name - self.value = value - self.custom_def = custom_def - self.schema = Schema(property_name, schema_dict) - - @property - def type(self): - return self.schema.type - - @property - def required(self): - return self.schema.required - - @property - def description(self): - return self.schema.description - - @property - def default(self): - return self.schema.default - - @property - def constraints(self): - return self.schema.constraints - - @property - def entry_schema(self): - return self.schema.entry_schema - - def validate(self): - '''Validate if not a reference property.''' - # Miguel: Cambios aqui - if not is_function(self.value): - if self.value is not None: - if self.type == Schema.STRING: - self.value = str(self.value) - self.value = DataEntity.validate_datatype(self.type, self.value, - self.entry_schema, - self.custom_def) - self._validate_constraints() - - def _validate_constraints(self): - # Miguel: Cambios aqui - if self.value and self.constraints: - for constraint in self.constraints: - constraint.validate(self.value) diff --git a/IM/tosca/toscaparser/relationship_template.py b/IM/tosca/toscaparser/relationship_template.py deleted file mode 100644 index a213595ca..000000000 --- a/IM/tosca/toscaparser/relationship_template.py +++ /dev/null @@ -1,68 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.entity_template import EntityTemplate -from IM.tosca.toscaparser.properties import Property - -SECTIONS = (DERIVED_FROM, PROPERTIES, REQUIREMENTS, - INTERFACES, CAPABILITIES, TYPE) = \ - ('derived_from', 'properties', 'requirements', 'interfaces', - 'capabilities', 'type') - -log = logging.getLogger('tosca') - - -class RelationshipTemplate(EntityTemplate): - '''Relationship template.''' - def __init__(self, relationship_template, name, custom_def=None): - super(RelationshipTemplate, self).__init__(name, - relationship_template, - 'relationship_type', - custom_def) - self.name = name.lower() - - def get_properties_objects(self): - '''Return properties objects for this template.''' - if self._properties is None: - self._properties = self._create_relationship_properties() - return self._properties - - def _create_relationship_properties(self): - props = [] - properties = {} - relationship = self.entity_tpl.get('relationship') - if relationship: - properties = self.type_definition.get_value(self.PROPERTIES, - relationship) or {} - if not properties: - properties = self.entity_tpl.get(self.PROPERTIES) or {} - - if properties: - for name, value in properties.items(): - props_def = self.type_definition.get_properties_def() - if props_def and name in props_def: - if name in properties.keys(): - value = properties.get(name) - prop = Property(name, value, - props_def[name].schema, self.custom_def) - props.append(prop) - for p in self.type_definition.get_properties_def_objects(): - if p.default is not None and p.name not in properties.keys(): - prop = Property(p.name, p.default, p.schema, self.custom_def) - props.append(prop) - return props - - def validate(self): - self._validate_properties(self.entity_tpl, self.type_definition) diff --git a/IM/tosca/toscaparser/topology_template.py b/IM/tosca/toscaparser/topology_template.py deleted file mode 100644 index 6822189fa..000000000 --- a/IM/tosca/toscaparser/topology_template.py +++ /dev/null @@ -1,213 +0,0 @@ -# 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. - - -import logging - -from IM.tosca.toscaparser.common import exception -from IM.tosca.toscaparser import functions -from IM.tosca.toscaparser.groups import NodeGroup -from IM.tosca.toscaparser.nodetemplate import NodeTemplate -from IM.tosca.toscaparser.parameters import Input -from IM.tosca.toscaparser.parameters import Output -from IM.tosca.toscaparser.relationship_template import RelationshipTemplate -from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph - - -# Topology template key names -SECTIONS = (DESCRIPTION, INPUTS, NODE_TEMPLATES, - RELATIONSHIP_TEMPLATES, OUTPUTS, GROUPS, - SUBSTITUION_MAPPINGS) = \ - ('description', 'inputs', 'node_templates', - 'relationship_templates', 'outputs', 'groups', - 'substitution_mappings') - -log = logging.getLogger("tosca.model") - - -class TopologyTemplate(object): - - '''Load the template data.''' - def __init__(self, template, custom_defs, - rel_types=None, parsed_params=None): - self.tpl = template - self.custom_defs = custom_defs - self.rel_types = rel_types - self.parsed_params = parsed_params - self._validate_field() - self.description = self._tpl_description() - self.inputs = self._inputs() - self.relationship_templates = self._relationship_templates() - self.nodetemplates = self._nodetemplates() - self.outputs = self._outputs() - self.graph = ToscaGraph(self.nodetemplates) - self.groups = self._groups() - self._process_intrinsic_functions() - - def _inputs(self): - inputs = [] - for name, attrs in self._tpl_inputs().items(): - input = Input(name, attrs) - if self.parsed_params and name in self.parsed_params: - input.validate(self.parsed_params[name]) - inputs.append(input) - return inputs - - def _nodetemplates(self): - nodetemplates = [] - tpls = self._tpl_nodetemplates() - for name in tpls: - tpl = NodeTemplate(name, tpls, self.custom_defs, - self.relationship_templates, - self.rel_types) - tpl.validate(self) - nodetemplates.append(tpl) - return nodetemplates - - def _relationship_templates(self): - rel_templates = [] - tpls = self._tpl_relationship_templates() - for name in tpls: - tpl = RelationshipTemplate(tpls[name], name, self.custom_defs) - rel_templates.append(tpl) - return rel_templates - - def _outputs(self): - outputs = [] - for name, attrs in self._tpl_outputs().items(): - output = Output(name, attrs) - output.validate() - outputs.append(output) - return outputs - - def _substitution_mappings(self): - pass - - def _groups(self): - groups = [] - for group_name, group_tpl in self._tpl_groups().items(): - member_names = group_tpl.get('members') - if member_names and len(member_names) > 1: - group = NodeGroup(group_name, group_tpl, - self._get_group_memerbs(member_names)) - groups.append(group) - else: - raise ValueError - return groups - - def _get_group_memerbs(self, member_names): - member_nodes = [] - for member in member_names: - for node in self.nodetemplates: - if node.name == member: - member_nodes.append(node) - return member_nodes - - # topology template can act like node template - # it is exposed by substitution_mappings. - def nodetype(self): - pass - - def capabilities(self): - pass - - def requirements(self): - pass - - def _tpl_description(self): - description = self.tpl.get(DESCRIPTION) - if description: - description = description.rstrip() - return description - - def _tpl_inputs(self): - return self.tpl.get(INPUTS) or {} - - def _tpl_nodetemplates(self): - return self.tpl[NODE_TEMPLATES] - - def _tpl_relationship_templates(self): - return self.tpl.get(RELATIONSHIP_TEMPLATES) or {} - - def _tpl_outputs(self): - return self.tpl.get(OUTPUTS) or {} - - def _tpl_substitution_mappings(self): - return self.tpl.get(SUBSTITUION_MAPPINGS) or {} - - def _tpl_groups(self): - return self.tpl.get(GROUPS) or {} - - def _validate_field(self): - for name in self.tpl: - if name not in SECTIONS: - raise exception.UnknownFieldError(what='Template', field=name) - - def _process_intrinsic_functions(self): - """Process intrinsic functions - - Current implementation processes functions within node template - properties, requirements, interfaces inputs and template outputs. - """ - for node_template in self.nodetemplates: - for prop in node_template.get_properties_objects(): - prop.value = functions.get_function(self, - node_template, - prop.value) - for interface in node_template.interfaces: - if interface.inputs: - for name, value in interface.inputs.items(): - interface.inputs[name] = functions.get_function( - self, - node_template, - value) - if node_template.requirements: - for req in node_template.requirements: - rel = req - for req_name, req_item in req.items(): - if isinstance(req_item, dict): - rel = req_item.get('relationship') - break - if rel and 'properties' in rel: - for key, value in rel['properties'].items(): - rel['properties'][key] = functions.get_function( - self, - req, - value) - if node_template.get_capabilities_objects(): - for cap in node_template.get_capabilities_objects(): - if cap.get_properties_objects(): - for prop in cap.get_properties_objects(): - propvalue = functions.get_function( - self, - node_template, - prop.value) - if isinstance(propvalue, functions.GetInput): - propvalue = propvalue.result() - for p, v in cap._properties.items(): - if p == prop.name: - cap._properties[p] = propvalue - for rel, node in node_template.relationships.items(): - rel_tpls = node.relationship_tpl - if rel_tpls: - for rel_tpl in rel_tpls: - for interface in rel_tpl.interfaces: - if interface.inputs: - for name, value in interface.inputs.items(): - interface.inputs[name] = \ - functions.get_function(self, - rel_tpl, - value) - for output in self.outputs: - func = functions.get_function(self, self.outputs, output.value) - if isinstance(func, functions.GetAttribute): - output.attrs[output.VALUE] = func diff --git a/IM/tosca/toscaparser/tosca_template.py b/IM/tosca/toscaparser/tosca_template.py deleted file mode 100644 index 0c7589fec..000000000 --- a/IM/tosca/toscaparser/tosca_template.py +++ /dev/null @@ -1,190 +0,0 @@ -# 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. - - -import logging -import os - -from IM.tosca.toscaparser.common.exception import InvalidTemplateVersion -from IM.tosca.toscaparser.common.exception import MissingRequiredFieldError -from IM.tosca.toscaparser.common.exception import UnknownFieldError -from IM.tosca.toscaparser.topology_template import TopologyTemplate -from IM.tosca.toscaparser.tpl_relationship_graph import ToscaGraph -from IM.tosca.toscaparser.utils.gettextutils import _ -import IM.tosca.toscaparser.utils.urlutils -import IM.tosca.toscaparser.utils.yamlparser - - -# TOSCA template key names -SECTIONS = (DEFINITION_VERSION, DEFAULT_NAMESPACE, TEMPLATE_NAME, - TOPOLOGY_TEMPLATE, TEMPLATE_AUTHOR, TEMPLATE_VERSION, - DESCRIPTION, IMPORTS, DSL_DEFINITIONS, NODE_TYPES, - RELATIONSHIP_TYPES, RELATIONSHIP_TEMPLATES, - CAPABILITY_TYPES, ARTIFACT_TYPES, DATATYPE_DEFINITIONS) = \ - ('tosca_definitions_version', 'tosca_default_namespace', - 'template_name', 'topology_template', 'template_author', - 'template_version', 'description', 'imports', 'dsl_definitions', - 'node_types', 'relationship_types', 'relationship_templates', - 'capability_types', 'artifact_types', 'datatype_definitions') - -log = logging.getLogger("tosca.model") - -YAML_LOADER = IM.tosca.toscaparser.utils.yamlparser.load_yaml - - -class ToscaTemplate(object): - - VALID_TEMPLATE_VERSIONS = ['tosca_simple_yaml_1_0'] - - '''Load the template data.''' - def __init__(self, path, a_file=True, parsed_params=None): - self.tpl = YAML_LOADER(path, a_file) - self.path = path - self.a_file = a_file - self.parsed_params = parsed_params - self._validate_field() - self.version = self._tpl_version() - self.relationship_types = self._tpl_relationship_types() - self.description = self._tpl_description() - self.topology_template = self._topology_template() - self.inputs = self._inputs() - self.relationship_templates = self._relationship_templates() - self.nodetemplates = self._nodetemplates() - self.outputs = self._outputs() - self.graph = ToscaGraph(self.nodetemplates) - - def _topology_template(self): - return TopologyTemplate(self._tpl_topology_template(), - self._get_all_custom_defs(), - self.relationship_types, - self.parsed_params) - - def _inputs(self): - return self.topology_template.inputs - - def _nodetemplates(self): - return self.topology_template.nodetemplates - - def _relationship_templates(self): - return self.topology_template.relationship_templates - - def _outputs(self): - return self.topology_template.outputs - - def _tpl_version(self): - return self.tpl[DEFINITION_VERSION] - - def _tpl_description(self): - return self.tpl[DESCRIPTION].rstrip() - - def _tpl_imports(self): - if IMPORTS in self.tpl: - return self.tpl[IMPORTS] - - def _tpl_relationship_types(self): - return self._get_custom_types(RELATIONSHIP_TYPES) - - def _tpl_relationship_templates(self): - topology_template = self._tpl_topology_template() - if RELATIONSHIP_TEMPLATES in topology_template.keys(): - return topology_template[RELATIONSHIP_TEMPLATES] - else: - return None - - def _tpl_topology_template(self): - return self.tpl.get(TOPOLOGY_TEMPLATE) - - def _get_all_custom_defs(self): - types = [NODE_TYPES, CAPABILITY_TYPES, RELATIONSHIP_TYPES, - DATATYPE_DEFINITIONS] - custom_defs = {} - for type in types: - custom_def = self._get_custom_types(type) - if custom_def: - custom_defs.update(custom_def) - return custom_defs - - def _get_custom_types(self, type_definition): - """Handle custom types defined in imported template files - - This method loads the custom type definitions referenced in "imports" - section of the TOSCA YAML template by determining whether each import - is specified via a file reference (by relative or absolute path) or a - URL reference. It then assigns the correct value to "def_file" variable - so the YAML content of those imports can be loaded. - - Possibilities: - +----------+--------+------------------------------+ - | template | import | comment | - +----------+--------+------------------------------+ - | file | file | OK | - | file | URL | OK | - | URL | file | file must be a relative path | - | URL | URL | OK | - +----------+--------+------------------------------+ - """ - - custom_defs = {} - imports = self._tpl_imports() - if imports: - main_a_file = os.path.isfile(self.path) - for definition in imports: - def_file = definition - a_file = False - if main_a_file: - if os.path.isfile(definition): - a_file = True - else: - full_path = os.path.join( - os.path.dirname(os.path.abspath(self.path)), - definition) - if os.path.isfile(full_path): - a_file = True - def_file = full_path - else: # main_a_url - a_url = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ - validate_url(definition) - if not a_url: - if os.path.isabs(definition): - raise ImportError(_("Absolute file name cannot be " - "used for a URL-based input " - "template.")) - def_file = IM.tosca.toscaparser.utils.urlutils.UrlUtils.\ - join_url(self.path, definition) - - custom_type = YAML_LOADER(def_file, a_file) - outer_custom_types = custom_type.get(type_definition) - if outer_custom_types: - custom_defs.update(outer_custom_types) - - # Handle custom types defined in current template file - inner_custom_types = self.tpl.get(type_definition) or {} - if inner_custom_types: - custom_defs.update(inner_custom_types) - return custom_defs - - def _validate_field(self): - try: - version = self._tpl_version() - self._validate_version(version) - except KeyError: - raise MissingRequiredFieldError(what='Template', - required=DEFINITION_VERSION) - for name in self.tpl: - if name not in SECTIONS: - raise UnknownFieldError(what='Template', field=name) - - def _validate_version(self, version): - if version not in self.VALID_TEMPLATE_VERSIONS: - raise InvalidTemplateVersion( - what=version, - valid_versions=', '. join(self.VALID_TEMPLATE_VERSIONS)) diff --git a/IM/tosca/toscaparser/tpl_relationship_graph.py b/IM/tosca/toscaparser/tpl_relationship_graph.py deleted file mode 100644 index 1a5ea7b66..000000000 --- a/IM/tosca/toscaparser/tpl_relationship_graph.py +++ /dev/null @@ -1,46 +0,0 @@ -# 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. - - -class ToscaGraph(object): - '''Graph of Tosca Node Templates.''' - def __init__(self, nodetemplates): - self.nodetemplates = nodetemplates - self.vertices = {} - self._create() - - def _create_vertex(self, node): - if node not in self.vertices: - self.vertices[node.name] = node - - def _create_edge(self, node1, node2, relationship): - if node1 not in self.vertices: - self._create_vertex(node1) - self.vertices[node1.name]._add_next(node2, - relationship) - - def vertex(self, node): - if node in self.vertices: - return self.vertices[node] - - def __iter__(self): - return iter(self.vertices.values()) - - def _create(self): - for node in self.nodetemplates: - relation = node.relationships - if relation: - for rel, nodetpls in relation.items(): - for tpl in self.nodetemplates: - if tpl.name == nodetpls.name: - self._create_edge(node, tpl, rel) - self._create_vertex(node) diff --git a/IM/tosca/toscaparser/utils/__init__.py b/IM/tosca/toscaparser/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/IM/tosca/toscaparser/utils/gettextutils.py b/IM/tosca/toscaparser/utils/gettextutils.py deleted file mode 100644 index f5562e2d7..000000000 --- a/IM/tosca/toscaparser/utils/gettextutils.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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. - -import gettext -import os - -_localedir = os.environ.get('tosca-parser'.upper() + '_LOCALEDIR') -_t = gettext.translation('tosca-parser', localedir=_localedir, - fallback=True) - - -def _(msg): - return _t.gettext(msg) diff --git a/IM/tosca/toscaparser/utils/urlutils.py b/IM/tosca/toscaparser/utils/urlutils.py deleted file mode 100644 index 628314cdf..000000000 --- a/IM/tosca/toscaparser/utils/urlutils.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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. - - -from six.moves.urllib.parse import urljoin -from six.moves.urllib.parse import urlparse -from IM.tosca.toscaparser.utils.gettextutils import _ - - -class UrlUtils(object): - - @staticmethod - def validate_url(path): - """Validates whether the given path is a URL or not. - - If the given path includes a scheme (http, https, ftp, ...) and a net - location (a domain name such as www.github.com) it is validated as a - URL. - """ - parsed = urlparse(path) - return bool(parsed.scheme) and bool(parsed.netloc) - - @staticmethod - def join_url(url, relative_path): - """Builds a new URL from the given URL and the relative path. - - Example: - url: http://www.githib.com/openstack/heat - relative_path: heat-translator - - joined: http://www.githib.com/openstack/heat-translator - """ - if not UrlUtils.validate_url(url): - raise ValueError(_("Provided URL is invalid.")) - return urljoin(url, relative_path) diff --git a/IM/tosca/toscaparser/utils/validateutils.py b/IM/tosca/toscaparser/utils/validateutils.py deleted file mode 100644 index 42bfc4664..000000000 --- a/IM/tosca/toscaparser/utils/validateutils.py +++ /dev/null @@ -1,154 +0,0 @@ -# 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. - -import collections -import dateutil.parser -import logging -import numbers -import re -import six - -from IM.tosca.toscaparser.common.exception import ( - InvalidTOSCAVersionPropertyException) -from IM.tosca.toscaparser.utils.gettextutils import _ -log = logging.getLogger('tosca') - - -def str_to_num(value): - '''Convert a string representation of a number into a numeric type.''' - if isinstance(value, numbers.Number): - return value - try: - return int(value) - except ValueError: - return float(value) - - -def validate_number(value): - return str_to_num(value) - - -def validate_integer(value): - if not isinstance(value, int): - try: - value = int(value) - except Exception: - raise ValueError(_('"%s" is not an integer') % value) - return value - - -def validate_float(value): - if not isinstance(value, float): - raise ValueError(_('"%s" is not a float') % value) - return validate_number(value) - - -def validate_string(value): - if not isinstance(value, six.string_types): - raise ValueError(_('"%s" is not a string') % value) - return value - - -def validate_list(value): - if not isinstance(value, list): - raise ValueError(_('"%s" is not a list') % value) - return value - - -def validate_map(value): - if not isinstance(value, collections.Mapping): - raise ValueError(_('"%s" is not a map') % value) - return value - - -def validate_boolean(value): - if isinstance(value, bool): - return value - - if isinstance(value, str): - normalised = value.lower() - if normalised in ['true', 'false']: - return normalised == 'true' - raise ValueError(_('"%s" is not a boolean') % value) - - -def validate_timestamp(value): - return dateutil.parser.parse(value) - - -class TOSCAVersionProperty(object): - - VERSION_RE = re.compile('^(?P([0-9][0-9]*))' - '(\.(?P([0-9][0-9]*)))?' - '(\.(?P([0-9][0-9]*)))?' - '(\.(?P([0-9A-Za-z]+)))?' - '(\-(?P[0-9])*)?$') - - def __init__(self, version): - self.version = str(version) - match = self.VERSION_RE.match(self.version) - if not match: - raise InvalidTOSCAVersionPropertyException(what=(self.version)) - ver = match.groupdict() - if self.version in ['0', '0.0', '0.0.0']: - log.warning(_('Version assumed as not provided')) - self.version = None - self.minor_version = ver['minor_version'] - self.major_version = ver['major_version'] - self.fix_version = ver['fix_version'] - self.qualifier = self._validate_qualifier(ver['qualifier']) - self.build_version = self._validate_build(ver['build_version']) - self._validate_major_version(self.major_version) - - def _validate_major_version(self, value): - """Validate major version - - Checks if only major version is provided and assumes - minor version as 0. - Eg: If version = 18, then it returns version = '18.0' - """ - - if self.minor_version is None and self.build_version is None and \ - value != '0': - log.warning(_('Minor version assumed "0"')) - self.version = '.'.join([value, '0']) - return value - - def _validate_qualifier(self, value): - """Validate qualifier - - TOSCA version is invalid if a qualifier is present without the - fix version or with all of major, minor and fix version 0s. - - For example, the following versions are invalid - 18.0.abc - 0.0.0.abc - """ - if (self.fix_version is None and value) or \ - (self.minor_version == self.major_version == - self.fix_version == '0' and value): - raise InvalidTOSCAVersionPropertyException(what=(self.version)) - return value - - def _validate_build(self, value): - """Validate build version - - TOSCA version is invalid if build version is present without the - qualifier. - Eg: version = 18.0.0-1 is invalid. - """ - if not self.qualifier and value: - raise InvalidTOSCAVersionPropertyException(what=(self.version)) - return value - - def get_version(self): - return self.version diff --git a/IM/tosca/toscaparser/utils/yamlparser.py b/IM/tosca/toscaparser/utils/yamlparser.py deleted file mode 100644 index cd6c5b224..000000000 --- a/IM/tosca/toscaparser/utils/yamlparser.py +++ /dev/null @@ -1,73 +0,0 @@ -# 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. - -import codecs -from collections import OrderedDict -import yaml - -try: - # Python 3.x - import urllib.request as urllib2 -except ImportError: - # Python 2.x - import urllib2 - -if hasattr(yaml, 'CSafeLoader'): - yaml_loader = yaml.CSafeLoader -else: - yaml_loader = yaml.SafeLoader - - -def load_yaml(path, a_file=True): - # Miguel: enable to load also a TOSCA string - if path.find("\n") == -1: - f = codecs.open(path, encoding='utf-8', errors='strict') if a_file \ - else urllib2.urlopen(path) - return yaml.load(f.read(), Loader=yaml_loader) - else: - return yaml.load(path, Loader=yaml_loader) - - -def simple_parse(tmpl_str): - try: - tpl = yaml.load(tmpl_str, Loader=yaml_loader) - except yaml.YAMLError as yea: - raise ValueError(yea) - else: - if tpl is None: - tpl = {} - return tpl - - -def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict): - class OrderedLoader(Loader): - pass - - def construct_mapping(loader, node): - loader.flatten_mapping(node) - return object_pairs_hook(loader.construct_pairs(node)) - - OrderedLoader.add_constructor( - yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, - construct_mapping) - return yaml.load(stream, OrderedLoader) - - -def simple_ordered_parse(tmpl_str): - try: - tpl = ordered_load(tmpl_str) - except yaml.YAMLError as yea: - raise ValueError(yea) - else: - if tpl is None: - tpl = {} - return tpl From 934c747c33128e06b48979863033ff510aae176d Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:18:24 +0100 Subject: [PATCH 009/509] Bugfix when stopping the REST im service --- IM/REST.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- im_service.py | 9 ++++++++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index d2e86ed38..857813962 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -23,6 +23,9 @@ AUTH_LINE_SEPARATOR = '\\n' +app = bottle.Bottle() +bottle_server = None + # Declaration of new class that inherits from ServerAdapter # It's almost equal to the supported cherrypy class CherryPyServer class MySSLCherryPy(bottle.ServerAdapter): @@ -30,6 +33,7 @@ def run(self, handler): from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter from cherrypy import wsgiserver server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) + self.srv = server # If cert variable is has a valid path, SSL will be used # You can set it to None to disable SSL @@ -38,21 +42,56 @@ def run(self, handler): server.start() finally: server.stop() + + def shutdown(self): + self.srv.stop() -app = bottle.Bottle() +class StoppableWSGIRefServer(bottle.ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + from wsgiref.simple_server import make_server + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + class server_cls(server_cls): + address_family = socket.AF_INET6 + + srv = make_server(self.host, self.port, app, server_cls, handler_cls) + self.srv = srv ### THIS IS THE ONLY CHANGE TO THE ORIGINAL CLASS METHOD! + srv.serve_forever() + + def shutdown(self): ### ADD SHUTDOWN METHOD. + self.srv.shutdown() + # self.server.server_close() def run_in_thread(host, port): bottle_thr = threading.Thread(target=run, args=(host, port)) bottle_thr.start() def run(host, port): + global bottle_server if Config.REST_SSL: # Add our new MySSLCherryPy class to the supported servers # under the key 'mysslcherrypy' - bottle.server_names['mysslcherrypy'] = MySSLCherryPy - bottle.run(app, host=host, port=port, server='mysslcherrypy', quiet=True) + bottle_server = MySSLCherryPy(host=host, port=port) + bottle.run(app, host=host, port=port, server=bottle_server, quiet=True) else: - bottle.run(app, host=host, port=port, quiet=True) + bottle_server = StoppableWSGIRefServer(host=host, port=port) + bottle.run(app, server=bottle_server, quiet=True) + +def stop(): + bottle_server.shutdown() @app.route('/infrastructures/:id', method='DELETE') def RESTDestroyInfrastructure(id=None): @@ -478,4 +517,4 @@ def RESTStopVM(infid=None, vmid=None, prop=None): return False except Exception, ex: bottle.abort(400, "Error stopping VM: " + str(ex)) - return False + return False \ No newline at end of file diff --git a/im_service.py b/im_service.py index 21d965130..433e4d77b 100755 --- a/im_service.py +++ b/im_service.py @@ -216,10 +216,17 @@ def im_stop(): """ try: # Assure that the IM data are correctly saved - InfrastructureManager.logger.info('************ Stop Infrastructure Manager daemon ************') + InfrastructureManager.logger.info('Stopping Infrastructure Manager daemon...') InfrastructureManager.stop() + + if Config.ACTIVATE_REST: + # we have to stop the REST server + import IM.REST + IM.REST.stop() except: InfrastructureManager.logger.exception("Error stopping Infrastructure Manager daemon") + + InfrastructureManager.logger.info('************ Infrastructure Manager daemon stopped ************') sys.exit(0) def signal_term_handler(signal, frame): From 3e355572d102ab48b0335047763377128d879feb Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:18:24 +0100 Subject: [PATCH 010/509] Bugfix when stopping the REST im service --- IM/REST.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- im_service.py | 9 ++++++++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index d2e86ed38..857813962 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -23,6 +23,9 @@ AUTH_LINE_SEPARATOR = '\\n' +app = bottle.Bottle() +bottle_server = None + # Declaration of new class that inherits from ServerAdapter # It's almost equal to the supported cherrypy class CherryPyServer class MySSLCherryPy(bottle.ServerAdapter): @@ -30,6 +33,7 @@ def run(self, handler): from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter from cherrypy import wsgiserver server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) + self.srv = server # If cert variable is has a valid path, SSL will be used # You can set it to None to disable SSL @@ -38,21 +42,56 @@ def run(self, handler): server.start() finally: server.stop() + + def shutdown(self): + self.srv.stop() -app = bottle.Bottle() +class StoppableWSGIRefServer(bottle.ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + from wsgiref.simple_server import make_server + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + class server_cls(server_cls): + address_family = socket.AF_INET6 + + srv = make_server(self.host, self.port, app, server_cls, handler_cls) + self.srv = srv ### THIS IS THE ONLY CHANGE TO THE ORIGINAL CLASS METHOD! + srv.serve_forever() + + def shutdown(self): ### ADD SHUTDOWN METHOD. + self.srv.shutdown() + # self.server.server_close() def run_in_thread(host, port): bottle_thr = threading.Thread(target=run, args=(host, port)) bottle_thr.start() def run(host, port): + global bottle_server if Config.REST_SSL: # Add our new MySSLCherryPy class to the supported servers # under the key 'mysslcherrypy' - bottle.server_names['mysslcherrypy'] = MySSLCherryPy - bottle.run(app, host=host, port=port, server='mysslcherrypy', quiet=True) + bottle_server = MySSLCherryPy(host=host, port=port) + bottle.run(app, host=host, port=port, server=bottle_server, quiet=True) else: - bottle.run(app, host=host, port=port, quiet=True) + bottle_server = StoppableWSGIRefServer(host=host, port=port) + bottle.run(app, server=bottle_server, quiet=True) + +def stop(): + bottle_server.shutdown() @app.route('/infrastructures/:id', method='DELETE') def RESTDestroyInfrastructure(id=None): @@ -478,4 +517,4 @@ def RESTStopVM(infid=None, vmid=None, prop=None): return False except Exception, ex: bottle.abort(400, "Error stopping VM: " + str(ex)) - return False + return False \ No newline at end of file diff --git a/im_service.py b/im_service.py index 21d965130..433e4d77b 100755 --- a/im_service.py +++ b/im_service.py @@ -216,10 +216,17 @@ def im_stop(): """ try: # Assure that the IM data are correctly saved - InfrastructureManager.logger.info('************ Stop Infrastructure Manager daemon ************') + InfrastructureManager.logger.info('Stopping Infrastructure Manager daemon...') InfrastructureManager.stop() + + if Config.ACTIVATE_REST: + # we have to stop the REST server + import IM.REST + IM.REST.stop() except: InfrastructureManager.logger.exception("Error stopping Infrastructure Manager daemon") + + InfrastructureManager.logger.info('************ Infrastructure Manager daemon stopped ************') sys.exit(0) def signal_term_handler(signal, frame): From 4aab6fdafef414cd128d3cec8d62396ca6690f2d Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:19:22 +0100 Subject: [PATCH 011/509] Pass a file to the toscaparse to maintain better compatibility --- IM/tosca/Tosca.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bfd6df0ab..188a83ef0 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -2,14 +2,16 @@ import logging import yaml import copy +import tempfile -from IM.tosca.toscaparser.tosca_template import ToscaTemplate -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.functions import Function, is_function, get_function, GetAttribute +from toscaparser.tosca_template import ToscaTemplate +from toscaparser.elements.interfaces import InterfacesDef +from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize from pylint.pyreverse.diagrams import Relationship from compiler.ast import Node + class Tosca: """ Class to translate a TOSCA document to an RADL object. @@ -23,10 +25,13 @@ class Tosca: logger = logging.getLogger('InfrastructureManager') - def __init__(self, path): - self.path = path + def __init__(self, yaml_str): self.tosca = None - self.tosca = ToscaTemplate(path) + # write the contents to a file as ToscaTemplate needs + with tempfile.NamedTemporaryFile(suffix=".yaml") as f: + f.write(yaml_str) + f.flush() + self.tosca = ToscaTemplate(f.name) @staticmethod def is_tosca(yaml_string): From 6a75fdc370165aa76ccea3dc425281d09d94b69d Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:19:22 +0100 Subject: [PATCH 012/509] Pass a file to the toscaparse to maintain better compatibility --- IM/tosca/Tosca.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bfd6df0ab..188a83ef0 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -2,14 +2,16 @@ import logging import yaml import copy +import tempfile -from IM.tosca.toscaparser.tosca_template import ToscaTemplate -from IM.tosca.toscaparser.elements.interfaces import InterfacesDef -from IM.tosca.toscaparser.functions import Function, is_function, get_function, GetAttribute +from toscaparser.tosca_template import ToscaTemplate +from toscaparser.elements.interfaces import InterfacesDef +from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize from pylint.pyreverse.diagrams import Relationship from compiler.ast import Node + class Tosca: """ Class to translate a TOSCA document to an RADL object. @@ -23,10 +25,13 @@ class Tosca: logger = logging.getLogger('InfrastructureManager') - def __init__(self, path): - self.path = path + def __init__(self, yaml_str): self.tosca = None - self.tosca = ToscaTemplate(path) + # write the contents to a file as ToscaTemplate needs + with tempfile.NamedTemporaryFile(suffix=".yaml") as f: + f.write(yaml_str) + f.flush() + self.tosca = ToscaTemplate(f.name) @staticmethod def is_tosca(yaml_string): From 48315f0c1d70c463b2dc7745d265c6af869b5467 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:43:33 +0100 Subject: [PATCH 013/509] Minor typo change --- IM/InfrastructureManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index a6578e513..3171d8be2 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1202,6 +1202,6 @@ def stop(): # Acquire the lock to avoid writing data to the DATA_FILE with InfrastructureManager._lock: InfrastructureManager._exiting = True - # Stop all the Ctxt threads of the Infrastructure + # Stop all the Ctxt threads of the Infrastructures for inf in InfrastructureManager.infrastructure_list.values(): - inf.stop() \ No newline at end of file + inf.stop() From dc36b7f7d22f8b58a471c55a7db6b1b5efd78844 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 15:43:33 +0100 Subject: [PATCH 014/509] Minor typo change --- IM/InfrastructureManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index a6578e513..3171d8be2 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1202,6 +1202,6 @@ def stop(): # Acquire the lock to avoid writing data to the DATA_FILE with InfrastructureManager._lock: InfrastructureManager._exiting = True - # Stop all the Ctxt threads of the Infrastructure + # Stop all the Ctxt threads of the Infrastructures for inf in InfrastructureManager.infrastructure_list.values(): - inf.stop() \ No newline at end of file + inf.stop() From 19cbd7191f85bfc40caf18f82d6ed87d455c4a19 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 16:01:20 +0100 Subject: [PATCH 015/509] Remove incorrect imports --- IM/tosca/Tosca.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 188a83ef0..44923404c 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -8,8 +8,6 @@ from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from pylint.pyreverse.diagrams import Relationship -from compiler.ast import Node class Tosca: From b37a8ad454a931a73433cc744bb459bb3360f1be Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 28 Oct 2015 16:01:20 +0100 Subject: [PATCH 016/509] Remove incorrect imports --- IM/tosca/Tosca.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 188a83ef0..44923404c 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -8,8 +8,6 @@ from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from pylint.pyreverse.diagrams import Relationship -from compiler.ast import Node class Tosca: From 3ba1db452bc6a280a242ffbf332432d2be896115 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 29 Oct 2015 12:51:15 +0100 Subject: [PATCH 017/509] Add custom types to IM TOSCA --- IM/tosca/Tosca.py | 12 +++- IM/tosca/custom_types.yaml | 139 +++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 IM/tosca/custom_types.yaml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 44923404c..1cd0de52b 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -6,9 +6,10 @@ from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef +from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize - +from toscaparser.utils.yamlparser import load_yaml class Tosca: """ @@ -18,12 +19,17 @@ class Tosca: """ - ARTIFACTS_PATH = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/artifacts" - CUSTOM_TYPES_FILE = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/custon_types.yaml" + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" logger = logging.getLogger('InfrastructureManager') def __init__(self, yaml_str): + # Load custom data + custom_def = load_yaml(Tosca.CUSTOM_TYPES_FILE) + # and update tosca_def with the data + EntityType.TOSCA_DEF.update(custom_def) + self.tosca = None # write the contents to a file as ToscaTemplate needs with tempfile.NamedTemporaryFile(suffix=".yaml") as f: diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml new file mode 100644 index 000000000..a6d4c9b9d --- /dev/null +++ b/IM/tosca/custom_types.yaml @@ -0,0 +1,139 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +tosca.nodes.Database.MySQL: + derived_from: tosca.nodes.Database + properties: + password: + type: string + required: true + name: + type: string + required: true + user: + type: string + required: true + root_password: + type: string + required: true + requirements: + - host: + capability: tosca.capabilities.Container + relationship: tosca.relationships.HostedOn + node: tosca.nodes.DBMS.MySQL + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_configure.yml + inputs: + password: { get_property: [ SELF, password ] } + name: { get_property: [ SELF, name ] } + user: { get_property: [ SELF, user ] } + root_password: { get_property: [ SELF, root_password ] } + + +tosca.nodes.DBMS.MySQL: + derived_from: tosca.nodes.DBMS + properties: + port: + type: integer + description: reflect the default MySQL server port + default: 3306 + root_password: + type: string + # MySQL requires a root_password for configuration + required: true + capabilities: + # Further constrain the ‘host’ capability to only allow MySQL databases + host: + type: tosca.capabilities.Container + valid_source_types: [ tosca.nodes.Database.MySQL ] + interfaces: + Standard: + create: mysql/mysql_install.yml + configure: + implementation: mysql/mysql_configure.yml + inputs: + root_password: { get_property: [ SELF, root_password ] } + port: { get_property: [ SELF, port ] } + +tosca.nodes.WebServer.Apache: + derived_from: tosca.nodes.WebServer + interfaces: + Standard: + create: apache/apache_install.yml + +# INDIGO non normative types + +tosca.nodes.indigo.GalaxyPortal: + derived_from: tosca.nodes.WebServer + properties: + admin: + type: string + description: email of the admin user + default: admin@admin.com + required: false + admin_api_key: + type: string + description: key to access the API with admin role + default: not_very_secret_api_key + required: false + user: + type: string + description: username to launch the galaxy daemon + default: galaxy + required: false + install_path: + type: string + description: path to install the galaxy tool + default: /home/galaxy/galaxy + required: false + interfaces: + Standard: + create: + implementation: galaxy/galaxy_install.yml + inputs: + galaxy_install_path: { get_property: [ SELF, install_path ] } + configure: + implementation: galaxy/galaxy_configure.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + galaxy_admin: { get_property: [ SELF, admin ] } + galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } + start: + implementation: galaxy/galaxy_start.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + + +tosca.nodes.indigo.GalaxyTool: + derived_from: tosca.nodes.WebApplication + properties: + name: + type: string + description: name of the tool + required: true + owner: + type: string + description: developer of the tool + required: true + tool_panel_section_id: + type: string + description: panel section to install the tool + required: true + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.indigo.GalaxyPortal + relationship: tosca.relationships.HostedOn + interfaces: + Standard: + create: + implementation: galaxy/galaxy_tools_configure.yml + inputs: + galaxy_install_path: { get_property: [ HOST, install_path ] } + galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } + galaxy_tool_name: { get_property: [ SELF, name ] } + galaxy_tool_owner: { get_property: [ SELF, owner ] } + galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } From 13d91e4a9700300b892afc5f5586c2b577e6e0a2 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 29 Oct 2015 12:51:15 +0100 Subject: [PATCH 018/509] Add custom types to IM TOSCA --- IM/tosca/Tosca.py | 12 +++- IM/tosca/custom_types.yaml | 139 +++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 IM/tosca/custom_types.yaml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 44923404c..1cd0de52b 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -6,9 +6,10 @@ from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef +from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize - +from toscaparser.utils.yamlparser import load_yaml class Tosca: """ @@ -18,12 +19,17 @@ class Tosca: """ - ARTIFACTS_PATH = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/artifacts" - CUSTOM_TYPES_FILE = "/home/micafer/codigo/git_im/im.tosca/IM/tosca/custon_types.yaml" + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" logger = logging.getLogger('InfrastructureManager') def __init__(self, yaml_str): + # Load custom data + custom_def = load_yaml(Tosca.CUSTOM_TYPES_FILE) + # and update tosca_def with the data + EntityType.TOSCA_DEF.update(custom_def) + self.tosca = None # write the contents to a file as ToscaTemplate needs with tempfile.NamedTemporaryFile(suffix=".yaml") as f: diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml new file mode 100644 index 000000000..a6d4c9b9d --- /dev/null +++ b/IM/tosca/custom_types.yaml @@ -0,0 +1,139 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +tosca.nodes.Database.MySQL: + derived_from: tosca.nodes.Database + properties: + password: + type: string + required: true + name: + type: string + required: true + user: + type: string + required: true + root_password: + type: string + required: true + requirements: + - host: + capability: tosca.capabilities.Container + relationship: tosca.relationships.HostedOn + node: tosca.nodes.DBMS.MySQL + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_configure.yml + inputs: + password: { get_property: [ SELF, password ] } + name: { get_property: [ SELF, name ] } + user: { get_property: [ SELF, user ] } + root_password: { get_property: [ SELF, root_password ] } + + +tosca.nodes.DBMS.MySQL: + derived_from: tosca.nodes.DBMS + properties: + port: + type: integer + description: reflect the default MySQL server port + default: 3306 + root_password: + type: string + # MySQL requires a root_password for configuration + required: true + capabilities: + # Further constrain the ‘host’ capability to only allow MySQL databases + host: + type: tosca.capabilities.Container + valid_source_types: [ tosca.nodes.Database.MySQL ] + interfaces: + Standard: + create: mysql/mysql_install.yml + configure: + implementation: mysql/mysql_configure.yml + inputs: + root_password: { get_property: [ SELF, root_password ] } + port: { get_property: [ SELF, port ] } + +tosca.nodes.WebServer.Apache: + derived_from: tosca.nodes.WebServer + interfaces: + Standard: + create: apache/apache_install.yml + +# INDIGO non normative types + +tosca.nodes.indigo.GalaxyPortal: + derived_from: tosca.nodes.WebServer + properties: + admin: + type: string + description: email of the admin user + default: admin@admin.com + required: false + admin_api_key: + type: string + description: key to access the API with admin role + default: not_very_secret_api_key + required: false + user: + type: string + description: username to launch the galaxy daemon + default: galaxy + required: false + install_path: + type: string + description: path to install the galaxy tool + default: /home/galaxy/galaxy + required: false + interfaces: + Standard: + create: + implementation: galaxy/galaxy_install.yml + inputs: + galaxy_install_path: { get_property: [ SELF, install_path ] } + configure: + implementation: galaxy/galaxy_configure.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + galaxy_admin: { get_property: [ SELF, admin ] } + galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } + start: + implementation: galaxy/galaxy_start.yml + inputs: + galaxy_user: { get_property: [ SELF, user ] } + galaxy_install_path: { get_property: [ SELF, install_path ] } + + +tosca.nodes.indigo.GalaxyTool: + derived_from: tosca.nodes.WebApplication + properties: + name: + type: string + description: name of the tool + required: true + owner: + type: string + description: developer of the tool + required: true + tool_panel_section_id: + type: string + description: panel section to install the tool + required: true + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.indigo.GalaxyPortal + relationship: tosca.relationships.HostedOn + interfaces: + Standard: + create: + implementation: galaxy/galaxy_tools_configure.yml + inputs: + galaxy_install_path: { get_property: [ HOST, install_path ] } + galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } + galaxy_tool_name: { get_property: [ SELF, name ] } + galaxy_tool_owner: { get_property: [ SELF, owner ] } + galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } From ad0ec339e083a646add135eb39bc335df702bced Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 3 Nov 2015 13:13:40 +0100 Subject: [PATCH 019/509] Add CLUES types --- IM/tosca/Tosca.py | 2 ++ IM/tosca/custom_types.yaml | 62 ++++++++++++++++++++++++++++++++++++++ examples/clues_tosca.yml | 34 +++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 examples/clues_tosca.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 1cd0de52b..dbb87db56 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -93,6 +93,8 @@ def to_radl(self): else: # Select the host to host this element compute = Tosca._find_host_compute(node, self.tosca.nodetemplates) + if not compute: + Tosca.logger.warn("Node %s has not compute node to host in." % node.name) interfaces = Tosca._get_interfaces(node) interfaces.update(Tosca._get_relationships_interfaces(relationships, node)) diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml index a6d4c9b9d..4a2457328 100644 --- a/IM/tosca/custom_types.yaml +++ b/IM/tosca/custom_types.yaml @@ -87,6 +87,11 @@ tosca.nodes.indigo.GalaxyPortal: description: path to install the galaxy tool default: /home/galaxy/galaxy required: false + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.Compute + relationship: tosca.relationships.HostedOn interfaces: Standard: create: @@ -137,3 +142,60 @@ tosca.nodes.indigo.GalaxyTool: galaxy_tool_name: { get_property: [ SELF, name ] } galaxy_tool_owner: { get_property: [ SELF, owner ] } galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } + + +tosca.capabilities.LRMS: + derived_from: tosca.capabilities.Root + properties: + lrms_type: + type: string + required: true + +tosca.capabilities.LRMS.Torque: + derived_from: tosca.capabilities.LRMS + properties: + lrms_type: torque + + +tosca.nodes.indigo.LRMS: + derived_from: tosca.nodes.SoftwareComponent + +tosca.nodes.indigo.LRMS.FrontEnd: + derived_from: tosca.nodes.indigo.LRMS + capabilities: + cluster_endpoint: + type: tosca.capabilities.Endpoint + +tosca.nodes.indigo.LRMS.FrontEnd.Torque: + derived_from: tosca.nodes.indigo.LRMS.FrontEnd + capabilities: + lrms_front_end: + type: tosca.capabilities.LRMS.Torque + interfaces: + Standard: + create: lrms/torque_install.yml + configure: lrms/torque_configure.yml + start: lrms/torque_start.yml + +tosca.nodes.indigo.CLUES: + derived_from: tosca.nodes.SoftwareComponent + properties: + secret_token: + type: string + description: Token to access the web interface + default: not_very_secret_token + required: false + requirements: + - lrms_front_end: + capability: tosca.capabilities.LRMS + node: tosca.nodes.indigo.LRMS.FrontEnd + relationship: tosca.relationships.HostedOn + interfaces: + Standard: + create: clues/clues_install.yml + configure: + implementation: clues/clues_configure.yml + inputs: + clues_secret_token: { get_property: [ SELF, secret_token ] } + clues_queue_system: { get_property: [ SELF, lrms_front_end, lrms_type ] } + start: clues/clues_start.yml \ No newline at end of file diff --git a/examples/clues_tosca.yml b/examples/clues_tosca.yml new file mode 100644 index 000000000..7c902ddbb --- /dev/null +++ b/examples/clues_tosca.yml @@ -0,0 +1,34 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA CLUES test for the IM + +topology_template: + + node_templates: + + clues: + type: tosca.nodes.indigo.CLUES + requirements: + - lrms_front_end: front_end_torque + + front_end_torque: + type: tosca.nodes.indigo.LRMS.FrontEnd.Torque + requirements: + - host: front_end_server + + front_end_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + #distribution: scientific + #version: 6.6 + From cf314b7994e2dfe602a82a51521ef82489c3cf89 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 3 Nov 2015 13:13:40 +0100 Subject: [PATCH 020/509] Add CLUES types --- IM/tosca/Tosca.py | 2 ++ IM/tosca/custom_types.yaml | 62 ++++++++++++++++++++++++++++++++++++++ examples/clues_tosca.yml | 34 +++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 examples/clues_tosca.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 1cd0de52b..dbb87db56 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -93,6 +93,8 @@ def to_radl(self): else: # Select the host to host this element compute = Tosca._find_host_compute(node, self.tosca.nodetemplates) + if not compute: + Tosca.logger.warn("Node %s has not compute node to host in." % node.name) interfaces = Tosca._get_interfaces(node) interfaces.update(Tosca._get_relationships_interfaces(relationships, node)) diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml index a6d4c9b9d..4a2457328 100644 --- a/IM/tosca/custom_types.yaml +++ b/IM/tosca/custom_types.yaml @@ -87,6 +87,11 @@ tosca.nodes.indigo.GalaxyPortal: description: path to install the galaxy tool default: /home/galaxy/galaxy required: false + requirements: + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.Compute + relationship: tosca.relationships.HostedOn interfaces: Standard: create: @@ -137,3 +142,60 @@ tosca.nodes.indigo.GalaxyTool: galaxy_tool_name: { get_property: [ SELF, name ] } galaxy_tool_owner: { get_property: [ SELF, owner ] } galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } + + +tosca.capabilities.LRMS: + derived_from: tosca.capabilities.Root + properties: + lrms_type: + type: string + required: true + +tosca.capabilities.LRMS.Torque: + derived_from: tosca.capabilities.LRMS + properties: + lrms_type: torque + + +tosca.nodes.indigo.LRMS: + derived_from: tosca.nodes.SoftwareComponent + +tosca.nodes.indigo.LRMS.FrontEnd: + derived_from: tosca.nodes.indigo.LRMS + capabilities: + cluster_endpoint: + type: tosca.capabilities.Endpoint + +tosca.nodes.indigo.LRMS.FrontEnd.Torque: + derived_from: tosca.nodes.indigo.LRMS.FrontEnd + capabilities: + lrms_front_end: + type: tosca.capabilities.LRMS.Torque + interfaces: + Standard: + create: lrms/torque_install.yml + configure: lrms/torque_configure.yml + start: lrms/torque_start.yml + +tosca.nodes.indigo.CLUES: + derived_from: tosca.nodes.SoftwareComponent + properties: + secret_token: + type: string + description: Token to access the web interface + default: not_very_secret_token + required: false + requirements: + - lrms_front_end: + capability: tosca.capabilities.LRMS + node: tosca.nodes.indigo.LRMS.FrontEnd + relationship: tosca.relationships.HostedOn + interfaces: + Standard: + create: clues/clues_install.yml + configure: + implementation: clues/clues_configure.yml + inputs: + clues_secret_token: { get_property: [ SELF, secret_token ] } + clues_queue_system: { get_property: [ SELF, lrms_front_end, lrms_type ] } + start: clues/clues_start.yml \ No newline at end of file diff --git a/examples/clues_tosca.yml b/examples/clues_tosca.yml new file mode 100644 index 000000000..7c902ddbb --- /dev/null +++ b/examples/clues_tosca.yml @@ -0,0 +1,34 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA CLUES test for the IM + +topology_template: + + node_templates: + + clues: + type: tosca.nodes.indigo.CLUES + requirements: + - lrms_front_end: front_end_torque + + front_end_torque: + type: tosca.nodes.indigo.LRMS.FrontEnd.Torque + requirements: + - host: front_end_server + + front_end_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + #distribution: scientific + #version: 6.6 + From 9a90a64b6d54d442d5100c881608ea2f31ebf1f6 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 5 Nov 2015 10:26:37 +0100 Subject: [PATCH 021/509] Merge devel branch --- IM/ConfManager.py | 1 + IM/InfrastructureManager.py | 51 +++++++++++++++++++++++- IM/REST.py | 2 + IM/ServiceRequests.py | 17 +++++++- connectors/Dummy.py | 78 +++++++++++++++++++++++++++++++++++++ connectors/__init__.py | 2 +- im_service.py | 5 +++ test/TestIM.py | 29 +++++++++----- test/TestRADL.py | 7 +++- test/TestREST.py | 17 ++++++-- test/test_im_logic.py | 5 ++- 11 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 connectors/Dummy.py diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 5ede39cdc..d30bc33c0 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -585,6 +585,7 @@ def configure_master(self): success = False cont = 0 while not success and cont < Config.PLAYBOOK_RETRIES: + time.sleep(cont*5) cont += 1 try: ConfManager.logger.info("Inf ID: " + str(self.inf.id) + ": Start the contextualization process.") diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 3171d8be2..7ac5aed03 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -790,6 +790,55 @@ def GetInfrastructureContMsg(inf_id, auth): InfrastructureManager.logger.debug(res) return res + @staticmethod + def GetInfrastructureState(inf_id, auth): + """ + Get the aggregated state of an infrastructure. + + Args: + + - inf_id(str): infrastructure id. + - auth(Authentication): parsed authentication tokens. + + Return: a str with the state + """ + + InfrastructureManager.logger.info("Getting state of the inf: " + str(inf_id)) + + sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) + + state = None + for vm in sel_inf.get_vm_list(): + if vm.state == VirtualMachine.FAILED: + state = VirtualMachine.FAILED + break + if vm.state == VirtualMachine.UNKNOWN: + state = VirtualMachine.UNKNOWN + break + elif vm.state == VirtualMachine.PENDING: + state = VirtualMachine.PENDING + elif vm.state == VirtualMachine.RUNNING: + if state != VirtualMachine.PENDING: + state = VirtualMachine.RUNNING + elif vm.state == VirtualMachine.STOPPED: + if state is None: + state = VirtualMachine.STOPPED + elif vm.state == VirtualMachine.OFF: + if state is None: + state = VirtualMachine.OFF + elif vm.state == VirtualMachine.CONFIGURED: + if state is None: + state = VirtualMachine.CONFIGURED + elif vm.state == VirtualMachine.UNCONFIGURED: + if state is None: + state = VirtualMachine.UNCONFIGURED + + if state is None: + state = VirtualMachine.UNKNOWN + + InfrastructureManager.logger.debug("inf: " + str(inf_id) + " is in state: " + state) + return state + @staticmethod def StopInfrastructure(inf_id, auth): """ @@ -1204,4 +1253,4 @@ def stop(): InfrastructureManager._exiting = True # Stop all the Ctxt threads of the Infrastructures for inf in InfrastructureManager.infrastructure_list.values(): - inf.stop() + inf.stop() \ No newline at end of file diff --git a/IM/REST.py b/IM/REST.py index 857813962..ca4e9128a 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -158,6 +158,8 @@ def RESTGetInfrastructureProperty(id=None, prop=None): res = InfrastructureManager.GetInfrastructureContMsg(id, auth) elif prop == "radl": res = InfrastructureManager.GetInfrastructureRADL(id, auth) + elif prop == "state": + res = InfrastructureManager.GetInfrastructureState(id, auth) else: bottle.abort(403, "Incorrect infrastructure property") bottle.response.content_type = "text/plain" diff --git a/IM/ServiceRequests.py b/IM/ServiceRequests.py index 0cb38f200..c399bc26a 100644 --- a/IM/ServiceRequests.py +++ b/IM/ServiceRequests.py @@ -37,6 +37,7 @@ class IMBaseRequest(AsyncRequest): GET_INFRASTRUCTURE_INFO = "GetInfrastructureInfo" GET_INFRASTRUCTURE_LIST = "GetInfrastructureList" GET_INFRASTRUCTURE_RADL = "GetInfrastructureRADL" + GET_INFRASTRUCTURE_STATE = "GetInfrastructureState" GET_VM_CONT_MSG = "GetVMContMsg" GET_VM_INFO = "GetVMInfo" GET_VM_PROPERTY = "GetVMProperty" @@ -91,6 +92,9 @@ def create_request(function, arguments = (), priority = Request.PRIORITY_NORMAL) return Request_StartVM(arguments) elif function == IMBaseRequest.STOP_VM: return Request_StopVM(arguments) + elif function == IMBaseRequest.GET_INFRASTRUCTURE_STATE: + return Request_GetInfrastructureState(arguments) + else: raise NotImplementedError("Function not Implemented") @@ -296,4 +300,15 @@ def _call_function(self): self._error_mesage = "Error stopping VM" (inf_id, vm_id, auth_data) = self.arguments InfrastructureManager.InfrastructureManager.StopVM(inf_id, vm_id, Authentication(auth_data)) - return "" \ No newline at end of file + return "" + +class Request_GetInfrastructureState(IMBaseRequest): + """ + Request class for the GetInfrastructureState function + """ + def _call_function(self): + self._error_mesage = "Error gettinf the Inf. state" + (inf_id, auth_data) = self.arguments + return InfrastructureManager.InfrastructureManager.GetInfrastructureState(inf_id, Authentication(auth_data)) + + diff --git a/connectors/Dummy.py b/connectors/Dummy.py new file mode 100644 index 000000000..d0b33daea --- /dev/null +++ b/connectors/Dummy.py @@ -0,0 +1,78 @@ +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +from IM.VirtualMachine import VirtualMachine +from CloudConnector import CloudConnector +from IM.radl.radl import Feature + + +class DummyCloudConnector(CloudConnector): + """ + Cloud Launcher to test the IM. + The connector does nothing. + """ + + type = "Dummy" + """str with the name of the provider.""" + + def concreteSystem(self, radl_system, auth_data): + res_system = radl_system.clone() + return [res_system] + + def updateVMInfo(self, vm, auth_data): + return (True, vm) + + def launch(self, inf, radl, requested_radl, num_vm, auth_data): + res = [] + for _ in range(num_vm): + now = str(int(time.time()*100)) + vm = VirtualMachine(inf, now, self.cloud, requested_radl, requested_radl) + + vm.info.systems[0].setValue('provider.type', self.type) + vm.state = VirtualMachine.RUNNING + + vm.info.systems[0].setValue("net_interface.0.ip","10.0.0.1") + vm.info.systems[0].setValue("disk.0.os.credentials.username", "username") + vm.info.systems[0].setValue("disk.0.os.credentials.password", "password") + + res.append((True, vm)) + + return res + + def finalize(self, vm, auth_data): + return (True, "") + + def stop(self, vm, auth_data): + vm.state = VirtualMachine.STOPPED + return (True, "") + + def start(self, vm, auth_data): + vm.state = VirtualMachine.RUNNING + return (True, "") + + def alterVM(self, vm, radl, auth_data): + if not radl.systems: + return (True, "") + system = radl.systems[0] + + new_cpu = system.getValue('cpu.count') + new_memory = system.getFeature('memory.size').getValue('M') + + vm.info.systems[0].setValue('cpu.count', new_cpu) + vm.info.systems[0].addFeature(Feature("memory.size", "=", new_memory, 'M'), conflict="other", missing="other") + + return (True, "") diff --git a/connectors/__init__.py b/connectors/__init__.py index 801dc15d4..2a2615e63 100644 --- a/connectors/__init__.py +++ b/connectors/__init__.py @@ -15,4 +15,4 @@ # along with this program. If not, see . -__all__ = ['CloudConnector','EC2','OCCI','OpenNebula','OpenStack','LibVirt','LibCloud','Docker','GCE','FogBow', 'Azure', 'DeployedNode','Kubernetes'] +__all__ = ['CloudConnector','EC2','OCCI','OpenNebula','OpenStack','LibVirt','LibCloud','Docker','GCE','FogBow', 'Azure', 'DeployedNode','Kubernetes','Dummy'] diff --git a/im_service.py b/im_service.py index 433e4d77b..3511ffb3c 100755 --- a/im_service.py +++ b/im_service.py @@ -123,6 +123,10 @@ def StartVM(inf_id, vm_id, auth_data): request = IMBaseRequest.create_request(IMBaseRequest.START_VM,(inf_id, vm_id, auth_data)) return WaitRequest(request) +def GetInfrastructureState(inf_id, auth_data): + request = IMBaseRequest.create_request(IMBaseRequest.GET_INFRASTRUCTURE_STATE,(inf_id, auth_data)) + return WaitRequest(request) + def launch_daemon(): """ Launch the IM daemon @@ -161,6 +165,7 @@ def launch_daemon(): server.register_function(GetVMContMsg) server.register_function(StartVM) server.register_function(StopVM) + server.register_function(GetInfrastructureState) InfrastructureManager.logger.info('************ Start Infrastructure Manager daemon (v.%s) ************' % version) diff --git a/test/TestIM.py b/test/TestIM.py index 1a3d051d2..f39fef00a 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -19,6 +19,11 @@ import unittest import xmlrpclib import time +import sys +import os + +sys.path.append("..") +sys.path.append(".") from IM.auth import Authentication from IM.VirtualMachine import VirtualMachine @@ -27,7 +32,7 @@ RADL_ADD_WIN = "network publica\nnetwork privada\nsystem windows\ndeploy windows 1 one" RADL_ADD = "network publica\nnetwork privada\nsystem wn\ndeploy wn 1 one" RADL_ADD_ERROR = "system wnno deploy wnno 1" -TESTS_PATH = '/home/micafer/codigo/git_im/im/test' +TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) RADL_FILE = TESTS_PATH + '/test.radl' #RADL_FILE = TESTS_PATH + '/test_ec2.radl' AUTH_FILE = TESTS_PATH + '/auth.dat' @@ -199,7 +204,15 @@ def test_19_addresource(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_20_addresource_noconfig(self): + def test_20_getstate(self): + """ + Test the GetInfrastructureState IM function + """ + (success, state) = self.server.GetInfrastructureState(self.inf_id, self.auth_data) + self.assertTrue(success, msg="ERROR calling GetInfrastructureState: " + str(state)) + self.assertEqual(state, "configured", msg="Unexpected inf state: " + state + ". It must be 'configured'.") + + def test_21_addresource_noconfig(self): """ Test AddResource function with the contex option to False """ @@ -210,7 +223,7 @@ def test_20_addresource_noconfig(self): self.assertTrue(success, msg="ERROR calling GetInfrastructureInfo:" + str(vm_ids)) self.assertEqual(len(vm_ids), 5, msg="ERROR getting infrastructure info: Incorrect number of VMs(" + str(len(vm_ids)) + "). It must be 3") - def test_21_removeresource(self): + def test_22_removeresource(self): """ Test RemoveResource function """ @@ -231,7 +244,7 @@ def test_21_removeresource(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_22_removeresource_noconfig(self): + def test_23_removeresource_noconfig(self): """ Test RemoveResource function with the context option to False """ @@ -249,7 +262,7 @@ def test_22_removeresource_noconfig(self): self.assertTrue(success, msg="ERROR getting VM state:" + str(res)) self.assertEqual(vm_state, VirtualMachine.CONFIGURED, msg="ERROR unexpected state. Expected 'running' and obtained " + vm_state) - def test_23_reconfigure(self): + def test_24_reconfigure(self): """ Test Reconfigure function """ @@ -259,7 +272,7 @@ def test_23_reconfigure(self): all_stopped = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_stopped, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_24_reconfigure_vmlist(self): + def test_25_reconfigure_vmlist(self): """ Test Reconfigure function specifying a list of VMs """ @@ -269,7 +282,7 @@ def test_24_reconfigure_vmlist(self): all_stopped = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_stopped, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_25_reconfigure_radl(self): + def test_26_reconfigure_radl(self): """ Test Reconfigure function specifying a new RADL """ @@ -344,8 +357,6 @@ def test_40_export_import(self): (success, res) = self.server.ImportInfrastructure(res, self.auth_data) self.assertTrue(success, msg="ERROR calling ImportInfrastructure: " + str(res)) - self.assertEqual(res, self.inf_id+1, msg="ERROR importing the inf.") - def test_50_destroy(self): """ Test DestroyInfrastructure function diff --git a/test/TestRADL.py b/test/TestRADL.py index fe83f69d5..9546c1b8e 100755 --- a/test/TestRADL.py +++ b/test/TestRADL.py @@ -16,8 +16,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -TESTS_PATH = '/home/micafer/codigo/git_im/im/test' +import sys +import os +TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) + +sys.path.append("..") +sys.path.append(".") from IM.radl.radl_parse import parse_radl from IM.radl.radl import RADL, Features, Feature, RADLParseException, system diff --git a/test/TestREST.py b/test/TestREST.py index de628198d..66d8aeea8 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -20,6 +20,10 @@ import os import httplib import time +import sys + +sys.path.append("..") +sys.path.append(".") from IM.VirtualMachine import VirtualMachine from IM.uriparse import uriparse @@ -28,11 +32,11 @@ PID = None RADL_ADD = "network publica\nsystem front\ndeploy front 1" RADL_ADD_ERROR = "system wnno deploy wnno 1" -TESTS_PATH = '/home/micafer/codigo/git_im/im/test' +TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) RADL_FILE = TESTS_PATH + '/test_simple.radl' AUTH_FILE = TESTS_PATH + '/auth.dat' -HOSTNAME = "jonsu.i3m.upv.es" +HOSTNAME = "localhost" TEST_PORT = 8800 class TestIM(unittest.TestCase): @@ -72,7 +76,7 @@ def wait_inf_state(self, state, timeout, incorrect_states = [], vm_ids = None): vm_ids = output.split("\n") else: - pass + pass err_states = [VirtualMachine.FAILED, VirtualMachine.OFF, VirtualMachine.UNCONFIGURED] err_states.extend(incorrect_states) @@ -213,6 +217,13 @@ def test_45_addresource_noconfig(self): resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) + + def test_46_getstate(self): + self.server.request('GET', "/infrastructures/" + self.inf_id + "/state", headers = {'AUTHORIZATION' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure state:" + output) + self.assertEqual(output, "configured", msg="Unexpected inf state: " + output + ". It must be 'configured'.") def test_47_removeresource_noconfig(self): self.server.request('GET', "/infrastructures/" + self.inf_id + "?context=0", headers = {'AUTHORIZATION' : self.auth_data}) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 1dc3e93d0..9d5cd4aa9 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -21,6 +21,9 @@ import sys from mock import Mock +sys.path.append("..") +sys.path.append(".") + from IM.config import Config # To load the ThreadPool class Config.MAX_SIMULTANEOUS_LAUNCHES = 2 @@ -40,7 +43,7 @@ def setUp(self): IM._reinit() # Patch save_data - IM.save_data = staticmethod(lambda: None) + IM.save_data = staticmethod(lambda *args: None) def tearDown(self): IM.stop() From 9fd7a241f1bece0b17c7a08c1fc6a9d85b50b8be Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 5 Nov 2015 10:26:37 +0100 Subject: [PATCH 022/509] Merge devel branch --- IM/ConfManager.py | 1 + IM/InfrastructureManager.py | 51 +++++++++++++++++++++++- IM/REST.py | 2 + IM/ServiceRequests.py | 17 +++++++- connectors/Dummy.py | 78 +++++++++++++++++++++++++++++++++++++ connectors/__init__.py | 2 +- im_service.py | 5 +++ test/TestIM.py | 29 +++++++++----- test/TestRADL.py | 7 +++- test/TestREST.py | 17 ++++++-- test/test_im_logic.py | 5 ++- 11 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 connectors/Dummy.py diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 5ede39cdc..d30bc33c0 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -585,6 +585,7 @@ def configure_master(self): success = False cont = 0 while not success and cont < Config.PLAYBOOK_RETRIES: + time.sleep(cont*5) cont += 1 try: ConfManager.logger.info("Inf ID: " + str(self.inf.id) + ": Start the contextualization process.") diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 3171d8be2..7ac5aed03 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -790,6 +790,55 @@ def GetInfrastructureContMsg(inf_id, auth): InfrastructureManager.logger.debug(res) return res + @staticmethod + def GetInfrastructureState(inf_id, auth): + """ + Get the aggregated state of an infrastructure. + + Args: + + - inf_id(str): infrastructure id. + - auth(Authentication): parsed authentication tokens. + + Return: a str with the state + """ + + InfrastructureManager.logger.info("Getting state of the inf: " + str(inf_id)) + + sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) + + state = None + for vm in sel_inf.get_vm_list(): + if vm.state == VirtualMachine.FAILED: + state = VirtualMachine.FAILED + break + if vm.state == VirtualMachine.UNKNOWN: + state = VirtualMachine.UNKNOWN + break + elif vm.state == VirtualMachine.PENDING: + state = VirtualMachine.PENDING + elif vm.state == VirtualMachine.RUNNING: + if state != VirtualMachine.PENDING: + state = VirtualMachine.RUNNING + elif vm.state == VirtualMachine.STOPPED: + if state is None: + state = VirtualMachine.STOPPED + elif vm.state == VirtualMachine.OFF: + if state is None: + state = VirtualMachine.OFF + elif vm.state == VirtualMachine.CONFIGURED: + if state is None: + state = VirtualMachine.CONFIGURED + elif vm.state == VirtualMachine.UNCONFIGURED: + if state is None: + state = VirtualMachine.UNCONFIGURED + + if state is None: + state = VirtualMachine.UNKNOWN + + InfrastructureManager.logger.debug("inf: " + str(inf_id) + " is in state: " + state) + return state + @staticmethod def StopInfrastructure(inf_id, auth): """ @@ -1204,4 +1253,4 @@ def stop(): InfrastructureManager._exiting = True # Stop all the Ctxt threads of the Infrastructures for inf in InfrastructureManager.infrastructure_list.values(): - inf.stop() + inf.stop() \ No newline at end of file diff --git a/IM/REST.py b/IM/REST.py index 857813962..ca4e9128a 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -158,6 +158,8 @@ def RESTGetInfrastructureProperty(id=None, prop=None): res = InfrastructureManager.GetInfrastructureContMsg(id, auth) elif prop == "radl": res = InfrastructureManager.GetInfrastructureRADL(id, auth) + elif prop == "state": + res = InfrastructureManager.GetInfrastructureState(id, auth) else: bottle.abort(403, "Incorrect infrastructure property") bottle.response.content_type = "text/plain" diff --git a/IM/ServiceRequests.py b/IM/ServiceRequests.py index 0cb38f200..c399bc26a 100644 --- a/IM/ServiceRequests.py +++ b/IM/ServiceRequests.py @@ -37,6 +37,7 @@ class IMBaseRequest(AsyncRequest): GET_INFRASTRUCTURE_INFO = "GetInfrastructureInfo" GET_INFRASTRUCTURE_LIST = "GetInfrastructureList" GET_INFRASTRUCTURE_RADL = "GetInfrastructureRADL" + GET_INFRASTRUCTURE_STATE = "GetInfrastructureState" GET_VM_CONT_MSG = "GetVMContMsg" GET_VM_INFO = "GetVMInfo" GET_VM_PROPERTY = "GetVMProperty" @@ -91,6 +92,9 @@ def create_request(function, arguments = (), priority = Request.PRIORITY_NORMAL) return Request_StartVM(arguments) elif function == IMBaseRequest.STOP_VM: return Request_StopVM(arguments) + elif function == IMBaseRequest.GET_INFRASTRUCTURE_STATE: + return Request_GetInfrastructureState(arguments) + else: raise NotImplementedError("Function not Implemented") @@ -296,4 +300,15 @@ def _call_function(self): self._error_mesage = "Error stopping VM" (inf_id, vm_id, auth_data) = self.arguments InfrastructureManager.InfrastructureManager.StopVM(inf_id, vm_id, Authentication(auth_data)) - return "" \ No newline at end of file + return "" + +class Request_GetInfrastructureState(IMBaseRequest): + """ + Request class for the GetInfrastructureState function + """ + def _call_function(self): + self._error_mesage = "Error gettinf the Inf. state" + (inf_id, auth_data) = self.arguments + return InfrastructureManager.InfrastructureManager.GetInfrastructureState(inf_id, Authentication(auth_data)) + + diff --git a/connectors/Dummy.py b/connectors/Dummy.py new file mode 100644 index 000000000..d0b33daea --- /dev/null +++ b/connectors/Dummy.py @@ -0,0 +1,78 @@ +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +from IM.VirtualMachine import VirtualMachine +from CloudConnector import CloudConnector +from IM.radl.radl import Feature + + +class DummyCloudConnector(CloudConnector): + """ + Cloud Launcher to test the IM. + The connector does nothing. + """ + + type = "Dummy" + """str with the name of the provider.""" + + def concreteSystem(self, radl_system, auth_data): + res_system = radl_system.clone() + return [res_system] + + def updateVMInfo(self, vm, auth_data): + return (True, vm) + + def launch(self, inf, radl, requested_radl, num_vm, auth_data): + res = [] + for _ in range(num_vm): + now = str(int(time.time()*100)) + vm = VirtualMachine(inf, now, self.cloud, requested_radl, requested_radl) + + vm.info.systems[0].setValue('provider.type', self.type) + vm.state = VirtualMachine.RUNNING + + vm.info.systems[0].setValue("net_interface.0.ip","10.0.0.1") + vm.info.systems[0].setValue("disk.0.os.credentials.username", "username") + vm.info.systems[0].setValue("disk.0.os.credentials.password", "password") + + res.append((True, vm)) + + return res + + def finalize(self, vm, auth_data): + return (True, "") + + def stop(self, vm, auth_data): + vm.state = VirtualMachine.STOPPED + return (True, "") + + def start(self, vm, auth_data): + vm.state = VirtualMachine.RUNNING + return (True, "") + + def alterVM(self, vm, radl, auth_data): + if not radl.systems: + return (True, "") + system = radl.systems[0] + + new_cpu = system.getValue('cpu.count') + new_memory = system.getFeature('memory.size').getValue('M') + + vm.info.systems[0].setValue('cpu.count', new_cpu) + vm.info.systems[0].addFeature(Feature("memory.size", "=", new_memory, 'M'), conflict="other", missing="other") + + return (True, "") diff --git a/connectors/__init__.py b/connectors/__init__.py index 801dc15d4..2a2615e63 100644 --- a/connectors/__init__.py +++ b/connectors/__init__.py @@ -15,4 +15,4 @@ # along with this program. If not, see . -__all__ = ['CloudConnector','EC2','OCCI','OpenNebula','OpenStack','LibVirt','LibCloud','Docker','GCE','FogBow', 'Azure', 'DeployedNode','Kubernetes'] +__all__ = ['CloudConnector','EC2','OCCI','OpenNebula','OpenStack','LibVirt','LibCloud','Docker','GCE','FogBow', 'Azure', 'DeployedNode','Kubernetes','Dummy'] diff --git a/im_service.py b/im_service.py index 433e4d77b..3511ffb3c 100755 --- a/im_service.py +++ b/im_service.py @@ -123,6 +123,10 @@ def StartVM(inf_id, vm_id, auth_data): request = IMBaseRequest.create_request(IMBaseRequest.START_VM,(inf_id, vm_id, auth_data)) return WaitRequest(request) +def GetInfrastructureState(inf_id, auth_data): + request = IMBaseRequest.create_request(IMBaseRequest.GET_INFRASTRUCTURE_STATE,(inf_id, auth_data)) + return WaitRequest(request) + def launch_daemon(): """ Launch the IM daemon @@ -161,6 +165,7 @@ def launch_daemon(): server.register_function(GetVMContMsg) server.register_function(StartVM) server.register_function(StopVM) + server.register_function(GetInfrastructureState) InfrastructureManager.logger.info('************ Start Infrastructure Manager daemon (v.%s) ************' % version) diff --git a/test/TestIM.py b/test/TestIM.py index 1a3d051d2..f39fef00a 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -19,6 +19,11 @@ import unittest import xmlrpclib import time +import sys +import os + +sys.path.append("..") +sys.path.append(".") from IM.auth import Authentication from IM.VirtualMachine import VirtualMachine @@ -27,7 +32,7 @@ RADL_ADD_WIN = "network publica\nnetwork privada\nsystem windows\ndeploy windows 1 one" RADL_ADD = "network publica\nnetwork privada\nsystem wn\ndeploy wn 1 one" RADL_ADD_ERROR = "system wnno deploy wnno 1" -TESTS_PATH = '/home/micafer/codigo/git_im/im/test' +TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) RADL_FILE = TESTS_PATH + '/test.radl' #RADL_FILE = TESTS_PATH + '/test_ec2.radl' AUTH_FILE = TESTS_PATH + '/auth.dat' @@ -199,7 +204,15 @@ def test_19_addresource(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_20_addresource_noconfig(self): + def test_20_getstate(self): + """ + Test the GetInfrastructureState IM function + """ + (success, state) = self.server.GetInfrastructureState(self.inf_id, self.auth_data) + self.assertTrue(success, msg="ERROR calling GetInfrastructureState: " + str(state)) + self.assertEqual(state, "configured", msg="Unexpected inf state: " + state + ". It must be 'configured'.") + + def test_21_addresource_noconfig(self): """ Test AddResource function with the contex option to False """ @@ -210,7 +223,7 @@ def test_20_addresource_noconfig(self): self.assertTrue(success, msg="ERROR calling GetInfrastructureInfo:" + str(vm_ids)) self.assertEqual(len(vm_ids), 5, msg="ERROR getting infrastructure info: Incorrect number of VMs(" + str(len(vm_ids)) + "). It must be 3") - def test_21_removeresource(self): + def test_22_removeresource(self): """ Test RemoveResource function """ @@ -231,7 +244,7 @@ def test_21_removeresource(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_22_removeresource_noconfig(self): + def test_23_removeresource_noconfig(self): """ Test RemoveResource function with the context option to False """ @@ -249,7 +262,7 @@ def test_22_removeresource_noconfig(self): self.assertTrue(success, msg="ERROR getting VM state:" + str(res)) self.assertEqual(vm_state, VirtualMachine.CONFIGURED, msg="ERROR unexpected state. Expected 'running' and obtained " + vm_state) - def test_23_reconfigure(self): + def test_24_reconfigure(self): """ Test Reconfigure function """ @@ -259,7 +272,7 @@ def test_23_reconfigure(self): all_stopped = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_stopped, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_24_reconfigure_vmlist(self): + def test_25_reconfigure_vmlist(self): """ Test Reconfigure function specifying a list of VMs """ @@ -269,7 +282,7 @@ def test_24_reconfigure_vmlist(self): all_stopped = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_stopped, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_25_reconfigure_radl(self): + def test_26_reconfigure_radl(self): """ Test Reconfigure function specifying a new RADL """ @@ -344,8 +357,6 @@ def test_40_export_import(self): (success, res) = self.server.ImportInfrastructure(res, self.auth_data) self.assertTrue(success, msg="ERROR calling ImportInfrastructure: " + str(res)) - self.assertEqual(res, self.inf_id+1, msg="ERROR importing the inf.") - def test_50_destroy(self): """ Test DestroyInfrastructure function diff --git a/test/TestRADL.py b/test/TestRADL.py index fe83f69d5..9546c1b8e 100755 --- a/test/TestRADL.py +++ b/test/TestRADL.py @@ -16,8 +16,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -TESTS_PATH = '/home/micafer/codigo/git_im/im/test' +import sys +import os +TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) + +sys.path.append("..") +sys.path.append(".") from IM.radl.radl_parse import parse_radl from IM.radl.radl import RADL, Features, Feature, RADLParseException, system diff --git a/test/TestREST.py b/test/TestREST.py index de628198d..66d8aeea8 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -20,6 +20,10 @@ import os import httplib import time +import sys + +sys.path.append("..") +sys.path.append(".") from IM.VirtualMachine import VirtualMachine from IM.uriparse import uriparse @@ -28,11 +32,11 @@ PID = None RADL_ADD = "network publica\nsystem front\ndeploy front 1" RADL_ADD_ERROR = "system wnno deploy wnno 1" -TESTS_PATH = '/home/micafer/codigo/git_im/im/test' +TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) RADL_FILE = TESTS_PATH + '/test_simple.radl' AUTH_FILE = TESTS_PATH + '/auth.dat' -HOSTNAME = "jonsu.i3m.upv.es" +HOSTNAME = "localhost" TEST_PORT = 8800 class TestIM(unittest.TestCase): @@ -72,7 +76,7 @@ def wait_inf_state(self, state, timeout, incorrect_states = [], vm_ids = None): vm_ids = output.split("\n") else: - pass + pass err_states = [VirtualMachine.FAILED, VirtualMachine.OFF, VirtualMachine.UNCONFIGURED] err_states.extend(incorrect_states) @@ -213,6 +217,13 @@ def test_45_addresource_noconfig(self): resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) + + def test_46_getstate(self): + self.server.request('GET', "/infrastructures/" + self.inf_id + "/state", headers = {'AUTHORIZATION' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure state:" + output) + self.assertEqual(output, "configured", msg="Unexpected inf state: " + output + ". It must be 'configured'.") def test_47_removeresource_noconfig(self): self.server.request('GET', "/infrastructures/" + self.inf_id + "?context=0", headers = {'AUTHORIZATION' : self.auth_data}) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 1dc3e93d0..9d5cd4aa9 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -21,6 +21,9 @@ import sys from mock import Mock +sys.path.append("..") +sys.path.append(".") + from IM.config import Config # To load the ThreadPool class Config.MAX_SIMULTANEOUS_LAUNCHES = 2 @@ -40,7 +43,7 @@ def setUp(self): IM._reinit() # Patch save_data - IM.save_data = staticmethod(lambda: None) + IM.save_data = staticmethod(lambda *args: None) def tearDown(self): IM.stop() From 2e4db763a02e6011b8e44fbe2f0f1413cd03317b Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 5 Nov 2015 10:37:45 +0100 Subject: [PATCH 023/509] Merge devel branch --- IM/VirtualMachine.py | 27 +- INSTALL | 2 +- README.md | 10 +- changelog | 1 + connectors/GCE.py | 3 +- connectors/OCCI.py | 195 ++++++++- connectors/OpenNebula.py | 2 +- contextualization/conf-ansible.yml | 12 +- doc/Makefile | 153 -------- doc/source/conf.py | 122 ++++-- doc/source/manual.rst | 12 +- doc/source/mimic/artwork/logo.svg | 107 ----- doc/source/mimic/layout.html | 48 --- doc/source/mimic/static/bg2.jpg | Bin 79203 -> 0 bytes doc/source/mimic/static/darkmetal.png | Bin 44361 -> 0 bytes doc/source/mimic/static/fondobarra2.png | Bin 414 -> 0 bytes doc/source/mimic/static/grycap.css | 259 ------------ doc/source/mimic/static/headerbg.png | Bin 298 -> 0 bytes doc/source/mimic/static/logo.png | Bin 15002 -> 0 bytes doc/source/mimic/static/metal.png | Bin 21543 -> 0 bytes doc/source/mimic/static/navigation.png | Bin 217 -> 0 bytes doc/source/mimic/static/print.css | 7 - doc/source/mimic/static/scrolls.css_t | 434 --------------------- doc/source/mimic/static/theme_extras.js | 26 -- doc/source/mimic/static/watermark.png | Bin 107625 -> 0 bytes doc/source/mimic/static/watermark_blur.png | Bin 14470 -> 0 bytes doc/source/mimic/theme.conf | 11 - 27 files changed, 320 insertions(+), 1111 deletions(-) delete mode 100644 doc/Makefile delete mode 100644 doc/source/mimic/artwork/logo.svg delete mode 100644 doc/source/mimic/layout.html delete mode 100644 doc/source/mimic/static/bg2.jpg delete mode 100644 doc/source/mimic/static/darkmetal.png delete mode 100644 doc/source/mimic/static/fondobarra2.png delete mode 100644 doc/source/mimic/static/grycap.css delete mode 100644 doc/source/mimic/static/headerbg.png delete mode 100644 doc/source/mimic/static/logo.png delete mode 100644 doc/source/mimic/static/metal.png delete mode 100644 doc/source/mimic/static/navigation.png delete mode 100644 doc/source/mimic/static/print.css delete mode 100644 doc/source/mimic/static/scrolls.css_t delete mode 100644 doc/source/mimic/static/theme_extras.js delete mode 100644 doc/source/mimic/static/watermark.png delete mode 100644 doc/source/mimic/static/watermark_blur.png delete mode 100644 doc/source/mimic/theme.conf diff --git a/IM/VirtualMachine.py b/IM/VirtualMachine.py index 0fcc8dc96..0183248be 100644 --- a/IM/VirtualMachine.py +++ b/IM/VirtualMachine.py @@ -434,21 +434,28 @@ def setIps(self,public_ips,private_ips): vm_system = self.info.systems[0] if public_ips and not set(public_ips).issubset(set(private_ips)): - public_net = None + public_nets = [] for net in self.info.networks: if net.isPublic(): - public_net = net - - if public_net is None: + public_nets.append(net) + + if public_nets: + public_net = None + for net in public_nets: + num_net = self.getNumNetworkWithConnection(net.id) + if num_net is not None: + public_net = net + break + + if not public_net: + # There are a public net but it has not been used in this VM + public_net = public_nets[0] + num_net = self.getNumNetworkIfaces() + else: + # There no public net, create one public_net = network.createNetwork("public." + now, True) self.info.networks.append(public_net) num_net = self.getNumNetworkIfaces() - else: - # If there are are public net, get the ID - num_net = self.getNumNetworkWithConnection(public_net.id) - if num_net is None: - # There are a public net but it has not been used in this VM - num_net = self.getNumNetworkIfaces() for public_ip in public_ips: if public_ip not in private_ips: diff --git a/INSTALL b/INSTALL index d62e13684..80335e10e 100644 --- a/INSTALL +++ b/INSTALL @@ -77,7 +77,7 @@ $ mv IM-X.XX /usr/local Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory. -$ ln -s /usr/local/im/scripts/im /etc/init.d +$ ln -s /usr/local/im/scripts/im /etc/init.d/im 1.4 CONFIGURATION diff --git a/README.md b/README.md index e7f0719ff..b220301a4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ However, if you install IM from sources you should install: + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. - To ensure the functionality the following values must be set in the ansible.cfg file: + To ensure the functionality the following values must be set in the ansible.cfg file (usually found in /etc/ansible/): ``` [defaults] @@ -141,6 +141,12 @@ On Debian Systems: $ chkconfig im on ``` +Or for newer systems like ubuntu 14.04: + +``` +$ sysv-rc-conf im on +``` + On RedHat Systems: ``` @@ -199,4 +205,4 @@ How to launch the IM service using docker: ```sh sudo docker run -d -p 8899:8899 --name im grycap/im -``` \ No newline at end of file +``` diff --git a/changelog b/changelog index d83ac252d..6f38c19c4 100644 --- a/changelog +++ b/changelog @@ -153,3 +153,4 @@ IM 1.4.0 * Add IM-USER tag to EC2 instances * Improve the DB serialization * Change Infrastructure ID from int to string: The API changes and the stored data is not compatible with old versions + * Add GetInfrastructureState function diff --git a/connectors/GCE.py b/connectors/GCE.py index 27b1d1095..54b47e370 100644 --- a/connectors/GCE.py +++ b/connectors/GCE.py @@ -148,7 +148,8 @@ def get_net_provider_id(radl): if net: provider_id = net.getValue('provider_id') - break; + if provider_id: + break; # TODO: check that the net exist in GCE return provider_id diff --git a/connectors/OCCI.py b/connectors/OCCI.py index 3799633e3..7c016b6e7 100644 --- a/connectors/OCCI.py +++ b/connectors/OCCI.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import time from ssl import SSLError import json import os @@ -148,11 +149,36 @@ def concreteSystem(self, radl_system, auth_data): return res + def get_attached_volumes(self, occi_res): + """ + Get the attached volumes in VM from the OCCI information returned by the server + """ + # Link: ;rel="http://schemas.ogf.org/occi/infrastructure#storage";self="/link/storagelink/compute_10_disk_0";category="http://schemas.ogf.org/occi/infrastructure#storagelink http://opennebula.org/occi/infrastructure#storagelink";occi.core.id="compute_10_disk_0";occi.core.title="ttylinux - kvm_file0";occi.core.target="/storage/0";occi.core.source="/compute/10";occi.storagelink.deviceid="/dev/hda";occi.storagelink.state="active" + lines = occi_res.split("\n") + res = [] + for l in lines: + if l.find('Link:') != -1 and l.find('/storage/') != -1: + num_link = None + num_storage = None + device = None + parts = l.split(';') + for part in parts: + kv = part.split('=') + if kv[0].strip() == "self": + num_link = kv[1].strip('"') + elif kv[0].strip() == "occi.storagelink.deviceid": + device = kv[1].strip('"') + elif kv[0].strip() == "occi.core.target": + num_storage = kv[1].strip('"') + if num_link and num_storage: + res.append((num_link, num_storage, device)) + return res def get_net_info(self, occi_res): """ Get the net related information about a VM from the OCCI information returned by the server """ + # Link: ;rel="http://schemas.ogf.org/occi/infrastructure#network";self="/link/networkinterface/compute_10_nic_0";category="http://schemas.ogf.org/occi/infrastructure#networkinterface http://schemas.ogf.org/occi/infrastructure/networkinterface#ipnetworkinterface http://opennebula.org/occi/infrastructure#networkinterface";occi.core.id="compute_10_nic_0";occi.core.title="private";occi.core.target="/network/1";occi.core.source="/compute/10";occi.networkinterface.interface="eth0";occi.networkinterface.mac="10:00:00:00:00:05";occi.networkinterface.state="active";occi.networkinterface.address="10.100.1.5";org.opennebula.networkinterface.bridge="br1" lines = occi_res.split("\n") res = [] for l in lines: @@ -362,6 +388,100 @@ def get_cloud_init_data(self, radl): else: return None + def create_volumes(self, system, auth_data): + """ + Attach a the required volumes (in the RADL) to the launched instance + + Arguments: + - instance(:py:class:`boto.ec2.instance`): object to connect to EC2 instance. + - vm(:py:class:`IM.VirtualMachine`): VM information. + """ + volumes = {} + cont = 1 + while system.getValue("disk." + str(cont) + ".size") and system.getValue("disk." + str(cont) + ".device"): + disk_size = system.getFeature("disk." + str(cont) + ".size").getValue('G') + disk_device = system.getValue("disk." + str(cont) + ".device") + # get the last letter and use vd + disk_device = "vd" + disk_device[-1] + system.setValue("disk." + str(cont) + ".device", disk_device) + self.logger.debug("Creating a %d GB volume for the disk %d" % (int(disk_size), cont)) + success, volume_id = self.create_volume(int(disk_size), "im-disk-%d" % cont, auth_data) + if success: + volumes[disk_device] = volume_id + system.setValue("disk." + str(cont) + ".provider_id", volume_id) + cont += 1 + + return volumes + + def create_volume(self, size, name, auth_data): + """ + Creates a volume of the specified data (in GB) + + returns the OCCI ID of the storage object + """ + try: + auth_header = self.get_auth_header(auth_data) + + conn = self.get_http_connection(auth_data) + + conn.putrequest('POST', "/storage/") + if auth_header: + conn.putheader(auth_header.keys()[0], auth_header.values()[0]) + conn.putheader('Accept', 'text/plain') + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Connection', 'close') + + body = 'Category: storage; scheme="http://schemas.ogf.org/occi/infrastructure#"; class="kind"\n' + body += 'X-OCCI-Attribute: occi.core.title="%s"\n' % name + body += 'X-OCCI-Attribute: occi.storage.size=%f\n' % float(size) + + conn.putheader('Content-Length', len(body)) + conn.endheaders(body) + + resp = conn.getresponse() + + output = resp.read() + + self.delete_proxy(conn) + + if resp.status != 201: + return False, resp.reason + "\n" + output + else: + if 'location' in resp.msg.dict: + occi_id = os.path.basename(resp.msg.dict['location']) + else: + occi_id = os.path.basename(output) + return True, occi_id + except Exception, ex: + self.logger.exception("Error creating volume") + return False, str(ex) + + def delete_volume(self, storage_id, auth_data): + auth = self.get_auth_header(auth_data) + headers = {'Accept': 'text/plain', 'Connection':'close'} + if auth: + headers.update(auth) + + self.logger.debug("Delete storage: %s" % storage_id) + + try: + conn = self.get_http_connection(auth_data) + conn.request('DELETE', "/storage/" + storage_id, headers = headers) + resp = conn.getresponse() + self.delete_proxy(conn) + output = str(resp.read()) + if resp.status == 404: + self.logger.debug("It does not exist.") + return (True, "") + elif resp.status != 200: + return (False, "Error deleting the Volume: " + resp.reason + "\n" + output) + else: + self.logger.debug("Successfully deleted") + return (True, "") + except Exception: + self.logger.exception("Error connecting with OCCI server") + return (False, "Error connecting with OCCI server") + def launch(self, inf, radl, requested_radl, num_vm, auth_data): system = radl.systems[0] auth_header = self.get_auth_header(auth_data) @@ -425,7 +545,11 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): instance_scheme = instance_type_uri[0] + "://" + instance_type_uri[1] + instance_type_uri[2] + "#" while i < num_vm: + volumes = {} try: + # First create the volumes + volumes = self.create_volumes(system, auth_data) + conn.putrequest('POST', "/compute/") if auth_header: conn.putheader(auth_header.keys()[0], auth_header.values()[0]) @@ -436,7 +560,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): body = 'Category: compute; scheme="http://schemas.ogf.org/occi/infrastructure#"; class="kind"\n' body += 'Category: ' + os_tpl + '; scheme="' + os_tpl_scheme + '"; class="mixin"\n' body += 'Category: user_data; scheme="http://schemas.openstack.org/compute/instance#"; class="mixin"\n' - #body += 'Category: public_key; scheme="http://schemas.openstack.org/instance/credentials#"; class="mixin"\n' + #body += 'Category: public_key; scheme="http://schemas.openstack.org/instance/credentials#"; class="mixin"\n' if instance_type_uri: body += 'Category: ' + instance_name + '; scheme="' + instance_scheme + '"; class="mixin"\n' @@ -448,6 +572,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): if memory: body += 'X-OCCI-Attribute: occi.compute.memory=' + str(memory) + '\n' + compute_id = "im." + str(int(time.time()*100)) + body += 'X-OCCI-Attribute: occi.core.id="' + compute_id + '"\n' body += 'X-OCCI-Attribute: occi.core.title="' + name + '"\n' # TODO: evaluate to set the hostname defined in the RADL body += 'X-OCCI-Attribute: occi.compute.hostname="' + name + '"\n' @@ -456,6 +582,12 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): #body += 'X-OCCI-Attribute: org.openstack.credentials.publickey.data="ssh-rsa BAA...zxe ==user@host"' body += 'X-OCCI-Attribute: org.openstack.compute.user_data="' + user_data + '"\n' + # Add volume links + for device, volume_id in volumes.iteritems(): + body += 'Link: ;rel="http://schemas.ogf.org/occi/infrastructure#storage";category="http://schemas.ogf.org/occi/infrastructure#storagelink http://opennebula.org/occi/infrastructure#storagelink";occi.core.target="/storage/%s";occi.core.source="/compute/%s";occi.storagelink.deviceid="/dev/%s"\n' % (volume_id, volume_id, compute_id, device) + + self.logger.debug(body) + conn.putheader('Content-Length', len(body)) conn.endheaders(body) @@ -466,6 +598,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): if resp.status != 201: res.append((False, resp.reason + "\n" + output)) + for volume_id in volumes.values(): + self.delete_volume(volume_id, auth_data) else: if 'location' in resp.msg.dict: occi_vm_id = os.path.basename(resp.msg.dict['location']) @@ -481,6 +615,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): except Exception, ex: self.logger.exception("Error connecting with OCCI server") res.append((False, "ERROR: " + str(ex))) + for volume_id in volumes.values(): + self.delete_volume(volume_id, auth_data) i += 1 @@ -488,6 +624,50 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): return res + def get_volume_ids_from_radl(self, system): + volumes = [] + cont = 1 + while system.getValue("disk." + str(cont) + ".size") and system.getValue("disk." + str(cont) + ".device"): + provider_id = system.getValue("disk." + str(cont) + ".provider_id") + if provider_id: + volumes.append(provider_id) + cont += 1 + + return volumes + + def delete_volumes(self, vm, auth_data): + auth = self.get_auth_header(auth_data) + headers = {'Accept': 'text/plain', 'Connection':'close'} + if auth: + headers.update(auth) + + try: + conn = self.get_http_connection(auth_data) + conn.request('GET', "/compute/" + vm.id, headers = headers) + resp = conn.getresponse() + + output = resp.read() + if resp.status == 404: + return (True, "") + elif resp.status != 200: + return (False, resp.reason + "\n" + output) + else: + occi_volumes = self.get_attached_volumes(output) + deleted_vols = [] + for _, num_storage, device in occi_volumes: + if not device.endswith("vda"): + deleted_vols.append(num_storage) + self.delete_volume(num_storage, auth_data) + + # sometime we have created a volume that is not correctly attached to the vm + # check the RADL of the VM to get them + radl_volumes = self.get_volume_ids_from_radl(vm.info.systems[0]) + for num_storage in radl_volumes: + self.delete_volume(num_storage, auth_data) + except Exception, ex: + self.logger.exception("Error deleting volumes") + return (False, "Error deleting volumes " + str(ex)) + def finalize(self, vm, auth_data): auth = self.get_auth_header(auth_data) headers = {'Accept': 'text/plain', 'Connection':'close'} @@ -498,17 +678,18 @@ def finalize(self, vm, auth_data): conn = self.get_http_connection(auth_data) conn.request('DELETE', "/compute/" + vm.id, headers = headers) resp = conn.getresponse() - self.delete_proxy(conn) output = str(resp.read()) - if resp.status == 404: - return (True, vm.id) - elif resp.status != 200: + if resp.status != 200 and resp.status != 404: return (False, "Error removing the VM: " + resp.reason + "\n" + output) - else: - return (True, vm.id) except Exception: self.logger.exception("Error connecting with OCCI server") return (False, "Error connecting with OCCI server") + + # now try to delete the volumes + self.delete_volumes(vm, auth_data) + + self.delete_proxy(conn) + return (True, vm.id) def stop(self, vm, auth_data): diff --git a/connectors/OpenNebula.py b/connectors/OpenNebula.py index b47175952..51bd9e2e8 100644 --- a/connectors/OpenNebula.py +++ b/connectors/OpenNebula.py @@ -773,7 +773,7 @@ def alterVM(self, vm, radl, auth_data): if self.checkResize(): if not radl.systems: - return "" + return (True, "") system = radl.systems[0] cpu = vm.info.systems[0].getValue('cpu.count') diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 809725f42..a0c2dd8cf 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -2,11 +2,21 @@ - hosts: all sudo: yes vars: - ANSIBLE_VERSION: 1.9.2 + ANSIBLE_VERSION: 1.9.4 tasks: - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" + + # Disable IPv6 + - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" + with_items: + - 'net.ipv6.conf.all.disable_ipv6' + - 'net.ipv6.conf.default.disable_ipv6' + - 'net.ipv6.conf.lo.disable_ipv6' + ignore_errors: yes + - command: sysctl -p + ignore_errors: yes - name: Apt-get update apt: update_cache=yes diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index c9ac13450..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,153 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/IM.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/IM.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/IM" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/IM" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/source/conf.py b/doc/source/conf.py index 68534aca8..0d10dae29 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,9 +1,11 @@ +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# IM documentation build configuration file, created by -# sphinx-quickstart on Tue Mar 11 16:49:14 2014. +# IM Documentation documentation build configuration file, created by +# sphinx-quickstart on Tue Sep 22 10:07:54 2015. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its +# containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,28 +13,37 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os + +sys.path.append(os.path.abspath('.')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../..')) - -from IM import __version__ as im_version +#sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.graphviz', - 'sphinx.ext.todo'] +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', +] + +# Math +mathjax_path = "http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" + # The suffix of source filenames. source_suffix = '.rst' @@ -43,18 +54,17 @@ master_doc = 'index' # General information about the project. -project = u'IM' -copyright = u'2014, Miguel Caballer Fernandez' +project = u'IM Documentation' +copyright = u'2015, I3M' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -pos = im_version.rfind('.') -version = im_version[:pos] +version = '1.0' # The full version, including alpha/beta/rc tags. -release = im_version +release = '1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -68,9 +78,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = [] +exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. +# The reST default role (used for this markup: `text`) to use for all +# documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. @@ -90,12 +101,33 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ["../..","."] + +html_theme = 'sphinx_rtd_theme' -# -- Options for HTML output --------------------------------------------------- +html_theme_options = { + # 'sticky_navigation': True # Set to False to disable the sticky nav while scrolling. + # 'logo_only': True, # if we have a html_logo below, this shows /only/ the logo with no title text +} + +html_static_path = ['_static'] + +html_context = { + 'css_files': [ + '_static/theme_overrides.css', # overrides for wide tables in RTD theme + ], + } # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'mimic' +#html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -103,7 +135,7 @@ #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ["."] +#html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -124,7 +156,11 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -145,13 +181,19 @@ #html_domain_indices = True # If false, no index is generated. -#html_use_index = True +html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +html_show_sourcelink = False + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False @@ -168,10 +210,10 @@ #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'IMdoc' +htmlhelp_basename = 'IMDocumentation' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). @@ -185,10 +227,11 @@ } # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'IM.tex', u'IM Documentation', - u'Miguel Caballer Fernandez', 'manual'), + ('index', 'IMDocumentation.tex', u'IM Documentation', + u'I3M', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -212,27 +255,27 @@ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'im', u'IM Documentation', - [u'Miguel Caballer Fernandez'], 1) + ('index', 'imdocumentation', u'IM Documentation', + [u'I3M'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False -# -- Options for Texinfo output ------------------------------------------------ +# -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'IM', u'IM Documentation', - u'Miguel Caballer Fernandez', 'IM', 'One line description of project.', + ('index', 'IMDocumentation', u'IM Documentation', + u'I3M', 'IMDocumentation', 'One line description of project.', 'Miscellaneous'), ] @@ -245,9 +288,8 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' -# -- Options for TODO extension ------------------------------------------------ - -#todo_include_todos = True +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False # -- Extension interface ------------------------------------------------------- @@ -255,3 +297,5 @@ def setup(app): app.add_object_type('confval', 'confval', objname='configuration value', indextemplate='pair: %s; configuration value') + + diff --git a/doc/source/manual.rst b/doc/source/manual.rst index 9330da12e..3c14bbddf 100644 --- a/doc/source/manual.rst +++ b/doc/source/manual.rst @@ -106,10 +106,10 @@ content and move the extracted directory to the installation path (for instance :file:`/usr/local` or :file:`/opt`):: $ tar xvzf IM-0.1.tar.gz - $ sudo chown -R r```````````````````````````````````````````````oot:root IM-0.1.tar.gz + $ sudo chown -R root:root IM-0.1.tar.gz $ sudo mv IM-0.1 /usr/local -Finally you must copy (or link) $IM_PATH//scripts/im file to /etc/init.d directory:: +Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory:: $ sudo ln -s /usr/local/IM-0.1/scripts/im /etc/init.d @@ -129,9 +129,13 @@ If you want the IM Service to be started at boot time, do To do the last step on a Debian based distributions, execute:: + $ sudo sysv-rc-conf im on + +if the package 'sysv-rc-conf' is not available in your distribution, execute:: + $ sudo update-rc.d im start 99 2 3 4 5 . stop 05 0 1 6 . -or the next command on Red Hat based:: +For Red Hat based distributions:: $ sudo chkconfig im on @@ -447,4 +451,4 @@ default configuration. Information about this image can be found here: https://r How to launch the IM service using docker:: - $ sudo docker run -d -p 8899:8899 --name im grycap/im \ No newline at end of file + $ sudo docker run -d -p 8899:8899 --name im grycap/im diff --git a/doc/source/mimic/artwork/logo.svg b/doc/source/mimic/artwork/logo.svg deleted file mode 100644 index 0907a4ea3..000000000 --- a/doc/source/mimic/artwork/logo.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - Project - - diff --git a/doc/source/mimic/layout.html b/doc/source/mimic/layout.html deleted file mode 100644 index 1519ea527..000000000 --- a/doc/source/mimic/layout.html +++ /dev/null @@ -1,48 +0,0 @@ -{# - scrolls/layout.html - ~~~~~~~~~~~~~~~~~~~ - - Sphinx layout template for the scrolls theme, originally written - by Armin Ronacher. - - :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{%- extends "basic/layout.html" %} -{% set script_files = script_files + ['_static/theme_extras.js'] %} -{# % set css_files = css_files + ['_static/print.css'] % #} -{% set css_files = css_files + ['_static/grycap.css'] %} -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %}{% endblock %} -{% block content %} -
- -
- -
-
-
- {% block body %}{% endblock %} -
-
-© I3M-GRyCAP-UPV, - Universitat Politècnica de València - 46022, Valencia.
-Contact: micafer1 _at_ upv.es -
-
-
-
-{% endblock %} diff --git a/doc/source/mimic/static/bg2.jpg b/doc/source/mimic/static/bg2.jpg deleted file mode 100644 index f61c156f36b61b86ca38275a7ce7689986924e2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79203 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 z 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 DJ9MLM diff --git a/doc/source/mimic/static/grycap.css b/doc/source/mimic/static/grycap.css deleted file mode 100644 index d03214ae7..000000000 --- a/doc/source/mimic/static/grycap.css +++ /dev/null @@ -1,259 +0,0 @@ -* { - padding: 0; - margin: 0; -} -body { - text-align: center; - background-color: #aaa; - background: url(bg2.jpg) no-repeat; -} -#main-wrapper { - width:900px; - margin:0 auto; - margin-top: 32px; -} -div.break { - clear: both; -} -div.hole { - background: transparent; - height: 24px; -} - -#code{ - margin-left: 40px; - padding-left: 10px; - width: 800px; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #FFF; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; - position: relative; - top: 3px; - left: 0px; - text-align: justify; - box-shadow: 0px 0px 40px #acc; - color: #034d80; - font-family: Courier; - font-size: 14px; -} - -#logo { - width: 900px; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #AAAA; - height: 128px; - box-shadow: 0px 0px 40px #acc; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; -} -#logo .img { - float: left; - position: relative; - top: 15px; - left: 52px; - padding-right: 10px; -} -#logo .text { - position: relative; - top: 3px; - left: 52px; - text-align: left; - height: 98px; - color: #034d80; - font-family: Helvetica, Arial, Verdana, sans-serif; - font-size: 90px; - text-shadow: 2px 2px 4px #575; -} -#logo .min { - position: relative; - left: -75px; - top: 21px; - padding-top: 21px; /* para explorer */ - font-size: 14px; - height: 32px; -} - - -#menu { - width: 100%; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #fff; - text-align: left; - height: 48px; - background: url(fondobarra2.png) -} -#menu-text { - position: relative; - top: 14px; - margin: 0px 10px; - text-align: center; -} -#menu-text a { - color: #fff; - font-family: Helvetica, Arial, Verdana, sans-serif; - font-size: 16px; - text-decoration: none; -} -#menu-text li { - display: inline; - padding: 0 16px; - border-right: 1px solid #aca; -} -#menu-text li.last { - border-right: 0px; -} -#menu-text ul { - list-style-type: none; - display: block; -} -#content { - width: 100%; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #fff; - font-family: Helvetica, Arial, Verdana, sans-serif; - font-size: 14px; - color: #367; - text-decoration: none; - text-align: left; - background: #fff no-repeat 0 0; - box-shadow: 0px 0px 40px #acc; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; -} -#content h1 { - font-size: 32px; - padding: 48px 52px 16px 52px; -} -#content h2 { - font-size: 24px; - padding: 16px 52px 0px 52px; -} -#content h3 { - padding: 16px 52px 0px 52px; -} -#content p, #content dd { - font-size: 14px; - padding: 8px 52px; - text-align: justify; - line-height: 22px; - with: 100%; -} -#content dd table { - margin: 5px 0px; -} -#content ol, #content ul { - font-size: 14px; - padding: 0px 56px; - text-align: justify; - line-height: 22px; - width: 100%; -} -#content dd p, #content dd dl, #content li pre, #content dd ul { - padding: 0px; - margin: 0px; -} - -#content li { - /*padding-right: 128px; /* los 72 + 56 */ - padding-right: 124px; - padding-bottom: 8px; - /*padding-top: 8px;*/ - position: relative; - left: 16px; -} -#content li p { - padding: 8px 0px; -} -#content a { - color: #367; - font-weight: bold; - /*text-decoration: underline;*/ -} - -#content table { - /* padding-left: 30px; */ -} -#content table thead td { - text-align: center; - font-weight: bold; -} -/*#content tbody td { - text-align: right; - padding: 5px 5px; -}*/ -#content th { - color:#fff; - background-color: #367; - padding: 7px 7px; - border-radius: 10px; - -moz-border-radius: 10px; - box-shadow: 0px 0px 40px #acc; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; -} -.credits { - font-size: 11px; - text-align: center; - padding-top: 32px; -} -.italic { - font-style: italic; -} -.destacado { - font-weight: bold; -} -#content img { - /* padding: 16px 52px; */ - border: 0px; - margin: 16px 52px; -} -#content div { - /*padding: 16px 52px;*/ -} -#content .right { - float: right; -} -#content .center { - clear: both; - text-align: center; -} -#content .left { - float: left; -} -.sup { - vertical-align: baseline; - font-size: 60%; - position: relative; - top: -0.4em; -} -.foot { -} -#content p.legend { - text-align: center; - font-size: 10px; - padding-top: 0; - padding-bottom: 0; -} -#content input { - width: 300px; -} -#content textarea { - width: 300px; - height: 200px; -} -#content input.button { - width: 100px; -} -.figure { - text-align: center; -} -#content p.caption { - text-align: center; - font-weight: bold; -} - diff --git a/doc/source/mimic/static/headerbg.png b/doc/source/mimic/static/headerbg.png deleted file mode 100644 index 0c5b3657c8538e47be88b49daf91600a7936d9f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^j6i&XgAGV(e(L@T5-1LGcVbv~PUa<$!O>_%)r1c48n{Iv*t(u1=&kHeO=jau(EMTTkVrxeh(-lTjCl~;+&tG zo0?a`;9QiNSdyBeP@Y+mp%9Xhs^ISF8}L3wH4mt;(bL5-MC1J2NsfF+3^;%INZK0?I;tz}`+&p{Dl`~u9(3Asz)WZ+$`CX=Y+Iks# z_Whqtry{(T3QcIKdUb!%sly?2pUm6rIDvn&vC*!SxH-S>#$<{eGMi$)bL;X?i*&U79cByE8EyMeEq2?`bm%ar$hQZU-&t;ucLK6Vy|8TDW diff --git a/doc/source/mimic/static/logo.png b/doc/source/mimic/static/logo.png deleted file mode 100644 index 6dc222dfece0b1d32198839d132d4af693b67404..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15002 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 diff --git a/doc/source/mimic/static/metal.png b/doc/source/mimic/static/metal.png deleted file mode 100644 index 97166f131933d6808f8098ba09075c4f973dbe6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21543 zcmZ_0by$<{8~+W8guo~zhtfz2qehE#r*t!Vbf-v)bTh^XX=z4-bTdM_OS%OS3Gtbq z@9&9!p8Gh~JI?LCu3cxm&)2nR4K;bfr_@g|Ffa%e6=bw9Ffg&u_lIz?(bs@?xo+qi zOb;#jw-{C9zytIHJXZw+4-5=^vVRv$jI3-*^piNAimI|W3r}!~u~{eK3>z32bQp>< zQaZj%Cx}&D#^EVQT1ejO!#^);&Yp&EoO)u)j`zpzWeP4!>j+7bmd=SB-BE|9j?~1u z3JD42o}QiUp^w?i%ge+2`#=HJX@{J>S$@^>d!dQT!yiA~8VoCSDJUpLk8*Nz^=xfz z%aKUrfA79NTQ9fI(*5_;dguGUEKaN=+Iz@Kp-lM?G06@>;3)x(PqkL zZ*Fe>eag*>|3qsf`Yr8$#U@4r<|d9>4?B+pu7s{6yjHKOtBQhitA!;c%{uP*RY$KU z9KTFt?)hEaU7!6kFuphYxAG`BIN0=|O(OTYt-~j%(|@(UzyD(0FX2dZ=4fJK!bhTf zu4(S$^-g#g8KC&??ye;`F-GTopDY}0=jYEt{Osy78ojg2pFisZe;C&o_hC&eE-oHk zT)4MjhnAY=h75I9=I*`S4&7PbxnmBwu$lKMb1Z#A?|YFm`+h!j=KY>e^~CUz>W>C6a`CfsbE~a~4ChT)chEzI|7QEhvG!7CvlGkh;q^Leb;}xG z@39Id2x`5bovU~KH$3btF3i4db^P9(Tjt@l&;E)gRUJeELa!X@+437^wAceuQKoZk z8cXSZle95D$yLpG7V|ZJUGgP%POF$Tr`Y!1_R&QaE-?7o`2O|*&$B89^Biq7>OZ4? z(kCnnrvtR5Z<3kDzJLFIA@Q7Lhhp@#TX68h)=xT(le6*haowXUf1D74{=+}+J`-n< z+(FlL3}r3$^^()ov-Qr5>jrIxm%tn)Z!MlP7VxFT(4E3X`CI~%?&^0kK!?!(W`ESn z+w@gFjR&HYcje@;=cyP|P%?Ccm>aL)wym9>ihW1TMbz3vy(42MiJ+`ijIWn35}J0l z8?96F1a@|tL*RA!_0W{c0@BimxwW-riD!G{fnRaeNd`~@X9~i{vO{WC1x6JKC_HQ- z!S?yKb*Pp$oBfP7pP&K%uP@Rw-RXx!_>On%lVZLhaJ}E|3}?-EUuc9j z47txvPfz1t#KihP#IimzI1SuKPwoJ?xvdTE@gpv7j0O@;=D`@r+j#V2fHYIim&yu7 z7AvgRp~vOgQMFd>uD?e2D3ZP*Vw7{W_E_gylxh=;vDY_e1T}r+arEcXlxH60@3k6( zHHwF`PJ=Uja&nR#jz7xH^il=YJ0lO+yoghC!-hrYxQ=fbIP^luo0ex)4yoD#5nLV8=KR#7-{Q_ zG_7O&^DwGg&k7{sg%?u)&~jmT`a>J^QKt;w6`~9^I?2`B-_BZ`jqEI9=fV@*?Rz$| zEl%&4TM`hO<=0ZFD1o{WM-RaoSl<(uTx51^q-Mz5c=@ZZwt<9FF`tL}=|G?px8b1R zW$K7_p^`2^OD0)zkl+TZy-e|&`niR6exkQt>0)v>Y;ci< zK#Nvq#^MY9jfu?Oozu}FRv(;D8B5_As%^_&;d%qSmV*sJ=wcg}3U z;R%!omyexyn%9l{?&f_EE8ecO!eq(^)>6vQizQ?{Lg4hj72)kqRdJQvCMI1%&|7gC zOUUat{oBVl+!!|p0~@!l<5v(tDpYyrOreA18MF3|4@7_HoUd0beOyF}fa=dZRl{SQ zyG6zB39-$MfbZr@P(ePpj>7S|4Xh?CUz4KB6Mr%y?%62MUnO&9ZPbHnT_*vBC?5Mx z`(k46bd3=YSryM;#>E*DFM`~gRW{O*fZ^i2rzWbq{yp8-eche`0it~Cqy76cc1&WW z@0+r!P6DAv{1{9YvXKoOygLlL@%C}_H|y!}x?-nWxAIw^M?k)F9!>mSr4dUcfo%zK zTHp*Yc5VahW1|1_F(zY~q6sJaj}&e+9-~ZDiiifzeE|tHkKt;E(+O(S@Pg%zn>|Lxk7Wg}MUeqaw?h00K2>)R0GyqjZB94R%^rsBEd-1lRRvx) ztC45@IY-ouip-$=N5Z)gef3k=-qDNoFFW*-5@`dRgZ?7;XA8fQf3f*JF@Jo?1m4UW z#C-X}TOb%cyiH9_9=5A1J6tSu2Zuv8KqlpOXOwz(W&b zofD2C-v4Dke9|IM-^4N;} zYjiDi*-??uZBCg0cl;pv_%>r@xQBVr4J*+1U60LcN=2U;@GC9eqc1lYlQTWRGF!QGP9Ldk3{9sGBlL@ZZa}Iu%y@HA()7t8O(s3z+my zn8p^@dqg_n|F>BZmaoTNmGW%S6>W#w-DgxcN415rjhtWeT&(nnjtQ8bhndT zp88emon1-VPUbzW>>Dox`Isj?=K3G`X$0`~+_ zK~M@?4X((Yfe^HQ6t9i8{lzX*E*RAAdBUtzM#f~OE#iY8aLGrN?w*LU7GY(v^SJ=pGbJdl<@aV(` zI81YIHS-hldygs56%byA=8ap*FuY&@co&W$V8uzwco-Zcwcgn~^!S6Vh`AZ()o=?n zDoeIkaZD;KS*&;H)K)DaFf!yxwum+E?LB}hBJ+127_TY{5${Kx`2!n|BXZKl`(o8q z<4oJp)L43p&Mz{MxKcZXP*E4K{Z*VF$jK(XinU9RDDAoEATg(mOhPpNGJKp)c(>g! z8|u>%_(uW(dAL2QMtFW!yEf|#{_1*Ewvu82TMW^Z91l!j95WcVNhcJba@HLmE-i3;HT+~$wwR~y@Im+PbSYlG=VjaCTP_>`*zi8WKIDFikzWe(8xm%*3 zt0K>?bn-psa#gt}?0MMu@WDcEx_tlVwqcsd!^^9|A05X2++I1lsVc3C^{SD5-=3(`PEeCH zxs2he_uHXTwEp@S@v=v+4^x1^`$A7$COJ7xcel03p_X04FEO#a#tlj}05{vRMK4wp z8;Z<^oUED;0@|R07Ud;y<>;P zRq2dJETh$YFCo6e8p$c|x%kiSpO}Sg#%$4*%f&+7|9}uYihP7>+ITQ-Lt4OoIBG3P zx~n_><=c*~NHu-YQns>K!c&I=8)vEI1Bjo8gWo^_*FB3^Jhne zwrBq7FP-w)%*z~`+uK+0xGKGKcetI&3;cqOQ#500RiA z91;(cAB{6Iy!`#sK2PsEE)G>tFblvNx)4X$h$tg>pAnE_D{48YX;@fsI{2)eXYp6+ z%fv8!5lJ7}NYwJG|f{R-b###)Mm!(o|k#q_jMM$Lb6Si%N=KVPjUhWwVkavcg_NuL4BZZT<|LpX3u``zq;-h(W~!% z@*e#QRv4P%&xFa1m}uX1i8NDHCsyze1orX6KmiCcN5{9Q3vg;0P?VBk;aM_ z+i#hB9fXE}kXDV2xs1^N+fFEet%GO`IZqn;1 zvqc)6g#i~HDwg_H(pX$~mX64d*?el>P}sL=-mMULRdfWU>iG1^%M|LSF7@N?r9nmCqkvCx_^@=soxIbwlbV;qtOE1X<5t8;IK zawe&mbpkx7p^%QDco3=bjmDnb%+=a7RR$65rMfrn)OxjOnO|s4pRCk)?PlH)6RIUC zyE^Hwl1v)W3!y3k?Y}+SdS+1!li)Wry&mUQTn1)2`6a{u&c8}IENx|4hoX_zb=NO-?l+tDt z&ggYxwg3$eeRk~!!+z`o;2+kqy6|1bHOOj)EkJi!CV1tB><*q3(d3zLL z^ePlWO8CxpdzLl`kolHh=dmQNsHtr)AYabmXZ9+!AC^Z1=bW?fGlNK&8Z+P4xb0SV zKKQ#CCRPf4SEbIRlu~FyRELJ$JO7l*;|}MeXIpH%OTHFUUQ=nfFtr|$V%;DA$#hmE zTj}yC?C9tS#@Mb}Jfcz5;dzpH72-v}hsS0K)3>&AVOVB&w z3mHh1@zc*rPmLN5p>b-nkh%WLVA6A#B>L?42V+&htFR({n!R6Xu8Eq`51ga@GyJZ6 zLkfMxJc~Sw_oxb{#o^rN+`S*%9-&LvqT`{MQx??j5j0tqTcL@}K>Jhe*iCCY%cH^e zo5dWZrq%XEQNfwt9c&_&{qz))go4fACiA0IwXfd9iTs2bQ*sa$d)c~3N=Zq5)9aR! z;D_bZ0>Mj^rPg`xvWIJiH?-62roZsjpShY7{rI?>OLROKMG7oCdxkJj4LjZA+GML_E<|71_;U*c|jEt}ZUP)~nt zK(lw&`3L~A)T6D4#u4J1O8$Qg5!%kU<&nc;B zL041CT#IR$sHNp_c5lN4aFbQvhctF7`l-J)9um9MnOezMF9V1~fQ5>q^qc6gUjCJ#$TX=To<`mfev+E zco)<7`BN6Cz7H9{ob<$>)9C|rOZQ8yrffeZz&VcIC^NZEn)9dSkaQqv3MR_Rojlu2 zbgL;OFviMyGx>7-nf9+H2uk$xMEkbg8_OL!YDi|^9BvI$fZb7=xBfE$ukT~cAyC`K zgCW`@XbFzj1xsAR^oJ5BQ$u+4|Vfe)Q}(PQ3y_}4^^W?q z#zU!zI?G7J9FHt^Q^!l8VyrhE17c_3XZ`y2kCWp5){!qbmzEm1#|chLcS$)tvyX8J zla~YMJzLSjOV4p)pb%ncoJThG@RLTx{})48*phEgf&LE8Z7o(tIWh%L(=$|_I&e!t z{;Jkkejki89&wOjCvZ$EueZa}YNXSn74041#G^XvQXRw2tf~8^s%_&Hf?gz8Z7b;# zK~W)Lsj(@*>l)<>H*gD0 z_w%is-5$0byh(ql36o8uV8jOeDpU(8iMYyVC4MC|;pL4PD`q03i z%iFY)nR-f#ag|s{QTpsM_#Hp9e3+26GT?ab7^rZ_!m^A?%q&rMxKTClC#$8=n7Y!O z6zWz|!4jNCkCJp3rnGCyO1x`g(c3RLW4mKEOoe}Kmdbe<8XgE70R=?c`CI?euinLY zL|!+v<;22Du}V}HYQ>Yjq{4mNfKtC~gKeknvf$BCS0$eaL{O_gcLwrPHBf!J@wnw) z3WHxKLterBLu38JCVDXY&Ai`i-{WuP(a6f+;xrzZN+n&|2~g4A zA?~1B)2oUWOxKBw8w448TnWz`@1%Dxeav|hSCKg5pqt{Nn|LZHE>;x8+u?pl}f-|2*5V)WBOywbi|8|jdC zb_rqgxq%(ttRaQG<)K%;AlZ_nqT;PW=4G^}M#nr4wu?H0enUY%{*wBs;;UmvojS20 z&ODWzr6L^BHj_(Og{BYV-~wUBdhQMA!+^$eWHyA!YBj9gdE`A+GHEGRY2~}D$Utm> zJ}G3H?1^Bb6b#{?({<L~?dtRQNF6Fw^d`e&u2eJOT6#r8 z(nlJuHJKI3KNVJg$HbSWiU_JSM8unvJa^ofGjld8#2?eo5WFPvuK%Xj`B1dlXixn+ zWchQQatgDWpgc(+s=TF!cG*Q*>Ox`PNxj&lEj6c8GMV*(cWb7^{gAoH99?&JI5=+_TJ5h-`D%I~jy`eWZ8cLHK3r zCH3#j6lr?(Ih@Y%Ky31NVS|pJ9bXF>s7VT%+IX%zH4meclhkUT6{2?)vA~KL9-&+v zKnFGse@yM^yBK4S>HOwOW!_4cZEZJKA0MG-Jj=3^I!BvAip4yPiy*az!^^_h1Uc z0*O`9odx{_n+Iaug=Nxu{p7%<&qh+ipk6lhGAE&Fg>C%JN7KI*fbFkD4TJkcSlTLD z^Ya&@<+R3bzluiJam7Y_p?bI!xby$!mrOI!rJV~;`N5p}B{`4ZY)I_|YB8<&97#6F zIi#|Gkptdu2l(Am15giD9J58gOBJRlF7eGig!%v7e%5?Lf>Rt*^YcU6r@)uvefM@i zPMXlTi{hKN!I(%V$m*;An+14Ml4%KHwE6m|LeSHyOP_he-qPEo9kZTLh{H$XCqn+Q z&-v zDo02XfJ%srfyfQ{-JoYnOd~3MAZv0BA7|RT4I$52=yysZ+-scaWq7JH2XLTuHTd95 zj|75WYrEM!{F=*fap;f7JVkMc)EMEhSvTsk7bUS!G-W$4C|fX*&nL3(1Z;%7h`o73 zB|H?6Sa1rKiBb`hK5>5hIZbpv3cRo43VL*E9Bqme*6$Bb!o2)@-P5i1I!r(mqM+`E z_k?BfXAf>Ze9Fqi`#6MeU>=wGbj#avF92jce5R}}&PPBW^ECl{GUE13*?PfIMRdB7 zbhj{!SW%P4-nOW>sZT2HNAtn<)k2vutM53-Tpu;8-U!pKko%@>V&!~G3!r4{V0lCr zeEr8tq&wcUx6kOi`GIxbA-lm2_a6W3OCKajw52sE?HjftMx)fKes7(0{<|MRt4_Lz zeg7HJJ)p+~z0sI9&&zga%XjfgQ zGg%Wp%%9hbsa!n342uS6vzHHfzQg}iDx#g))YGkHT|*oR$x7mwlM{Q&IE?QZbVIO% zq8{-UXT-hffJUh1r^KONd>CuRh+E+0j)-FwW25|W4q?G`==tLLSSeC+OjXhHD>Gmy z5RPZ37hi9Oa!vE{1RYYT65}jds!AC|g=JkW(mGRNCfy&S{M)3flmmQ;7Ty1txz9JTF@D$*nxtCS<+qQWKOP(cE3h@8dkkE+ElRzcUp5Y-q9 zr*#9H(MfUfm%{5HR*=ursirQeB(oix)z!f&cNvL6R}DTPk*hD8bOkRRvkqMo+auj% zXAp%mEmL&mZ#>y#nuM8}($!4wj-FYVBdyriASxNOn0}= zuvS*s#&G05yJ*J2a)flnb|d6^jM{&pArs6~87-{D_qp(M68x>8UeVz zzg{vm9$da#fVF$UYvO_8e?77Qdum7`UBR0unVhRaAC_0-RoaOVS{s791^c0>k(CH- zcI7v&4+Sti&w_gWj-xECR?nVhfN4UI8ml+x&-=MU{8Ya$L0AAgCBDPRhQ`Zd4Hckk z9(3(xy!7bA7=QN{(i=n^M_n9!^{ngU;vkzzHwpS|1g`K#wf5NKUF7gk*_9_BF&Jd%2mCMw&Lyk z#nW0*J9ZIoH4cMD7(f=H`_8_+g8D=e(T6X^Up;1W1?ol&QHZuE4mBGL^P$lmGJ&hmF-OgHfHGR>b;qeqm(3m4sK_y;dB(I(d` z_kCLS(rSh6B^`Ag%pvv-zx6Ns`W!kaP z1R~jnYLVOFdqdl|t!(tVcb@xU9j4HBVJ`H+E_ZAEW54l`<~5yx``u~kTGpsrhnaZ0 zAUHK#M`$C}K~l{?1Pi!rc)_%LJrWo8MK5v(cx{7SckGl`Gx%a3t&Q0u7g4BAsKU1Y z*EJGFrtah$HBiIKtU*Adu8~?vt=lrAS@^hK(sPWtjmj_P7L`AqG`i0HSday)SZA71 zJMkEO+nU!};HBv?J79O#4pl|v?XEG1aF`+aF zLE7b#dyUfG+0gmq!0wLii>H9(W_}Z4Z%m~*#!a1?WA%QooquQong-0Oo|g~fsk(zUi?eLQd69W zF!0fbd^7RpU1Qa-*s7MpMAtX@I$L?|cq$)zzbKMM7Yu-M+OzQ$oeLlnX6P`5PKi{h zY`8{YxlD}N(wba*$KjOxQK$)3jY7=uI7gv=z4=s`MAsfWa&hTmo=Z8*`hvq zQxINv>}d%e(fKIMIbFl_GyxAM_C==1T*Kt6@b!e}7}MN3_5xSLHwOJFflvPH>LX#9 z?jqc@ZXQ0vi7e49N6bSwyw)8vpGzXM^V8x*>FUVquoWap{){LGIZ<^TzEx_RwI z8vDks#mNE8M=(6UAf^;K!JH#Z4uoJ?Ne9}|fs*s4LyBHhp?irZ8+je-@-ns%PZU@m z93E6$s#8|mFBCA~@k%LK^LVt;&Cs)d7>G;}(;itNj9{KOzr4=WwmB0K?dAMAic^71 zI+H6gfDMQ1z~rfgmDa_6Q5RY!-o{7|Vn&_q`Y-31!QUym3kyG1s)`Qt&)s6JINQi{ ztN97iupIT)i{xzVLizamFX;R1DpR$ZnVNQJv7;f+mAEbunADmeI^Dnp^phnAv8 z4mcy#QJ0R~yQie%5E zJ-wdpabs*`52%Sz;uYkaZLKCT0yL}U(uK1!#40+LQvb!BLTdt$x)B!^mFmsNzvC?Bz!4lW`rrqc9K=m5^%NRU3 zsPE*h!lQcl)3LJ=valXDcQz&|dmlN$`$;$Rbm!=>!NEZa_SvxpOr2GJ=Wm6yxCPBatF{C#9^BtV50?=VnbH2&S}4QV zW>YTbWc2pU@^)??2JM|XoQmC034iQSinICQ9LgL+4b7%ud>o^rvhr5X$l1mOclcE4 za%0r}IZe0RE}u=fL(t2?*f7qqxX+>zt}PNUo5{ed<&6!rD%E)7Ets`3B#1xO`pw$J z5v2Y+()nJza$;;UOluD-8de8U7gj{BfGM?%no(1TxDug1ox=DDz>lhmLs79UEMvl2 z`d+bMbLOs|-}MGf9~lK$u6W#?I`9GBsh_B-y!`(?Ddp(y#ms$WV_7{(+_v$Ovp5f$Ko)xiiLrwxsmgqHJz zik83P62O-gcG4m)1Dzz=O=dro=*W>}llQ^JEpy|bR@!Q|ts}(oURGI#lwm3z;WF$( zl1H;O0#+S;icjmvltfFO2uLKr|wGj5`X~G%+P2B6=-?b!kzj5q`)WfB}2_ z&D`Tz@i&QO!KlF3=@q^OsQR#;XN<2dTmK)N)BdTn_d2v)ggJknfC{6*TsMg#%gt&RUW>+Owz1s!!@uC@GE(kGE;uIu)8m_;ihP_GMd$y!~`6c-LVNJj~y<5)vmiYvO^IY z(c?9vySIziyc%^}4IZ|qJxwA>ziFKY^3%?nLm02^LAN)24mhR1kSU?Lek$Pok-@;< zqDSK8T0uTQP9&{j;X>x6TDP1$W4Tp|`Bs3p1!F(x88hs6m>=Dutul=W-j}h1g4XhB znastAcl@t@QjvHGeil1}3tSfB6A3>D9D-SZnGK>&-CrJc{|Ky4mE7F;yJg<`^((^f zaM#k)fuHonOsA34y0=857c~ad)h;q>#ol8C5CE*;4Nu8x)ioBZ)i0d7;imLhIasV@ zOINQrY|UnN4fVrN1263OIixOsq;E{=IsArl(SOZ? zIqml%YXlXNxv6OjmxFUZraD5^lOAEI*-SVK!At{I*+A3-!zs~(V!sS`OKw(GF8pJ` z`z+qIefC7FXimzlCY6?JP|%vfg&|5l3-{wc2J~2}Dy9FnoeBg(_mYLiYj^J@JPGQ? z`KCW!Zd+RS&OrSx61|%4C#cQr@SQ(@9vAnYvG#$2_rrx}%CcQ+UjEl#hN3uq#LyJz zN`!AgMM;;#rp>eA;^({NyT7I7Ep}~huCphX(@@a2UV=Nve5NO82K2b-|HFVjN7(2D zRDkJNWpEhyY{k(~<$r8xfmXDavH@4RS@3!y*^;GZg=LNlQ-J=ojasJA2<1w0#auup zb;1~3tONlafwVj!dpZ%E87S?fUByh}@(@@s*~u~e;xPb)UwiosylEaAcJS~JT>OK% zpKw6e{rqvS+NqKWEY5m#gYBkD51%9cX zZI&rn=cN6*Fq=y>D7FGa-7O+V24f#1kA=)4{_%qguoDG*j>~rRI!ZOk=Im{+II8=u zL#?Fg)T1ht4+~duCQhUA)?KjgcWN;*{XfvB>BX&xTy9A5{-9hcTn|h1+`jyzVDjut!YPaxv1d*1)G;Zwz^`?)xrC2HL>mmPJF9zEz@30 z7vp1T`E;0lEuWb4E2l$OAto+O2rR0Ohlf7_-!l17$Y^jyueRs3Dx$G*wrT)cU8`+g z$OG|p5V)(b(uPYN{jlo6EPs5A%Y>H9)WO8Sm6a9LN;g;Meadu?L*v|MsDvwmlV6s( ze)BLJQqlf4(I_)RC{ZYG6)LyH)a;@0rG-R!N>_$VfsGpA2*Tq!m7O=W<$$Sk1NS$6Su9{5dGFt$1_d zrcr2F_hVR$hIcTRuN|$6aP-!MXA(8@RuEL9_Y3WJEeYvY;=paK$kr~FXfR6m?^A0GSn4mjs5C@^R2Oe7ES<&9qze~wgnj^h2;=J+Ul z=~m6kr6j_Qx`H(&5qUQ@QTITYzM`V+GLVz|A?#ZSaGGG@z^1t9M&hQ`zpFf&4F@@y z@dF*S>7P4Qq(>yedKUnL4^C~G_LDa~+nRFK$fNSN#k9!7iM zZd+`>t@Wr&Nv49<*00ntRx|f^G3%cc(gcXbequgAt^FCtg28hpS0!W3*sHGE-kAU#D1#J+2$Z6SN>nWSN)uI zt5L`}3Kfnkfwx<=wfm7LqiXV>i+ynafs#q{vJN)gjrO}RYSNDHk^>i)jT+#}e5C+XaMXOKf~w?>Y1Y_vYf(7+$5JcZX2+>S zhcN(^8-71`t7+y&;-d`JRTsLl*kGD%ExWF8GM`LPrYM(u>ipQ*$LELJZB#yYB9mHs zX3X`F8km2gs*t+G?mUbL)DC$yJlsj1-uI@TJO%*(BhX3 zr!cFJWwH%*wie0VbJv>hQC(uIE0HRq=AWaT>G6Qg??Ya6p0wSw!mY9Gk zd5|DsG|2cUDr}d_DhBca(o^$|hPdz5#G7izcjf0{ zZiZ|Qg}pX~N%`p!7wEE&X0hzY)IOiG+9FR{Al6N6#ff&LlX28LQT-~~t6raA{|by6 z?7uI-bg+2@l)z$`Pk&_4ive)av2(EQM+~XA+c{`0MlZQ(u-?5?)k*D^`R&c+Ud(Q> z8oeDWhN>K=SRGCsd@$n=c9{s|iw3k>pa24ZZpTHmrE^1qM6aGwZOsm75bzgOaQaL! z%`8QyZ1YPK&^S4Z|H(G$(t4rX>=GUJ4frV8qF_wItt;wQ09-oHyTu3Gw$+M~s*XUn z6^ew}%K@tz^H$y6^{dXd9&HW6u1vxrlD?yr6$MEEFjFoHvzc$AVp8DcA&RZ7j)FF% zOkva&+*_M1m}=ENmDo!@idN3dWy4g3fHBVE7mRc3zM11^8|rT6Nx%a9BfEOsQ1GiA zx;=q*Y#bm2bTg@TvGC2Xjng81U-eUgx{8Tij#-c(7J8!lU`Xh zv%_pRHkvWgqzJE7o!onFHYY(wV#YFGgy&}Mhr$~~=BG&}e$1r#Y(w0KD(wd^dt#vt z5yHRtfZutUDH<8c=cyyE@G&qO^(`5YNscO*RUOjkG*_nrER>62+vY{@WkQ!6+8i|r z3dpIbMRR21So`3Ja(a{ZNtxK!(!&w5m1oHsimDzVLu3cmGFKWgm+G!9rJ91=` zOu6RUc2Qxm0FlwAe=;3M-7LJ&U(O88#uR0IaNuVzY>x2~KPMoxuYLK#;Te4K22PXIPFfI20%#S>YLC1jBF3>VP5vLli5B1f$;gOqTtz0hierA8 zm*GzQIL@@%Ex`{1vf>Kwuck+opp=E=mPU=yB@NZ%RJK?zG+&jhG?cU`yEaOYA-dY( z6#P$qOLP{k?w2wZQs?71VF6Su&tjMUAMvf79EYE0Sn~%p^~3pGOcOY-ak30)giAy(gO|WflHF3KK2EWWv-WExYH4=d zGIPX(y1vOR+tdU&eQAVM7KKBeJ(0t^w#CN6>JLkAeDi_@0qJ#&ua|Du_-2~OtY_L~ zdI`DtJ>0#4O9J0WV}v+%2034|?6*3r7~wM7%S|V=iiny-fel&zG_5Tu{b@-UDUY>*k;$e4>NU37HAz9Gh8>T-Og`+7$(5eL=DMe)M zf{8j7ylTokj2<3Gd{$VyOY)}7rB&2GAtz96Zs6u_exoZj;+ddzuYQKodES2U+sn4B z(MJ5UZ=V@pbAOeHDJT`yCbx588j`;mSGzm--`A+3X|IN)2bb$QDMmzQ`NHsIl7R2S+KYGzVd4`1e>20fV;(=#sk!!hwLuU0-8_JBB1~Ki?`k( zBsffierE(YmtxfqH+#9XBzi%6qobpU|Eq{IafkA2+<2CfFv1iXl53A22M2IKlm0z8eW@4M_<7BGtb668iG^_n z;1r=9Gi!2WWaw)K52qPb#H$we9oUBR7hgTd;n15h_a>iwRZN2SLqHo+E7_QOgXr|) z;TJ)CHkt`RatI{Q;lRVgV_Wo3Uq~*EMI9j^NEWH} z{YdT&T2k`k5BNzuusD|a21@nVMWs;-%~x*owbD5>t}=hd@L0xGP-SsPpu=h4$yfZm zQU@*eZgt@E?M->TQSm3;rw#Gh+0nY!RWH#!=Y zMNj*8L04MCK%R|LpsouWFI}8lhp_YkY;6_!QXa9^VvDE*YKY#5>{Iy)M0|9Xk&zD7 zOZrzv-|b5y{lX~7pTB>RN8D=oLd;lo^`c|O zUhA->TY$xTt1G9)6aO0hmf|`(I`Bd2J9E3j>x-0e&?_XXA@fyQT6K8Cs9cnTq`1IP zb2l_?^HHn|0SnIo&>Z7$qm_zO_SN&WMiN)}nWnVATw^c>9SdVPcSzRhK)kK+1Y3 zA?}jw`sZ)po2pVLSLnoXOMj&fWCVIDZBu4i>}v7Y&oCzZrIG$lzssW^cJ2VHE|B*H zA1L(X_?@w<<7z`*dU=qwjZGB=a~g13*C5oZAHIWv{KZEvnyy_GzhCGb&zse{#(}t) zOH5I4>!dYeyw~bh8WXO4Z}a)G@L^i+&S+h={=nBPBQN!O?TO%L4P%R|D0K}I*6ruz zzwBJ2-45%HkrsMC?(g4N3}Z675Gv+1;-Vxl7EQAb5$5uR^6MZCb|=96PC4>_Y=|bf zv8eryvl}6qHq!mW=4UyX=^f_?$!9%+%n-jB%+H*iVat+TxG#lg90jaKQ}_T~A(TNn z=2c?yO&{X%vy6IT_f)x=R3}ZlEtlmTlgIfZ&H< z9?!m*QMf6sodC-c@UO09%e8S9$Ycos#hVlSR%c z;zj@}AAq=gxb0)nF`L3`&XR=I#><}gyuTV%UMZilQ+muDwEQY0T5h1Y9^BJ z?#k%sIoPsR{&V~Lx_59GQ)u1z7SQ(bNYql_eoIbHERp4|nc}*?X`a66`-Q<=3fKmO zK5<^~z}y+gz?~RgL7ow<<__L2~{+p9-dVhvVKab=U0)v94S`#m=4h;@w#I*5Di&{}WSwYlF zqB%VtUK`3-{z+Tc{ma1XEYqE9ulvH`npd8+U{@X8jmKyu6RfKBPxriQ7TSJn!9>Z5 zU2mzaj-r3?V+skE;s6 z5uLFt&HeERq8XC!9w=jH41W(RJ77Cwj;d{5ii*2UIyyUVNL}~56rg)Dm`A&=22JOT zaev5c=RXSmnHg2jX>ap|KL8WcobBBTwm~f_jO8pm9oW8wRD=DG0_Y zk5W><6yO84Hn?9q`UtHgeKA3Kzl$RtYH^u2eNtAE7;9+68s}*BN|!?|eB@+to~Y{} zEV#YBJ@}ruS>EG2LlfhHZvhWnLPSl2Tf=;0G{c2~^zxVqbTxHMdBL@G@Roy8D~m4Q z&~Y`nJe4D$tx>vnnuADlhmY2^)kE@z$EYUpt;iA4JPK#FFAr}Qiyyhip_k* z;G_Dx_(`xl*YwN`$(OHyI7+CjpX{>s_UHmG9Bh?&3#b3Dx zydSb;&*9jTRIIH#4C!*yS*sX+7#J{A%viaV?|J&}%x}bOgG0y5oUb~`mqX9IA!C9{gpV^BM4V5wbArlSLQEF+N|Dr%lLmU06|M9^;*MO*ki=R_03nf zmH?h`8>XqFi*FI8Izg|SBjT#|k|6ZRtFt!8u4)Cty7U9`9+ylf+MTNPxo~Es%#a#q zkh*D}VtCR=s)>{>qw#Nc+zIe(oCieu{-582{eR~c!>G%HdW}VG`0ZyjgD}};n@<4- z{YlI2izwJo2p=lY)fh1{X&~)RarW@9O^9yyy>|FW!TTO3>xe=zqx2?m2sn_wn+4P0J65SHqrL(g^SQtpZ$)S1rV{C#I!@a8XgjUxCzptsh1 zkL&WG#Fn%Yu^vPI)P8M;DNZDNA^wv@S=~Gr!&e09KBdqTQF;d@k}V#R@7BQ0aay&2 zPB``Rxg~iuH1_5LNG5<>Tb$X6cnG7Jm;so${Icrt5>rI#<%C==R$R|EBXY(GfEvNF7H|>AP!3-wIGmp)pk$jDCF2q zV8g>)7YUafd?gqAol%vB?m>Fz3stNzn9T8K+RuU$G8Nu+4N6e;9}u7~+M?=B;C;s= zv4*%3R(@8`(5qGCZOp-0Co?nziz;=?Xz>ogw%fN*P>UTqo+S_@3$iqy7!5jtpcH&pYVe%FZ)@`FtBm>zs~))5s80UqcmFgZM7$ z_^k2rj@G^Z^)RcOS?PM)@ERv|CdrKI-<_SE^PQ%zR?;E?obB5+!e}ygIKHK{Sm%0F zBv8jO*4^EmXl3NZ$jKQjcy@4aO%3(Vg*^zm%469kOy~0Bwa{dD)Job5zQhh&boXr! zN!}>A85e}0)K#3P5sM0=V#ARBUQcSmZMy)hnZS)cq3VBm7s9SY>$f9KIE}$m(YwP6 zy-yAABk&_Yn&-^kTwEg+cnuM%@7;Y^bN#co3I&!rhvWzsmIgkvK693s&B{}js6o=7}4D(yT zLA*0ta3PIM=bN%CHs9i{E>kHQ$qC*F#AkdB5aczzxwzje`Z8*1Cf4n_o4j3?aWs(M zKv`E|`dyis=)l#uULvEpmUZ5#OZ(vY-3Y#!M;mrUjT!wys`1JTtFvnNn7g$sgHr$U zB(ycB7UW23RLDFH$(-1vVeIy*Cy(2@b=2GC1EM0&Y+3L*Ki$mQVBmb`pacEMaP`A-&WSRT!M7#y1OM>@p1 zn5&V3x8|=5`bpGcNXpVxJHtzv#J<%b>lEci{9|pVbl71Xm?eWp;6Z%*ND{%o$%(8~S zJ$rtJST*SAigjoio&4Q+_c7wQQn-D$W~IcJe(g^e0%X0REw!Gi1|@`i7-^-y1~Y|P z$;uA_414`UHqf!k8LhZ<%e7}HqrX~6nO*Tu{pv7E&I+?Isd*=Rxku4!nLKgYWe_Z1 zGOM^8pHgy)dJ2N2?b9eY>yPdyP1_#tj(^=UaTTF`aD$r-1h1k9aUITS|^b_<6WTjRX2iJZ?la9y9kO7sN!`+vXzaN#AjL;{^iS<`kN=JB$3 z?slfb@Z?3f+ROa`uxC8LJ^B z`eOUin0OiX*kSjaw!dEio03UoDoG!7&7+8k9i<}mvx07dE6yc(Sk~;nwOVJbOxcwt zvb(8{&l#j>&ubbPzPRVpdt?oHhy1V(eryB1lo%jFrC3s0B9iT70j9~XM zRZ;tlrBI1lZ4)7`T+cI@0%?o;uJMwc^<1vEdVnoCd6~MX9r`~>*j%pL3modF!h+f4 zG&PJ+%+`X{cgj7L8t=jEeD?l|6ik+?4;O>6l41(=KjvzMoK5Bl)o&h*YTY4M0MP4s NPFq9&ezh7R;y>T5HH!cM diff --git a/doc/source/mimic/static/navigation.png b/doc/source/mimic/static/navigation.png deleted file mode 100644 index 1e248d4d755d58f2853b3d2b9ffab262ba2580c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^j6kfx!2~2XTwzxL2^0spJ29*~C-V}>;VkfoEM{Qf z76xHPhFNnYfP(BLp1!W^H(1#?)wo;Ux8?$cBuiW)N}Tg^b5rw57@Uhz6H8K46v{J8 zG8EiBeFMT9`NV;W+&oSK!JzF5va%UOMF_{=LUyQn{?j#u3pt+cxkl_0X34dAH}yUGwerZ-)GxITKIalimh2k-^i|&t;uc GLK6TV%tagk diff --git a/doc/source/mimic/static/print.css b/doc/source/mimic/static/print.css deleted file mode 100644 index 715d90ab6..000000000 --- a/doc/source/mimic/static/print.css +++ /dev/null @@ -1,7 +0,0 @@ -@media print { - div.header, div.relnav, #toc { display: none; } - #contentwrapper { padding: 0; margin: 0; border: none; } - body { color: black; background-color: white; } - div.footer { border-top: 1px solid #888; color: #888; margin-top: 1cm; } - div.footer a { text-decoration: none; } -} diff --git a/doc/source/mimic/static/scrolls.css_t b/doc/source/mimic/static/scrolls.css_t deleted file mode 100644 index 9e171b7f0..000000000 --- a/doc/source/mimic/static/scrolls.css_t +++ /dev/null @@ -1,434 +0,0 @@ -/* - * scrolls.css_t - * ~~~~~~~~~~~~~ - * - * Sphinx stylesheet -- scrolls theme. - * - * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -body { - background-color: #222; - margin: 0; - padding: 0; - font-family: 'Georgia', serif; - font-size: 15px; - color: #eee; -} - -div.footer { - color: black; - padding: 8px; - font-size: 11px; - text-align: center; - letter-spacing: 0.5px; -} - -div.header { - margin: 0 -15px 0 -15px; - background: url(headerbg.png) repeat-x; - border-top: 6px solid {{ theme_headerbordercolor }}; -} - -div.relnav { - border-bottom: 1px solid #111; - background: url(navigation.png); - margin: 0 -15px 0 -15px; - padding: 2px 20px 0 28px; - line-height: 25px; - color: #aaa; - font-size: 12px; - text-align: center; -} - -div.relnav a { - color: #eee; - font-weight: bold; - text-decoration: none; -} - -div.relnav a:hover { - text-decoration: underline; -} - -/* -#content { - background-color: white; - color: #111; - border-bottom: 1px solid black; - background: url(watermark.png) center 0; - padding: 0 15px 0 15px; - margin: 0; -} - -h1 { - margin: 0; - padding: 15px 0 0 0; -} - -h1.heading { - margin: 0; - padding: 0; - height: 80px; -} - -h1.heading:hover { - background: #222; -} - -h1.heading a { - background: url({{ logo if logo else 'logo.png' }}) no-repeat center 0; - display: block; - width: 100%; - height: 80px; -} - -h1.heading a:focus { - -moz-outline: none; - outline: none; -} - -h1.heading span { - display: none; -} - -#contentwrapper { - max-width: 680px; - padding: 0 18px 20px 18px; - margin: 0 auto 0 auto; - border-right: 1px solid #eee; - border-left: 1px solid #eee; - background: url(watermark_blur.png) center -114px; -} - - -#contentwrapper h2, -#contentwrapper h2 a { - color: #222; - font-size: 24px; - margin: 20px 0 0 0; -} - -#contentwrapper h3, -#contentwrapper h3 a { - color: {{ theme_subheadlinecolor }}; - font-size: 20px; - margin: 20px 0 0 0; -} -*/ - -table.docutils { - border-collapse: collapse; - /* border: 2px solid #aaa; */ - margin: 5px 52px; -} - -table.docutils td { - padding: 2px; - /* border: 1px solid #ddd; */ -} - -p, li, dd, dt, blockquote { - color: #333; -} - -blockquote { - margin: 10px 0 10px 20px; -} - -/* -p { - line-height: 20px; - margin-bottom: 0; - margin-top: 10px; -} -*/ -hr { - border-top: 1px solid #ccc; - border-bottom: 0; - border-right: 0; - border-left: 0; - margin-bottom: 10px; - margin-top: 20px; -} - -dl { - margin-left: 52px; -} - -li, dt { - margin-top: 5px; -} - -dt { - font-weight: bold; - color: #000; -} - -dd { - line-height: 20px; -} - -th { - text-align: center; - padding: 3px; - background-color: #f2f2f2; -} - -a { - color: {{ theme_linkcolor }}; -} - -a:hover { - color: {{ theme_visitedlinkcolor }}; -} - -pre { - background: #ededed url(metal.png); - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; - padding: 5px 5px; - margin: 0px 52px; - font-size: 13px; - font-family: 'Bitstream Vera Sans Mono', 'Monaco', monospace; -} - -tt { - font-size: 13px; - font-family: 'Bitstream Vera Sans Mono', 'Monaco', monospace; - color: black; - padding: 1px 2px 1px 2px; - /*background-color: #fafafa; */ -} - -a.reference:hover tt { - border-bottom-color: #aaa; -} - -cite { - /* abusing , it's generated by ReST for `x` */ - font-size: 13px; - font-family: 'Bitstream Vera Sans Mono', 'Monaco', monospace; - font-weight: bold; - font-style: normal; -} - -div.admonition { - margin: 0px 52px; - border: 1px solid #ccc; -} - -div.admonition p.admonition-title { - background-color: {{ theme_admonitioncolor }}; - color: white; - font-weight: bold; - font-size: 15px; -} - -div.admonition p.admonition-title a { - color: white!important; -} - -a.headerlink { - color: #B4B4B4!important; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none!important; - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -a.headerlink:hover { - background-color: #B4B4B4; - color: #F0F0F0!important; -} - -table.indextable { - width: 100%; -} - -table.genindextable td { - vertical-align: top; - width: 50%; -} - -table.indextable dl dd { - font-size: 11px; -} - -table.indextable dl dd a { - color: #000; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -table.modindextable { - width: 100%; - border: none; -} - -table.modindextable img.toggler { - margin-right: 10px; -} - -dl.function dt, -dl.class dt, -dl.exception dt, -dl.method dt, -dl.attribute dt { - font-weight: normal; -} - -dt .descname { - font-weight: bold; - margin-right: 4px; -} - -dt .sig-paren { - font-size: larger; -} - -dt .descname, dt .descclassname { - padding: 0; - background: transparent; -} - -dt .descclassname { - margin-left: 2px; -} - -dl dt big { - font-size: 100%; -} - -ul.search { - margin: 10px 0 0 30px; - padding: 0; -} - -ul.search li { - margin: 10px 0 0 0; - padding: 0; -} - -ul.search div.context { - font-size: 12px; - padding: 4px 0 0 20px; - color: #888; -} - -span.highlight { - background-color: #eee; - border: 1px solid #ccc; -} - -div.highlight { - background: none repeat scroll 0% 0% #FFF; -} - -#toc { - margin: 0 -17px 0 -17px; - display: none; -} - -#toc h3 { - float: right; - margin: 5px 5px 0 0; - padding: 0; - font-size: 12px; - color: #777; -} - -#toc h3:hover { - color: #333; - cursor: pointer; -} - -.expandedtoc { - background: #222 url(darkmetal.png); - border-bottom: 1px solid #111; - outline-bottom: 1px solid #000; - padding: 5px; -} - -.expandedtoc h3 { - color: #aaa; - margin: 0!important; -} - -.expandedtoc h3:hover { - color: white!important; -} - -#tod h3:hover { - color: white; -} - -#toc a { - color: #ddd; - text-decoration: none; -} - -#toc a:hover { - color: white; - text-decoration: underline; -} - -#toc ul { - margin: 5px 0 12px 17px; - padding: 0 7px 0 7px; -} - -#toc ul ul { - margin-bottom: 0; -} - -#toc ul li { - margin: 2px 0 0 0; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: 'Georgia', serif; -} - -div.viewcode-block:target { - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; - margin: -1px -5px; - padding: 0 5px; -} diff --git a/doc/source/mimic/static/theme_extras.js b/doc/source/mimic/static/theme_extras.js deleted file mode 100644 index a21bff59b..000000000 --- a/doc/source/mimic/static/theme_extras.js +++ /dev/null @@ -1,26 +0,0 @@ -$(function() { - -/* var - toc = $('#toc').show(), - items = $('#toc > ul').hide(); - - $('#toc h3') - .click(function() { - if (items.is(':visible')) { - items.animate({ - height: 'hide', - opacity: 'hide' - }, 300, function() { - toc.removeClass('expandedtoc'); - }); - } - else { - items.animate({ - height: 'show', - opacity: 'show' - }, 400); - toc.addClass('expandedtoc'); - } - }); -*/ -}); diff --git a/doc/source/mimic/static/watermark.png b/doc/source/mimic/static/watermark.png deleted file mode 100644 index eb1b6be957b2f137a0667b35f43c0d4cb6291cf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107625 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 Ii5my~Kde%1k^lez diff --git a/doc/source/mimic/static/watermark_blur.png b/doc/source/mimic/static/watermark_blur.png deleted file mode 100644 index 563f6cde06f058a9e1dcec895265c0445822a5df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14470 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 C8W-6B diff --git a/doc/source/mimic/theme.conf b/doc/source/mimic/theme.conf deleted file mode 100644 index 4e7800f90..000000000 --- a/doc/source/mimic/theme.conf +++ /dev/null @@ -1,11 +0,0 @@ -[theme] -inherit = basic -stylesheet = scrolls.css -pygments_style = tango - -[options] -headerbordercolor = #1752b4 -subheadlinecolor = #0d306b -linkcolor = #1752b4 -visitedlinkcolor = #444 -admonitioncolor = #28437f From 88953f9b0baa19b4fac3ce99bf24b213114b5b70 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 5 Nov 2015 10:37:45 +0100 Subject: [PATCH 024/509] Merge devel branch --- IM/VirtualMachine.py | 27 +- INSTALL | 2 +- README.md | 10 +- changelog | 1 + connectors/GCE.py | 3 +- connectors/OCCI.py | 195 ++++++++- connectors/OpenNebula.py | 2 +- contextualization/conf-ansible.yml | 12 +- doc/Makefile | 153 -------- doc/source/conf.py | 122 ++++-- doc/source/manual.rst | 12 +- doc/source/mimic/artwork/logo.svg | 107 ----- doc/source/mimic/layout.html | 48 --- doc/source/mimic/static/bg2.jpg | Bin 79203 -> 0 bytes doc/source/mimic/static/darkmetal.png | Bin 44361 -> 0 bytes doc/source/mimic/static/fondobarra2.png | Bin 414 -> 0 bytes doc/source/mimic/static/grycap.css | 259 ------------ doc/source/mimic/static/headerbg.png | Bin 298 -> 0 bytes doc/source/mimic/static/logo.png | Bin 15002 -> 0 bytes doc/source/mimic/static/metal.png | Bin 21543 -> 0 bytes doc/source/mimic/static/navigation.png | Bin 217 -> 0 bytes doc/source/mimic/static/print.css | 7 - doc/source/mimic/static/scrolls.css_t | 434 --------------------- doc/source/mimic/static/theme_extras.js | 26 -- doc/source/mimic/static/watermark.png | Bin 107625 -> 0 bytes doc/source/mimic/static/watermark_blur.png | Bin 14470 -> 0 bytes doc/source/mimic/theme.conf | 11 - 27 files changed, 320 insertions(+), 1111 deletions(-) delete mode 100644 doc/Makefile delete mode 100644 doc/source/mimic/artwork/logo.svg delete mode 100644 doc/source/mimic/layout.html delete mode 100644 doc/source/mimic/static/bg2.jpg delete mode 100644 doc/source/mimic/static/darkmetal.png delete mode 100644 doc/source/mimic/static/fondobarra2.png delete mode 100644 doc/source/mimic/static/grycap.css delete mode 100644 doc/source/mimic/static/headerbg.png delete mode 100644 doc/source/mimic/static/logo.png delete mode 100644 doc/source/mimic/static/metal.png delete mode 100644 doc/source/mimic/static/navigation.png delete mode 100644 doc/source/mimic/static/print.css delete mode 100644 doc/source/mimic/static/scrolls.css_t delete mode 100644 doc/source/mimic/static/theme_extras.js delete mode 100644 doc/source/mimic/static/watermark.png delete mode 100644 doc/source/mimic/static/watermark_blur.png delete mode 100644 doc/source/mimic/theme.conf diff --git a/IM/VirtualMachine.py b/IM/VirtualMachine.py index 0fcc8dc96..0183248be 100644 --- a/IM/VirtualMachine.py +++ b/IM/VirtualMachine.py @@ -434,21 +434,28 @@ def setIps(self,public_ips,private_ips): vm_system = self.info.systems[0] if public_ips and not set(public_ips).issubset(set(private_ips)): - public_net = None + public_nets = [] for net in self.info.networks: if net.isPublic(): - public_net = net - - if public_net is None: + public_nets.append(net) + + if public_nets: + public_net = None + for net in public_nets: + num_net = self.getNumNetworkWithConnection(net.id) + if num_net is not None: + public_net = net + break + + if not public_net: + # There are a public net but it has not been used in this VM + public_net = public_nets[0] + num_net = self.getNumNetworkIfaces() + else: + # There no public net, create one public_net = network.createNetwork("public." + now, True) self.info.networks.append(public_net) num_net = self.getNumNetworkIfaces() - else: - # If there are are public net, get the ID - num_net = self.getNumNetworkWithConnection(public_net.id) - if num_net is None: - # There are a public net but it has not been used in this VM - num_net = self.getNumNetworkIfaces() for public_ip in public_ips: if public_ip not in private_ips: diff --git a/INSTALL b/INSTALL index d62e13684..80335e10e 100644 --- a/INSTALL +++ b/INSTALL @@ -77,7 +77,7 @@ $ mv IM-X.XX /usr/local Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory. -$ ln -s /usr/local/im/scripts/im /etc/init.d +$ ln -s /usr/local/im/scripts/im /etc/init.d/im 1.4 CONFIGURATION diff --git a/README.md b/README.md index e7f0719ff..b220301a4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ However, if you install IM from sources you should install: + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. - To ensure the functionality the following values must be set in the ansible.cfg file: + To ensure the functionality the following values must be set in the ansible.cfg file (usually found in /etc/ansible/): ``` [defaults] @@ -141,6 +141,12 @@ On Debian Systems: $ chkconfig im on ``` +Or for newer systems like ubuntu 14.04: + +``` +$ sysv-rc-conf im on +``` + On RedHat Systems: ``` @@ -199,4 +205,4 @@ How to launch the IM service using docker: ```sh sudo docker run -d -p 8899:8899 --name im grycap/im -``` \ No newline at end of file +``` diff --git a/changelog b/changelog index d83ac252d..6f38c19c4 100644 --- a/changelog +++ b/changelog @@ -153,3 +153,4 @@ IM 1.4.0 * Add IM-USER tag to EC2 instances * Improve the DB serialization * Change Infrastructure ID from int to string: The API changes and the stored data is not compatible with old versions + * Add GetInfrastructureState function diff --git a/connectors/GCE.py b/connectors/GCE.py index 27b1d1095..54b47e370 100644 --- a/connectors/GCE.py +++ b/connectors/GCE.py @@ -148,7 +148,8 @@ def get_net_provider_id(radl): if net: provider_id = net.getValue('provider_id') - break; + if provider_id: + break; # TODO: check that the net exist in GCE return provider_id diff --git a/connectors/OCCI.py b/connectors/OCCI.py index 3799633e3..7c016b6e7 100644 --- a/connectors/OCCI.py +++ b/connectors/OCCI.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import time from ssl import SSLError import json import os @@ -148,11 +149,36 @@ def concreteSystem(self, radl_system, auth_data): return res + def get_attached_volumes(self, occi_res): + """ + Get the attached volumes in VM from the OCCI information returned by the server + """ + # Link: ;rel="http://schemas.ogf.org/occi/infrastructure#storage";self="/link/storagelink/compute_10_disk_0";category="http://schemas.ogf.org/occi/infrastructure#storagelink http://opennebula.org/occi/infrastructure#storagelink";occi.core.id="compute_10_disk_0";occi.core.title="ttylinux - kvm_file0";occi.core.target="/storage/0";occi.core.source="/compute/10";occi.storagelink.deviceid="/dev/hda";occi.storagelink.state="active" + lines = occi_res.split("\n") + res = [] + for l in lines: + if l.find('Link:') != -1 and l.find('/storage/') != -1: + num_link = None + num_storage = None + device = None + parts = l.split(';') + for part in parts: + kv = part.split('=') + if kv[0].strip() == "self": + num_link = kv[1].strip('"') + elif kv[0].strip() == "occi.storagelink.deviceid": + device = kv[1].strip('"') + elif kv[0].strip() == "occi.core.target": + num_storage = kv[1].strip('"') + if num_link and num_storage: + res.append((num_link, num_storage, device)) + return res def get_net_info(self, occi_res): """ Get the net related information about a VM from the OCCI information returned by the server """ + # Link: ;rel="http://schemas.ogf.org/occi/infrastructure#network";self="/link/networkinterface/compute_10_nic_0";category="http://schemas.ogf.org/occi/infrastructure#networkinterface http://schemas.ogf.org/occi/infrastructure/networkinterface#ipnetworkinterface http://opennebula.org/occi/infrastructure#networkinterface";occi.core.id="compute_10_nic_0";occi.core.title="private";occi.core.target="/network/1";occi.core.source="/compute/10";occi.networkinterface.interface="eth0";occi.networkinterface.mac="10:00:00:00:00:05";occi.networkinterface.state="active";occi.networkinterface.address="10.100.1.5";org.opennebula.networkinterface.bridge="br1" lines = occi_res.split("\n") res = [] for l in lines: @@ -362,6 +388,100 @@ def get_cloud_init_data(self, radl): else: return None + def create_volumes(self, system, auth_data): + """ + Attach a the required volumes (in the RADL) to the launched instance + + Arguments: + - instance(:py:class:`boto.ec2.instance`): object to connect to EC2 instance. + - vm(:py:class:`IM.VirtualMachine`): VM information. + """ + volumes = {} + cont = 1 + while system.getValue("disk." + str(cont) + ".size") and system.getValue("disk." + str(cont) + ".device"): + disk_size = system.getFeature("disk." + str(cont) + ".size").getValue('G') + disk_device = system.getValue("disk." + str(cont) + ".device") + # get the last letter and use vd + disk_device = "vd" + disk_device[-1] + system.setValue("disk." + str(cont) + ".device", disk_device) + self.logger.debug("Creating a %d GB volume for the disk %d" % (int(disk_size), cont)) + success, volume_id = self.create_volume(int(disk_size), "im-disk-%d" % cont, auth_data) + if success: + volumes[disk_device] = volume_id + system.setValue("disk." + str(cont) + ".provider_id", volume_id) + cont += 1 + + return volumes + + def create_volume(self, size, name, auth_data): + """ + Creates a volume of the specified data (in GB) + + returns the OCCI ID of the storage object + """ + try: + auth_header = self.get_auth_header(auth_data) + + conn = self.get_http_connection(auth_data) + + conn.putrequest('POST', "/storage/") + if auth_header: + conn.putheader(auth_header.keys()[0], auth_header.values()[0]) + conn.putheader('Accept', 'text/plain') + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Connection', 'close') + + body = 'Category: storage; scheme="http://schemas.ogf.org/occi/infrastructure#"; class="kind"\n' + body += 'X-OCCI-Attribute: occi.core.title="%s"\n' % name + body += 'X-OCCI-Attribute: occi.storage.size=%f\n' % float(size) + + conn.putheader('Content-Length', len(body)) + conn.endheaders(body) + + resp = conn.getresponse() + + output = resp.read() + + self.delete_proxy(conn) + + if resp.status != 201: + return False, resp.reason + "\n" + output + else: + if 'location' in resp.msg.dict: + occi_id = os.path.basename(resp.msg.dict['location']) + else: + occi_id = os.path.basename(output) + return True, occi_id + except Exception, ex: + self.logger.exception("Error creating volume") + return False, str(ex) + + def delete_volume(self, storage_id, auth_data): + auth = self.get_auth_header(auth_data) + headers = {'Accept': 'text/plain', 'Connection':'close'} + if auth: + headers.update(auth) + + self.logger.debug("Delete storage: %s" % storage_id) + + try: + conn = self.get_http_connection(auth_data) + conn.request('DELETE', "/storage/" + storage_id, headers = headers) + resp = conn.getresponse() + self.delete_proxy(conn) + output = str(resp.read()) + if resp.status == 404: + self.logger.debug("It does not exist.") + return (True, "") + elif resp.status != 200: + return (False, "Error deleting the Volume: " + resp.reason + "\n" + output) + else: + self.logger.debug("Successfully deleted") + return (True, "") + except Exception: + self.logger.exception("Error connecting with OCCI server") + return (False, "Error connecting with OCCI server") + def launch(self, inf, radl, requested_radl, num_vm, auth_data): system = radl.systems[0] auth_header = self.get_auth_header(auth_data) @@ -425,7 +545,11 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): instance_scheme = instance_type_uri[0] + "://" + instance_type_uri[1] + instance_type_uri[2] + "#" while i < num_vm: + volumes = {} try: + # First create the volumes + volumes = self.create_volumes(system, auth_data) + conn.putrequest('POST', "/compute/") if auth_header: conn.putheader(auth_header.keys()[0], auth_header.values()[0]) @@ -436,7 +560,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): body = 'Category: compute; scheme="http://schemas.ogf.org/occi/infrastructure#"; class="kind"\n' body += 'Category: ' + os_tpl + '; scheme="' + os_tpl_scheme + '"; class="mixin"\n' body += 'Category: user_data; scheme="http://schemas.openstack.org/compute/instance#"; class="mixin"\n' - #body += 'Category: public_key; scheme="http://schemas.openstack.org/instance/credentials#"; class="mixin"\n' + #body += 'Category: public_key; scheme="http://schemas.openstack.org/instance/credentials#"; class="mixin"\n' if instance_type_uri: body += 'Category: ' + instance_name + '; scheme="' + instance_scheme + '"; class="mixin"\n' @@ -448,6 +572,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): if memory: body += 'X-OCCI-Attribute: occi.compute.memory=' + str(memory) + '\n' + compute_id = "im." + str(int(time.time()*100)) + body += 'X-OCCI-Attribute: occi.core.id="' + compute_id + '"\n' body += 'X-OCCI-Attribute: occi.core.title="' + name + '"\n' # TODO: evaluate to set the hostname defined in the RADL body += 'X-OCCI-Attribute: occi.compute.hostname="' + name + '"\n' @@ -456,6 +582,12 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): #body += 'X-OCCI-Attribute: org.openstack.credentials.publickey.data="ssh-rsa BAA...zxe ==user@host"' body += 'X-OCCI-Attribute: org.openstack.compute.user_data="' + user_data + '"\n' + # Add volume links + for device, volume_id in volumes.iteritems(): + body += 'Link: ;rel="http://schemas.ogf.org/occi/infrastructure#storage";category="http://schemas.ogf.org/occi/infrastructure#storagelink http://opennebula.org/occi/infrastructure#storagelink";occi.core.target="/storage/%s";occi.core.source="/compute/%s";occi.storagelink.deviceid="/dev/%s"\n' % (volume_id, volume_id, compute_id, device) + + self.logger.debug(body) + conn.putheader('Content-Length', len(body)) conn.endheaders(body) @@ -466,6 +598,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): if resp.status != 201: res.append((False, resp.reason + "\n" + output)) + for volume_id in volumes.values(): + self.delete_volume(volume_id, auth_data) else: if 'location' in resp.msg.dict: occi_vm_id = os.path.basename(resp.msg.dict['location']) @@ -481,6 +615,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): except Exception, ex: self.logger.exception("Error connecting with OCCI server") res.append((False, "ERROR: " + str(ex))) + for volume_id in volumes.values(): + self.delete_volume(volume_id, auth_data) i += 1 @@ -488,6 +624,50 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): return res + def get_volume_ids_from_radl(self, system): + volumes = [] + cont = 1 + while system.getValue("disk." + str(cont) + ".size") and system.getValue("disk." + str(cont) + ".device"): + provider_id = system.getValue("disk." + str(cont) + ".provider_id") + if provider_id: + volumes.append(provider_id) + cont += 1 + + return volumes + + def delete_volumes(self, vm, auth_data): + auth = self.get_auth_header(auth_data) + headers = {'Accept': 'text/plain', 'Connection':'close'} + if auth: + headers.update(auth) + + try: + conn = self.get_http_connection(auth_data) + conn.request('GET', "/compute/" + vm.id, headers = headers) + resp = conn.getresponse() + + output = resp.read() + if resp.status == 404: + return (True, "") + elif resp.status != 200: + return (False, resp.reason + "\n" + output) + else: + occi_volumes = self.get_attached_volumes(output) + deleted_vols = [] + for _, num_storage, device in occi_volumes: + if not device.endswith("vda"): + deleted_vols.append(num_storage) + self.delete_volume(num_storage, auth_data) + + # sometime we have created a volume that is not correctly attached to the vm + # check the RADL of the VM to get them + radl_volumes = self.get_volume_ids_from_radl(vm.info.systems[0]) + for num_storage in radl_volumes: + self.delete_volume(num_storage, auth_data) + except Exception, ex: + self.logger.exception("Error deleting volumes") + return (False, "Error deleting volumes " + str(ex)) + def finalize(self, vm, auth_data): auth = self.get_auth_header(auth_data) headers = {'Accept': 'text/plain', 'Connection':'close'} @@ -498,17 +678,18 @@ def finalize(self, vm, auth_data): conn = self.get_http_connection(auth_data) conn.request('DELETE', "/compute/" + vm.id, headers = headers) resp = conn.getresponse() - self.delete_proxy(conn) output = str(resp.read()) - if resp.status == 404: - return (True, vm.id) - elif resp.status != 200: + if resp.status != 200 and resp.status != 404: return (False, "Error removing the VM: " + resp.reason + "\n" + output) - else: - return (True, vm.id) except Exception: self.logger.exception("Error connecting with OCCI server") return (False, "Error connecting with OCCI server") + + # now try to delete the volumes + self.delete_volumes(vm, auth_data) + + self.delete_proxy(conn) + return (True, vm.id) def stop(self, vm, auth_data): diff --git a/connectors/OpenNebula.py b/connectors/OpenNebula.py index b47175952..51bd9e2e8 100644 --- a/connectors/OpenNebula.py +++ b/connectors/OpenNebula.py @@ -773,7 +773,7 @@ def alterVM(self, vm, radl, auth_data): if self.checkResize(): if not radl.systems: - return "" + return (True, "") system = radl.systems[0] cpu = vm.info.systems[0].getValue('cpu.count') diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 809725f42..a0c2dd8cf 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -2,11 +2,21 @@ - hosts: all sudo: yes vars: - ANSIBLE_VERSION: 1.9.2 + ANSIBLE_VERSION: 1.9.4 tasks: - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" + + # Disable IPv6 + - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" + with_items: + - 'net.ipv6.conf.all.disable_ipv6' + - 'net.ipv6.conf.default.disable_ipv6' + - 'net.ipv6.conf.lo.disable_ipv6' + ignore_errors: yes + - command: sysctl -p + ignore_errors: yes - name: Apt-get update apt: update_cache=yes diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index c9ac13450..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,153 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/IM.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/IM.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/IM" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/IM" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/source/conf.py b/doc/source/conf.py index 68534aca8..0d10dae29 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,9 +1,11 @@ +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# IM documentation build configuration file, created by -# sphinx-quickstart on Tue Mar 11 16:49:14 2014. +# IM Documentation documentation build configuration file, created by +# sphinx-quickstart on Tue Sep 22 10:07:54 2015. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its +# containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,28 +13,37 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os + +sys.path.append(os.path.abspath('.')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../..')) - -from IM import __version__ as im_version +#sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.graphviz', - 'sphinx.ext.todo'] +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', +] + +# Math +mathjax_path = "http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" + # The suffix of source filenames. source_suffix = '.rst' @@ -43,18 +54,17 @@ master_doc = 'index' # General information about the project. -project = u'IM' -copyright = u'2014, Miguel Caballer Fernandez' +project = u'IM Documentation' +copyright = u'2015, I3M' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -pos = im_version.rfind('.') -version = im_version[:pos] +version = '1.0' # The full version, including alpha/beta/rc tags. -release = im_version +release = '1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -68,9 +78,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = [] +exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. +# The reST default role (used for this markup: `text`) to use for all +# documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. @@ -90,12 +101,33 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ["../..","."] + +html_theme = 'sphinx_rtd_theme' -# -- Options for HTML output --------------------------------------------------- +html_theme_options = { + # 'sticky_navigation': True # Set to False to disable the sticky nav while scrolling. + # 'logo_only': True, # if we have a html_logo below, this shows /only/ the logo with no title text +} + +html_static_path = ['_static'] + +html_context = { + 'css_files': [ + '_static/theme_overrides.css', # overrides for wide tables in RTD theme + ], + } # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'mimic' +#html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -103,7 +135,7 @@ #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ["."] +#html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -124,7 +156,11 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -145,13 +181,19 @@ #html_domain_indices = True # If false, no index is generated. -#html_use_index = True +html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +html_show_sourcelink = False + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False @@ -168,10 +210,10 @@ #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'IMdoc' +htmlhelp_basename = 'IMDocumentation' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). @@ -185,10 +227,11 @@ } # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'IM.tex', u'IM Documentation', - u'Miguel Caballer Fernandez', 'manual'), + ('index', 'IMDocumentation.tex', u'IM Documentation', + u'I3M', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -212,27 +255,27 @@ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'im', u'IM Documentation', - [u'Miguel Caballer Fernandez'], 1) + ('index', 'imdocumentation', u'IM Documentation', + [u'I3M'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False -# -- Options for Texinfo output ------------------------------------------------ +# -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'IM', u'IM Documentation', - u'Miguel Caballer Fernandez', 'IM', 'One line description of project.', + ('index', 'IMDocumentation', u'IM Documentation', + u'I3M', 'IMDocumentation', 'One line description of project.', 'Miscellaneous'), ] @@ -245,9 +288,8 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' -# -- Options for TODO extension ------------------------------------------------ - -#todo_include_todos = True +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False # -- Extension interface ------------------------------------------------------- @@ -255,3 +297,5 @@ def setup(app): app.add_object_type('confval', 'confval', objname='configuration value', indextemplate='pair: %s; configuration value') + + diff --git a/doc/source/manual.rst b/doc/source/manual.rst index 9330da12e..3c14bbddf 100644 --- a/doc/source/manual.rst +++ b/doc/source/manual.rst @@ -106,10 +106,10 @@ content and move the extracted directory to the installation path (for instance :file:`/usr/local` or :file:`/opt`):: $ tar xvzf IM-0.1.tar.gz - $ sudo chown -R r```````````````````````````````````````````````oot:root IM-0.1.tar.gz + $ sudo chown -R root:root IM-0.1.tar.gz $ sudo mv IM-0.1 /usr/local -Finally you must copy (or link) $IM_PATH//scripts/im file to /etc/init.d directory:: +Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory:: $ sudo ln -s /usr/local/IM-0.1/scripts/im /etc/init.d @@ -129,9 +129,13 @@ If you want the IM Service to be started at boot time, do To do the last step on a Debian based distributions, execute:: + $ sudo sysv-rc-conf im on + +if the package 'sysv-rc-conf' is not available in your distribution, execute:: + $ sudo update-rc.d im start 99 2 3 4 5 . stop 05 0 1 6 . -or the next command on Red Hat based:: +For Red Hat based distributions:: $ sudo chkconfig im on @@ -447,4 +451,4 @@ default configuration. Information about this image can be found here: https://r How to launch the IM service using docker:: - $ sudo docker run -d -p 8899:8899 --name im grycap/im \ No newline at end of file + $ sudo docker run -d -p 8899:8899 --name im grycap/im diff --git a/doc/source/mimic/artwork/logo.svg b/doc/source/mimic/artwork/logo.svg deleted file mode 100644 index 0907a4ea3..000000000 --- a/doc/source/mimic/artwork/logo.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - Project - - diff --git a/doc/source/mimic/layout.html b/doc/source/mimic/layout.html deleted file mode 100644 index 1519ea527..000000000 --- a/doc/source/mimic/layout.html +++ /dev/null @@ -1,48 +0,0 @@ -{# - scrolls/layout.html - ~~~~~~~~~~~~~~~~~~~ - - Sphinx layout template for the scrolls theme, originally written - by Armin Ronacher. - - :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{%- extends "basic/layout.html" %} -{% set script_files = script_files + ['_static/theme_extras.js'] %} -{# % set css_files = css_files + ['_static/print.css'] % #} -{% set css_files = css_files + ['_static/grycap.css'] %} -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %}{% endblock %} -{% block content %} - -{% endblock %} diff --git a/doc/source/mimic/static/bg2.jpg b/doc/source/mimic/static/bg2.jpg deleted file mode 100644 index f61c156f36b61b86ca38275a7ce7689986924e2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79203 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 z z-xF^ni&xZEAbne+EZr7-DnCX(<^nEhA~Ns;FjgrOHvH1)E7uUfhf$t(cuM{MKb;C( zl|joW>=-as)*QRU_F86X)|93)BDF_lNOja8Pdiz_Y-^*yCr|`>-%bT2LZtmMO35y! zxTTqsvQThxgcVjDR^lb`)|)tBK2sYiJ^Rs03b%(ip2nE)F_n4tWyH_4FMj%dWJ5ah z^4!}33&|t+(!4iPy$%)kRAi)IS{YS6#61YcF<#dWGAUil`L>BbLr~Z+)MdN|Ep1va z)Opx{?@sCz#2ih%#)AR8gU_eER(3E9hyOVrMB5YvC9I*TYDZCcq=1)2_wLj63KmZ` z2pAC#jOCF#B?eJC-9YYW%9o4$#X1wX$%I;rUGwlSzs6 zQO#IeM=wOhW!w==al0)v5g+#48$A=*%+3_;)8HSrHx@y+#s?Hx4GTS)@4){*GlId0 zMT}ioO9$RL*(&p0ZR8Dx_DAqB%*2~=1T3Wmwn8RFv8jqF-pz}O0vB1xMT0E5&pb`@ zc6V@IrHnWFCdVCSGJ)kzUiFFB28JGQdmv#UFa^&P!#a`IY^2->YnjHo`u#`v>A4>( z)Ry^D4TtJ~-1(Yu%fd3^C2GG)JrUvI7@*4x!id1W{ms_D8Fv|)q$+tVh|pEitPyNr zK#k3YYg}~mLg8K0?ttd2hCGtgWYiOtWaSA!cP5?tCH_k{CXh8imBDkbPxfQ8x_ofp zq~S*%3S-wsxmcQCYDwq7H#TvOfYpQLdFGvv&kb7@M}>Bl3g{T|u8!BTA6EYf$_Rxw z?K`}F3cnO$;1Zzk?3s^&pA-umfKur2+a^|fT>#|fNUy>FT|+Bd!(Z38!xvdlcRXi^04>CqR4Y0cL_PjpB7poQq zO>ndBMW2y9T=t*OyC|E`A;WSFOpZbBc5=dhan)@kpZaMBkydVLi2)TQ9)Vw@r>1eZ z^0Nuo(ZA9|0s~E{`&6m6WAniW8P`X2!X5GN%3pVryHu1vkGNNxMHuS`AEwPdxv;h* zl;Ar&UeEThgj=&qIhp00-pk&N*W{6g+19>UQE?yG(rT*L>MR>O3YI^y_tM=oH2^jx zBD#sT{3hk|z_R$g{Dn|@GZGC{{Qg@RHy*Jd=|67gxZOr|!{3z~V1tRS$BRA3ZGEg> z#AlI|j(Hd|3oEq>c)txls^f{O6dLO`UUn8rn=}9s zY&O|b85QmAm|~8t;1Lpo4wg0uI=lbTDYET)t)>Zu7;Kw#V|8w4BtznBm$aYu_z%^1 zCW>dYBgzK0Ldo`yP4%AHa0^E^sEv5F^XK$?a%nDmjq)mnRm8mUd^J789uE^oTn2DS zIx*R~+o3GU?pIwRmnN#NV48o_NqOr1+S;^4{d+;p8;>2=PubSF(-ta=<7>IEYXcx- zyJozP@Yk#dTvbSJyXoB?Y)oZ)LC3|^io2>-jtx7OdAOP(}y1Lp=}h#iE^y$!6*L> zI8dE}feRxP6#+u4N5@lniai7#gsz85GC*PH0{{KoyTpfW%4<>AQTXntu{)MbTVpS$ zvGL3yM`Oq9I#e5s?;ZeiU2ih5JW=g3HBu4nL%*FwU7Rp8Z~EByG!d3?Wb-G6^> zuuf=D$5(pz;e5P634284t1pk7WN8>=qwv5;|DXd+1QWleTyS8Qua(cdl=k@huw zuO)sJtzXb31d+xseuJA3I(D@V4*Y(NhFLai@x6ssx zf;9-(DpuFQZ#K})%^wbRwi9^Yz$!zht(bJ(RyT37QAI|(H;hAZdHfJ;XW;#|W*Jflq-rk*Kto+*&2iD5X)pP-*iiMA z?OCrcYWfmAKYo1=+VvNC^D+sw9QF9k>_}dHPX&kXc>TB-%)OXJ+FxK}P~G>cRds?A|CE)FE_+1m)q0*)#pyCis~Oevw9e4zM0RWc&xb5@+W1j&J24? zS&2~iz&z*-$O>r4^SHTY@^~5ULtUBbwT`2RoqJHC4%6fB3U|UCns@yNK=aF#Vq#|g zsxz}J;h(40P>wP*5eYfyw__TSpzUSqI@w1eK{vtAW)5^jd6GP-Wv~X*mq+0HIYFP- zJ`aio-~D+NW;hWQR7!k#%T6|Fs_W{Wu3XZwY8B5M7&G{jZeuyqhkf?h5R%e#yfk-e zq^rYDhV`>eLvGIPi~$XZ0(grPI!%x*kfTZMF|Tf;cuUE=MjNafzni+H%`h{XH~tj$k=%wDRrr;=8@0#xYW6npo;MGfWG6K z8+bdVV*NqSrSBe?k-D*$@V*Of#M_k#KHNiHzU5vDO{o3A%C9XV;!)nrBZh^MZE{>+ zw=`7{UJzAz2WK`E8C~g6=5rmWN~Q})L84J^VCWqeAMwj>4al`D%MiDL*P=JQDil(L zywzVDZ)zHFS{j4&snnD?LV5P{O8Rn7D`om&_r`aHO|=h$`X%yKCGzNHI_hiFW(>Is zCX`8$07fvTuaD6~Pz#)!B!PE&RInl#L4qm|nh4Y(8d?O+ zF)@NbK@qgQL2R)I1u+8#5yBvdAXUIf42S`wVi-iKEkgk@K|>U5i9{lTVvPfXsGz9P zK!vN^)~R>xV0+ts@4oN-`}l*i_sKqIPiL>a_FBKS)+DvK2MeT#oH3K!==o$$^n8lQ z;zD*HMa)!JNlZL}P(;tyh=>F1H4G=s7-ufQESA&a{b{j?C`(V_H^vt6f-(kL9U~+y zU+lM^(m$l~{f{CT6!dj}&Fvo9e z1NNTC>Rs~=Blk2lva?9{;( zg})JK`iH6Q^k(q6%OFI?+fto>)BT;J1KA6mu3-6r zVcK;8zeb0H#(PgSqwQ?lb9a-*nrOwgFy8?Epi7?0I=>*;L7i?TygTXTtFr>EnO~xL z{T;~mHnQV<+6>Q!#DXROK>J+z*~_Wn!S&I8y<5XgZ=XA@9!fYr*a(~X2i{y;+;NtZ0X8SGR*rao2^v}E#Yd-AQk%2Q z49lDRGimN~UeGzS$b|6YLDG8y7kb~3teZ*1&3O!mqM;a_otlg3!&&xlr z)NH5e_WD&^Tx>1%mq{x|+bUV%HyBH}`5zp!scnz%li~9m>G(3V_NY9CuEDa<#HFH=?c`GmpzPxc)rG4zhDKWE(hs%hluDECeT zc7);Io>SrUJhUw*KRjkELZXT7@G9@@t{)?3ug0Da*Op(^?EFBgqQe@U-%%Zzn8?&9 zr*m#--Z;#gn9gQ|hkx1@8ER{ ztHHKNw^i;&KD3HAO4lH3c0?b1LP7$fA`*Q8NtHq!g% z1fri$S{ebE_5i@q#nBc7iubsat9^V=9-FJERSh~57bJmAXbJ8)Kbj2w($#773xq?< zh!*$(Iu|YcV>O6XAP^`LVj6*fY2MQ8N36^4emgIg>&dq%Z6++c>TeN#d_v6e&nJC9 zZAxuhfobHnX0sjJ88q)L9fcN=5ZsO|xO2HZo@>=`vhLfNR#HRh_buiv)A)_M$dlCc zs;7?HINWNz@5VTB$F?dd1KVDt*t(S$zsL3at-dSrT%x$nGGVx#_lCN$il`H~6XrAj z!XfakN3@^D1KOa8-476U9-JGYk5mz67E>|rY`4CRSCM7S~qe6oRuxW6A(_* zvEyw-(bM_cIL(wLoM!yYJj`suJ$3n8HvQSpv2AhUi5O_myZUii}dAy8#R)nH4M4fjq$9_&Daoh0?^ z<-Z0Dy4oMoS&4Zw>(2m8jS4Obx88r=m*0iEMElmOl}W^~c26SVCmA?c+{SaEbBG*u zS|t4@ha#cP^<)q+ChTe^GCJX9@)BLlP@A4$o@F*oX!urQ7K>5DX+p#8LZZ;H%)XKi z8&Fzdo%yYOJ{+8kzxr&8m6e#iIf+irrjojE$c|q4%nH-)_$2$3&h{BMi5?+QXTbO4 za=(#hXU6uKuc=w#%IO6+k2eUG#+3u15LCmdgmT!VO^Z{ob{@edf6m1Wo@+G|EF!Yr zZh9QD7W?Sn7KVLQ+7fkRDvwU@`|%{_uKx_;fU8Vu(2O{}ROR`fR;}BT?#Wdq<(jCZ zRu1f35Ank!0bArr++5Yyx5B&+5^Ed(?A2vi5QSON?{vi*_TX5eg*gW(sWZJ+0oV-Cb}b_`BWVw2rJU|yZ~dTm zvPmb%o--a?ZZ@9HtXI*WP(z;>z_r7S}C1RByS5Mv)F5(@UL8F~6y@ zQX2_KBIGpteIY1J4tpCQM-GQDqGrh+iKYT!9KdZGw(687GKu?)j%ItkUv=fwq=URK z_Qw;QI?PJ3<-ERP^pyNo)Ut>wDfL8)Fa=HoU`yU|f(pTGT!aM|1RG(&TAc<>t7Bnq zYZPrB)?2_Kx0Nv0hqW`DTFj&UpL9Pt(x>){<*eVjedU;Ta&PF&Ag#+2?fQYHBHo0{ zDb+E2c5F9zbHBf+I6OaU^sFo|P#V;5cA{qYlDl6FmHO7Vl~ZrXa#}YcocRNt?a1tG zW3H3dJ(!?%TYaBK9XSl8l*vGAAa`+CuS{lj(qxY$ufyOadPOokDSM=FEv7b>w>{`~ zE8>@497gXBmj3fC;4@H78`wLJKI1q2Mo80o1n@j43B7L69MGBlsTDDgdtYwPN_7G# zrR^c*?M2RFK&CCf<H=OpxY z%iijlnkm|UeUdYr``@pqt+`sq@uAptE+4vO5t5?Z@sAUj%hN{jHEwW9TSL={V{yc} zDZb_9b2JN)z5l9VL$pCZ*f!W!Jq7{0M;3PXsiXkdx7e@V8v?PtO-LuZn#eP4m7UXs zr389ao|kd=T3h14MB?I{uv zUfp_8VYhcpO(LB(U-*q`CP_3|%eULR7F%cs=SuQY)y!!NF^qHb9!b(4IDFgTH0F8t zbAq@bs(#vsgxUYtu|d2nYLCs0##F)GuFRgkd`&f}^CgZ#0)oL)UlP5jGHXX*hn7wa zlJu7mAOI9oz|V#M1j6qvXU3`bZK`9R+bwSAYNCUp7&j6fjps|B#SIu$a(Fm;>8b2gu*ipGldjmFK*UT(UaM zFx*5ok76w*n%RPH>-WJhlMUb!8{cYmdWe=yHW#w$h_G#xa^*q4hGIl2R}On9Va|ag zw?wALjTr{1WyN@{#&@HaH=6_0qYSdxi$UWB8b*X@Y8YH0UZH)#z|7No27rqA_K@?+ z_{4OGe{4@uOZq)|akgYB7-{Eq)bpNkHh3&b78{^i!bAe$Y6#~lF7j-(Fe5Xuzb2hU zmRCKuETWrGB)EmijBM_r0*UMe!*&nt&JoI)6!=mq2a(VF3gkHFXjET!LCN{CPW-I} zIPKhPYos+NV3Hv8<)P&G4k9&%Z`b=|rx@`{n*o!uUxEUDB)!$m?javQe3ZzP4;yac zJuOFb|5dPbNS&t1Qm5k|l$4fKVty5TU441@2*X37N{2`Iue@xv?9@mz+cz%mF6(IY z02oHp+1>lfRC$3w&EUrT0BfZ;kcqI~dAy*^*Be0c3RM-uV(N_*g7;>No^ue`G_E6I z^i}Dmcz@EV@w^TA141j%$_gDH<)9S-xgV_4g;wpXw=EQqjMYVL6H0w?GH?^S{MN08 zwkbvg4^u6*?$D7V43GWpUf}l>K=tH@%lWU^sayCmea;a6<{LzDuM_74V3V>D?M0rX z!TA_EtLh71vpWZ%d4?6{2>9f#$&D4_=5i}Ed)>P*-tCJ78U@)U>&mbe+uL0>m2pDFjd@xGm_l|DImA^**ck%UNM-?{+tg zmFcV+#?a%<<{`!#-)zTSVJqknvDe=1O}r&e&+L80x&%3ZTZ;TX0w)~nA_iTd2hX|X z_M~5mH?IJVo;tH%hb?sjtCwon6_$Qg5hZ{3D`H$VyJ_8UX5`_sGnMG{b3g&>b}3HI zxgeDRfJ$~!XX&JZB@HiCi#Cu@r*xMR+m!2PC8Jv9wksq97RCmQCvm9W!3gm|osOzR z^%ZaSRhLuYvZCT^G!W2lW5zg+V5XH|64q?~adWg8nZQ{_G$Sbk^wRF!Rm#%ND0*SJaYyurjOm4`0%QDS;@H|ruFs7Y{G@7!z~ zv@Fz(C}}Nu&0ak~&T99dXC%Xu{!_6r3DbPlH^j46e!JK17JH4H2@yWdi`d{q@Ruel>i<9eZ)pugUUyo z{)|jVG9l1E6nj~TVBE}&NlIqFEZ~0_?McR7JUl@~Lr)u@fDS+Gyb_F4PYUcuaF6|o z#a@)m*KEImK6r1x>ga_=n6DPRTZbAK&yEK!_Xutz?PPkBpQCXCk=K=nvWJT!``oD)%f`(ft2Vo`Fc z(Fp}OeLGhRqk+#>zcvm}JbU#stuLCdzuvnr#M%?rgC)X;Rm6MmT>q$gGRCwB~eG8TxNsgnz=Qz9fKi#&l zk}|^GNCKp}rt7_#u97k%-0i~;NCNmj|L6a%wfN`%hzq~}jeq{-`uhjO@BhJ{zy8m! zU%>A_1{Qw*`QKmR_lJM~4gUTxe*XpV^KpOw>d%jZe}DJ$pX>WqKi{?1uV4T9eDKd- ztv{c0fByRU^WWe2^RZYzfBXBNUw=Mu{e0%HU!O<)^Pdc_bARLgx6d>DeX8wi{(QpE z%U!tE=L>({a{YZv{5;jqm;QVv|9%Ai{?V^rzpm>FpZT*K-!ENjTcE$6{aKd#Gr!Bu z?_c9ybNu|y=c|8yApG<9;qmhd+iKrGM?R16Eb`ya{Pp+muFrz}d6=JF`~AvW&S?feFlDh?fbj@ z`S+i{{ryp&BlSJu`}O(RzU^}TdF!7~y6@5F7x(}Ce*V2mKil#5oc#LiF2c9`y@S7g z9{&50-vjyiH2&xR{pa^`{VeHcg+AA7-COhBO0N4yzGmV2yJ8p9k+1*u{Igv=gg;*F zfBN|q?!UG2e|%0qE2T|LFZVevE{E z`t6Y*-sM>x`^sOxV)u`EG50^`$C$Bs%T-pAX7}x`wu|+~wfFFGjpCV$JiP<$DNeMrf{0 zH4^P&|M{cKEa|>LY2R+o2;TpR?b#?^W_tjt=mO0-0#l)2(nlqVI%9k zU~|lAmXYxg@IF1%R%7kU#jt%WwjTN5!fzF(JBsw%+)q2cEXT;`4&k9!ER{ z<&!}_DYkcH-COnH&?d_{GphUYJI->;sQw*wd5c%7{<~WCKM^5=7bB%zfLR6Qt5*)` z5&w>EE%DvmxYv1e;a-j0O!bo?ZEqKS9iK#ZNk6{dW5$zw5I$fJNVm;Kwt2~``44?m@oWNDdA1>)}I{8gWA`Fu9vCe&HIurs3AY7WE{%d(HZ#pnORl> zttEy_SMj(qPC*9r4LQDE6w!%{apXbIp|)_CH{yx2y}nk9dK}-J5=a0cj5xAA%OrBl z%fS+3XR96qIQTf4$=&hUAleL(mAK zRQwf7QiX^|t=&bYXGFzPP&21P6f9?+WcPw-GM(xNkkT%=fCh4#KATT!2qb(n+9yTz z&MbC}^$}_WtdOL%yb$&WJB~~PnEaHW$aVkAwp{67Sdy>(T>*Lit_Bc859&FG!WmpB zo01~Z1E@56Jb2jc=L_<#!6SXaEp4}ukEPtBqi1PZ;Gq@<21ZL!oy23s7DX)lgtB3@g%sxSi`cVpZ1KcfpYeM~X2G>FY*Oq4wXGM9BNXRHlaE5BhHm3`h5Z~*? z0uI}t%k-Ba0}Oju!6nee{ZTl2l)t}_OMn&?EefB7Z7_k zh;T6&qV=lhxinsp1`g;iA_G)4Wb4&%ps{nJj)2ZwIq2km8>l!khecEiIc?EqBM~j3 zN!XT!JL}%k_s?LtehQNXVmWx$ZvbPjLU=!_(kGuSBdIR@-k0R;4oU_|Z-tu@Gra|y z^C~RU^of^|Sb#FP7G`447Yb-`EEDGP*R~zplM&Hl<`=BG$gbQ zNQa4gBRHwiJa(;Yx}9_(6;)ZU{8QDWviWsma>kiX{Wqt<(Le*+As1 zCuWHO{uRhgog3lFCnH31V#+YrlkC+Fbk5grP5d5Bzfa z+!k!1cg`(VZfC#Y!r1&uS$5vvJ7SSIM$*LUTp@RorPl-}xsDP&d9z*&o5gxRZ5;=~ z53lclv;vd;4ne%ch_GQ6k1^v{TPw0p&dtD~FPFq7P^k7uthVJG0-Wxnr?ph!mMb+Y!JVS6eR*dPSZnSh z@$pA}SC050d>yT%O^l$jC|A@dF87L2cGAd9sO|>kS(cSNKJ_qIm%|d2)rHF*@zcqO zb5uHh7`N1$`lnMVhzcve)G5|Qi!M~*>dFtX!&#qcM)nvqQT;*W2<2WNGcp&*p$0v= zAC0wmg4Nw%jmxr01n=>&&KKR8h{!WtvVfY+iR{BsJqVZ}v`bg_1=muX%`Dvwpw4Pz zfWyTM&OLypuP0@ykf%Y7!ZSPzi-)8p{Y!WG4r*MR;Xz3m%1ue5zP2NV)z9)cC2M{F zIYhx+Rr#C=>Ox5jTL1pP%cj!o&ZjfpddhYjc#F{AK4WP*gj~loParGbdiMC<0yJ`s zphB=dvHb@Qs6gM~&$?^gm7R3X9F{~nXZ5WHBYz(_2D99KY@FB^p(Pu*y}MvhaY!SHNfj6WV*i)9sE3VK8&&CU>sPX9@-R@ zGDD`?+fb}y^RCE6J@mLhK0d?GAtXYAORFs%4wyvTBgtvB6>2GcQ@539QqI`192c%{ z!;W(T(2QGndJk!AjSLUjc(!DGW3zeVxUhOF-@&_`vBo-`wU4LeS}vu`<%lCFhz4=M zA^6z96E22#YtA-bTzZhyL{0EnFjLAz$v;;tbOn%nG$C^ex{+2)j8^LHn}#q$Z$>eg zAuI~hLo<3JD5FJ%s7OD`jMhY7F*mO=UP~8eXl6HuTTqft0ikP#Vj%2Y6s*js3OGyW zf}dQVUVF-xdr4*U?X01#rBwJWEAkW8Eh#t5xt06z2 zFnkbZZkn#z5JK$O6JzvZs-~y|-8QXo-F)sHc4mkGY%pbK!Hd&{vUFCdJ>WN$^8xZf z1>!UNTU~u-q6RZboLgwpVnZQwU!{@|qW7ou9b4~&LXV!v=!<(H;rT#!<08Xs<3uto zE@bhV=`4kL=n%PZF??gLp>Y`|SY4y3V=$;)qmgG=CKQFR;u?=;+GETX>?i>n;S(13 z#${)2@)oFxWXwsjJlm2G00ksx4u~;91Y*QBB}z0ovWNI>?1=7oH6?QPI344adxo^x z)ei2It$}Z=dyXWe+Z3TDU11?p_4^*}VwnHi=HgtF7gt9l9ct47g-U-xQo=GG!&0`$;^NH75Q^{bOjqZvaISc`t%rBV$j<3+cd5{o+ z&U@*xh5*{gOZYTLrP1K!(_r{VN9zijly&=mj#dqOHjs4+3MH9kEBuNC?9SO5IYR^VSqk$SAV4E_B zrUh`M?_!lDa|jiK+v=jt56K$Q!p^Zyj%@%8TWbWt?I_P!YgX6l1SxIs8JYRN(|(GG9h zepPVLlq7HvY^fNfBfKJeVEcm~p(Fwla4MlCr#*8y)zq}?clpalh_l7&w6`v! z0EjZN1G=5qOJ-E>yWO|BYR;wmVJShS)a;e;&9;RB>YSp&U6+Yn61@*AO;uCqb@0zAPGFeGmiZTo|puX-BM&qf|!SR7t4{CTrH- zV3roZQ7f!SA!&_YD*fO^odMGL@r1*C>uAFPrXdRdLhi}2_QCHWPY<&a1*$_724d?; zW*~OVxf7$nAHaHfaX`uU7&^N z7%n$U1;uqfgEhK@K^?KK-g-8~F({@Q)w+5AJrM@1js$d@b{#xrJ5rV1Q)!4|G=3$t z$0IA8Bi>O?G@_d0bpcWyv}9DEJGJ?(rlFZ{EOaa3Be`mK>XcADfYp;EXDW7}obU!k zLWTigRM`r=K}(Y-e*-E)5d(l|`#0Fc;VZ`B?x+^1GvjZxAp538Q2Sd~7VqsRkO68N z58F3?Us1%tdPuLmmL`MA4*hECEQkD)joC)q(CUIucLM3o6s0^(E%8FcZ|p*;5G$9U z2W#(0CM9bBqEWbzL^HkQ5ppa28FG~%hg&{3raq2<2fs3(K^$@m{b_n;i7q%T*{VZC z7k2dhE)t|>D=IsTIuXunvZw$S_Q1r_y^(MzEBl$2CPApzeCvNiBcqe+Ls8I2QjXFk zEDRI53@(>GghnRDNG{=WkIjj6KjUufaZ9NGII?;Ic{AW4PByacC%ZkHI$M2CKWBVn zPF>_JBd7Ul8kirpUq=XqR7Gc#hiUSSeSw>C8|0rn1tisC1M3mxvmI|CoSYxvw6YHW~pPy?az)2DJR%M||YWMXyW0$i0}obynaZ}vkBrN#M1Vr#I^ zmh|m9QdHX2Y_o^)Z*n$G-TR&>t z2<0dYyX&#tQ=aWUJo33jq(g^CJS)S^nIVOVm|{%`i;g&lx#hipvVD{&Mdek((rF&E-&JRvJVqr9gW3XvRR~zS{ZzwyLxGSbq3d}qX zK1a!E!jn2X!z)|*B04ymqMku4R_)lHXp7El0yWa5LA1B+ryo;CL!GINu;&usR)9;i zl1Y~rnWw3Q8LrEj6WW2P{1K$9kV_#7JrXel1_{)VhY$d1}<5r2bs5P zxsxP7guqh$vYjG-EW?vzQpIY_{}F`*QdCHxPgz=*Sbl(cxAaN)^uh68Y>e+^rGqq< zj9ux8`f*|%np2OWhkr3UiRpl=5kJ_O-ftt9EUAK4JQx$Yu7G?UMDV1f3z zUgVc_({tnO6E#gbMDxR|p4?De;%sXa1h)(z2??B0SKsyy1uH5^sgQ;odvWWa#lUR` ziEFZLVW)0mbO`xl(})QJE$w^^Oz?02IcpfD31s9oJhf}|@MZa;Yun#6B4;HHG6RaI z+L3}PY|`qzp&qeqgX!F%h(<;^svI+88P^>(uYw7}&JIr=)DEJ`6oU`7=_9*13E3g1 zgACzdk(m+V=EebE4u=E+VN^2@yTle*>l3@kN+{dYVvF^T^47@7Q87L3u{ZBu9HToP59e+v#xM~Wt zDkfH+>T*xbL)_UFcs4Tp&0evEZyc&rGTYHh@F0C=1nD$*?U5&~Ei4En$Y(8_po)Z#C5Okbz`seIMIllVaA4OEw*A?)qW2uWgh zShYdXdWbU8qwcJteMuZ$=z7esRx4V94$tQQrpf1Hu;3nWF5B}>oyAy&*FoyqVxMyO zVKmBBeu0xU7R*03cFuh%C%R56lzwn(#a_nTpaAdV%fV9uYg#S@HbmOXc;8+nOA4Df z?J+MB$GOUg26Z`02R1eA(_w&nq?T3aP?&|e*?sKgzvab`#{sd-gX1sh7b8*x$4%H#u z|F__U;Dc=)`PK5ft10lOz!fejWAPv69CI>8=$ssl|93}MIA%IZ0OU^gezd4}#}^aI zD^s3Y@Li$agpgab}>CE}o^2E{;OFIrK>qbV`(vYjs>4uE@v4l`UZ&H!y7K zhZ`or%St6IFNC`BtEj(UZj3h<(Y$4LUZ7%XyM?*28!mT9&m3nhzf~01>?TA;UQn=WL$H%CAZub-wV@Efl#(YpeLf3FY?5b6+;>w za-HfTq&k+KKGGr3deIkn7IGQH!r-FnFJOv>!ChR8gQUSEK!oKd>t{~oR&bp(K4a!HGo2&}6X(avlSg z3+3W+Pr2Cbj#?2~pF7?A_aM6*Sk3&Ggv>9W)jh&m2~I ztE3O6=cwt}7IG6HuLieb4xg3cSywbY8Yua^DH1}sPut7l=!aV=$echSL=|0Sdvm$T zu%#?D(Xsfnkj>$sch-mS0UG}F^d_*_OMloiA^=To*Kpz?n`1d{%^UK{gq&gZm^ADV zG#7)=9wnr*O9^7rHVqu?Z{@WVg6R>-dY~1iQ=>vg$goXIe0#(&4I}Cnc3N=@vg?oGUsnM13z&Ge9l>7MDtT)frknr#%*G8qHzQwf$ zZ8e@|b@1j0CT)0PA)i;23&eDh8KO@G$A!LmVt z!>=0#*jshX@@vU>xLY>7)Gfpo^yN{4i`K7~@wRJC#@6*qz`v}Tck?N-j5yfkD2nN- zatA|NLuM*R=55-Ddtni0_M$zK3l{AyIEOFZkdG>`Jjj%s&sAeUD@V`7d@X4Vy**b? zR>hKKz6eyYTL~J3js;kumN7Rs8)CS+Nz&772GD&O4AaZri@{@DebV(RWYk{n!rTfg zBZb$&z-1x*sj-U_Bw|D+pP>s%{D2ljhn%MkLtyMz{5Dx4Gua}_hBERAg|O{?2wBTKN{{P7a{PvkjQy()BQhu@1U+aLm5hwep5*!Q#|$z8_;~ zblzWS(?_&$AE|8A5fSW=!PZ2{L=B{*A3Ux30 zDl6CWLQ(QKA3MX~*?Go!xS_{1(zA$gAM~S1lC6QJ6B`bl_jb#6dYQVDb`$~oB)%V< zG+}8`XczzR1m8K)n>9yB=8S)B(uKYH3p5xGbR-uQAefo}VL{xZOU$SJZ=>Bf??{jO z=~NE7bZ7F!mZ6+19DsGt+c((jvf`w$nUYPbC;?))#KL|qLisKU&~u$9yOc0VZ&Q0v zboh5DW&p;RK{NwONlG-fM5+D3Kmm6Jjyt~#yUy5ZxthY7Djy}fH6BzEl5>DFq-6RLm!9YxM@Qb#@+l0$3^pJ>ll9ok}2cE!;U(0O3M zx5F~SEm4^bl{*xNf*A@NVwW<-I5qzCJ`7Vc?yCL;G2+OmpiT2!W-jIQtP&ZQmZN z4egNNK-+lvu_&mDJF}o$a|}&Vz;JF7RWl^XG7q>KIa1Zm)`@37nRgHk!>!;NnCt>O zYsJY`bO>A~rpl#uRpXoe*fzOxP&%e=UPl9!&wZjH2;mYGzPVbmb$q!U%Pu|^f)LFI zBo47E_h87psRmGxDOb?WGv}m>+bLpu3y$&aQWsGt?aFV+y05N<34`2>J%!`}zZ)~r z{qa?t8&{|sJ8AvB>l4x71R%jMj`qUC@eb;%4dvZj6vV}Aflo*DO^~rRyVdJ3h zibDBO6Tfk3V7kp{phDuav_m#M8d3Lgdg{1pjw zn3k@P7Iiv`u-1BtgXYa-9&8c+n+TiTyfF?_J7LK&s^(Ogl zL{Li&9o-O(v1;g|V9GELA|YUYrf+vnMC0PDCTe=1?LSl;mqJbopb3GYhxIxl?=$6j zzB_RfYyNJBU;yPM2@D+|g(zG8@f++M2G377T;_X#SF~wd#tA#2?I&a0Lh|a@=97bnBIMcmKj|TjqH~;8^MJl{FVJRUKj+6w{L&&W5Y6(NO3&!dp&9WNQ zk_(hS-1ZV+v*Irp@B}D;W>)p6i17jy`5gCa^W~#V9olw44Oj3hAiXQdX^xYZ_!7z8icz$+RC7CIROW!G|1sZu)@8Z z`~EI>YqgtM({uf}?`@{d*Yo{#K|hQ)C|xG|O83Fqjp>QjMsJ z-Z|#l|C!9ia=6Qm>tbnA=+Du`n7VTh%OR*Jy9i}$G+nV-fH~Mu>DUkhcr#0P7m(lr z1x~FXuJOtG_|4Qw+M3I*VZK!a0XLR`gn}_>l-X*t2zpvpko#6LcN84-=jeJpzGqnw z`9C9WA;)4*$Gy9F9z2g(3OK2Hpi55PqAo*d<;+<3<8rcj?EGok9NEkizntI_tPKCA zl{<{>VUFH%p!bgXdX7`3EW3qN(3m|Pmx)t}!V9^hVfafxl*R!WT=KHiJlWpJD5iBj z(cl6bnfi0Zo>yRm@My7_H8&BjDbX>|yTB1)5H)vS9M1^7REj->OE0iQ_KZu(E&M8#tya0<%QTbl%r@wnlx;U;aNcjPV0qxYF24uH=7FU zcSI=42vKoX-S=t75f?S4rUmTU*p%&_lO^?Qk z<>=DJNt`6HsG+SE1iz2hg+AvRvZ6R`$+4BEsh6@04N~_kP=ZtT(l-(e)Qxz?w$NR0 zx<98`yrbrV#)^%E2#Aq_pORpU?5XwJ`x1n;MAS@;UIKPAw-_B6x)6~KR4|PYhpxb3 zE39fNT-@G=#}xzUG5}3frB9JJw^&&npuqv?ZYM48*g;s(UT0!J-jFt3qz~`Ix@&*A zHihDHXXYU4L5sPIOU+@v3-hJ!sW1?mKh``~S3?<~xc*vNtCN(jofGRFgTfS99Gz<_ zfM&bhT*)Ir-@TKEP+Fb$x@jZ>$Ujp329~sqPeZ`dM{dL>YD+T$#*}*7_8bCo|3waA z2N$8zrw3!hx8sU?Sb)?yh7tw#CmDwZL-Fgg%Qzw0Up&mCKF_8OD4;uSL6r%gF<>_O z`S79ZW2T{KGu+ubFn5mOi%*!{TyWj7gWdcI7x04;bWSpr;M3T6G)G!!{i^Qdlj zK}J~cI3ql9=cQEoNHm+3M_~5J!L~_=LmUl*C8osY?0B{rB+W6bq`vI@4ZG8Qr-T!K z=TmqB5?CFNJLj;7aS5dywQo``U5w8o(`&wne)mtOFP!+#POjAPE(>`v2qGsOw8pT@ z21QuD3wjx`z!!ADSaY>dP6ceCIn$`k7FR&Fk#XS5oQyvE-*YnGETLEM?+J z2_Z)WpJxkb&ndvPnHMs>2ghl5H8FHgx}k{6w$18rV+#sEtvy95xSIWlUW0qPVM-t|w`OL7UY ze0tFnm|vKmcX&B;lFTq0f+?eaO>$@|KG8p=^`lK~icA zl!=LNG92bR0Tcy>BFNDc36<89(38T>>XkSm*rT-cCzY{C@vROOiqr<_5Qk6GnGB|g zl8=Kn=>%Zaqi}?Fc<##(VYxXD3fBnJYXh0WDHbdPtoGxZs4;y}c>^=@>Fp;>q{y8M zDbAw}WbSqBiv}D5MO=_w{XR3u17{)cvf-uCId?Ns0rhn9^ht4I(-rEv@@Vsl$9Zs* zF6zeEp^nl|EKFBo*JIcCmW-L=@eIu{0BEAW0jbRjP_ip!j7*aP9gX2g)^a4ZIyp@6 zs(pyso|35*&2fcm6f2T5Uplmf#!VS`fn<2kjZe}QDMDv)e7 z*A;Pu_n?31ONP&h4%1-hw4!#1p)e>SVKGwQlfA0%6iu;XNx#f!)aQmmjZ+T|L2mJW zZ3ObQ(I<2y>F2kk^bB3oDe33djM$RS@(K~qL@=f{p;sTJzXl1^pwwz&<|&yor9ho~ zpaI^80H$U@Q5)+lY@0$0k%?0nYfvn@?{3^Lm`H6&2&p`tp;;-fhA=uHy5OivC)o7_ z1yQmBxm0fOrFjhy6tu7{G}HhHd=A!4kUEj=?#RGZclh5GVCJ&=t#?Fm&f^RJuUAXf zeTGuCK9T3i_EAM2qNoBv++ESrVYQGF{&&<-M+OMaP;fSD9C0(;0TDbFElp{K1C(XS zq@$MqcqYz1(WvLip%i>pbU&jZYQ_6b@39(vZ=4-~YRd0x-D>t>&#H#`3rtVFg`y>i za{l@O&JX%1l-7>Fo1aE~gy=w1&n}>YkMk z=^}r2;z&CN$9|V_=n{2W)jQtPx5kiW()g*zA27E>V0LQFAy4*zm3uU2QpjD0Z1rUo z39F<>m#XQZa!Ku>7zF?Pa$u7?chL3GeeAOQ@Wnl;t?fd_VP=s!1mhSd66kKS42lG~ zqCcFR==+1`{}N4l>qlI(iU1-pLYXq9jGCYe33b1zTnq#w4LvV0#xbwA%S%a$5Fd_S z6f=VzY;gx@=bvYLu1R6_$*gb!O$1X;76cV5l0Iyy3fJ97>cT`UmxDEd1S8GLPTs$_ zMpC3QP=TQuspk+7w15^Y%pH(eo-0tBG~=F=^o-t3^v${k1*+}KCQMn}Z)^2G*Z<(bZJdHl#RBO=blJU6CDE8lL&%1RgfQCsH3Y|?!gP{+`$_A- zgG)KlyC*bsPm@?%uq7f_8#*+R-4;`Q=<{6HZ~ykUVtbVnk2fd+20`r8r9&aFv#Q%Q!y96>%#_mci`8?} zp@D3`9ds=+Y=x-UYahPCDFVdCAx=6jxD+0Nu1lbhHS!D4s7lE(bu{I##55oaZ^1W9 z!rU6JT=?8{#8Tc!dXR)PtrDzI zmK(0^uV24@a{S9vxWEp-vBP|QjmR#H|DNPOPAi&fhW8^~gfzSvi6>CA5c(khoME*H z*l zBRU%LCu_r3F&s7}NmP8zWuj=A$af}dy@YI&cP1#KA*|W}MU}y)ElmB`NEltfMJ;kZ z(n0fSn=L=iTb&3M6h0mF&8~sb+U%-1qD6e~yL;TpY4^_kW-O{jrzwOKEFa-2Ov!}* zatyS9moRryUN5!3%ruV)W7BaT_E0n*gjbrFf4VO zquDA$RT#u|Z-z)C?#Kfp(DA;m!3U*Laa-M6cA(%CNwP>zO<@!%QW*E;i(Q8`nY;JG zI6aOvH}jx`0SP@rA4%@L)Wo(*$(>Ll@8`*MqOYN+6#nBhs8hDQ(hI$xs)K z=ddj*(z!&JfWMB6_iY_!r^_exnE^>_y4HrZgFJBQHne~LM+sp9#RBm(+DpL-H{%;U zoz+e!E0w#;v+NIGl^FozwaFMHg`Tj&OSk_m_Xbbi|EZ)Gql9kC;rF~8m}uuHF4;-l zgtkXiz(~Ew-Jx89xQCN;<_7^qiAvznl>0lcu@QU(iXx^8KQblzmKeM5ynO0Wj^Szs zl`_u-L`d2sLowD$p@w{xo)fg;_i$hX$NZt5LpIQbZ74g7#1>_)+QWHEh21AdFfnwq z$-PIQbayMI3cl^!MDY3iw2Kuo|0xb(3m)9KF3S@I%AK~w@jRA0TPPcYnUD0Q5Bdj0hsug7bG|Ht%W~ACK%@UKUKtACEYTlXkBXb7C1i-h4 z8VZ2Zcys++(m*uqz{^UYO5dJlmI@pBx+M$g(qFC`bE=X+wGo&b!M}~87_iO>@}bQ_ z-`-H1fkVn2u-QsL17VH}sY9ro++4`%BGx*PyNjCRV3d@$}6zQTXO0u&uU zdcBPLW49U=8~LdUxaOdej=%#Ip$$p^$Zn2LM|=)L0$#Y)x$2ywB?$f$vKKu=ZzBIi z6YI982l524oLYLPv%M9x8AdmC2pc!bpyS@0OJAT=qQh)i+`V8mDA^i0KJ^s+>I4U? z)teFA3w&!mRTRj{;hd_noIHwCitCXF#dtN+EvTDY7ZWL^h1ApGtC!Tdh1K7l!jp0(}s_($k{XyBkBrcqmH?j|(=H zKZ;7d6I0G`RBumotQ}S?-6}afz^-nKsC?sKMfYmO{F6E z64)3jUO+k^657v~l(N1nD`t4WvJZm-qs_HES$gQpP}vqg7?#^6i1ok}m(Z;+mH+NwL!8vvacVMIVT_ z=Nn*PZ@ws)EYY6}W6nIk&O8W+0Wm}O{RNVg-*z?jvROklPX4s^MBnGR!fSMccF2F~ z4C-AVV?+P8D4pD#E^5=p)oF;GBWt6K*>RG3wK3ran`nX()SM6_g?w1UPYQvClmvZ= z6bGdZRK%IW&M~TYA)D5=RUq|MQBu2n&Wr3a$|)d>iW}2U3M=5&2bVQ>uSRxPcz`CI4E^1;~dKnTmG3(n6VOKLlf$n6) zN`w1eqFB$hO(QMnWbMuQs%zclsWjCIiFynZ2|`(wJBY&?T7Nl(zmsK|xuPo$r<9P6 zH7Sv$K4hvs$5hHBao-KZ<%(Q-SMdp|$Rx-GWFS};iVm8cr6o_%Se{d2>Go;yWF|!d zlQ8A8gJvXxm7|#|Izxqos=9Ch0)qv=lG%L02_vg{zKs5fp8?_Rd(4F053&|`wi8w{ z4N6~vW*<(nsQ?}EY)-GsGD)5yRwwa(Bc+5MyR+{9r|O@Wg;U?a$3f3x4)A+A5~yg| zH1T{jqVTyrA1kaj>+wTBtV|0fzzI>F0Nv50`dCqY>2s8+RN8oQI_Z#9ggA&gFs6WM z5c=%e%yVjl+?O@EPfRG(HDY}`wx{5EMCYT*JO&8vgAFoCd9ged&*P2#UNs}thmy}n zFeYtaKMZ8kVid4o;0}(nOo`cUEY>@_bQ=}Bx`IhOq5<8$npJSK8#)l;d4t#x1ZJRa z6ZJ(ZoDnFk8rn0%!U7@Q$Nv9fskmqWjiQu>>}n3}VGDQ2E}Um08PHw_sSd#>yH}hD z@Ce**gEeUHK&kVb^m4Bk+?~zB!W2_#oTG~y*jvCR^c4;TVoc103Z`M(B-8ZBd_`%9hUuy&ugk8VXv5Eh}gW+XAlFwJE)K~tN7bpGnz^K zy<~!rkf8%ZQ!tluK|GBTg}V|pCLJ1_X8kFKN4XmgCSHu#6Zp4ABIQPc#t-hdL%~)k zonAXjdl*D>GgvsiGCcc3IFi1jTx9j72?5uQpUn(%n+A6cF8zx&L5oeoNL8YYJV9-I z(J6K(yzTHc)m-Q+j{UDGuWmE+=aA+DG{DC4=m`h{+W{)L>$Y`LMh=IQCdyQr?+EJ) z6U}<&!y^VDpDo8MTQpnWy<-eeF8ch(4F)iCA+m91%_fS2bKTcionuZF#8wXR6=Uw| zxGO#D9tPVfY$?tjWd2KZQ6&$cN(y1HC4>O~7yocI@;Mj{SU8<%{k6bTWP=`Cp8}H% zZh;|3+q=a_(Xd3ugj9OqQsEU#ix~%tw&&91H=IIL+2eerkon{X0F+Z|XbcWR7oD*% z<$Jh=^E~wSP5#T(jIGy{aG5D1xO!hF0Q$}p93Wt@j(QY8T3dw(!!!IBm_2>cX|0DB z$iZ^>DZSZN6BRK)JwECJ?g2pC)Xc6XOTp^ z;wPANI;Kb=%RAgaXi-)qiiTBy#jSJ{eF{&V$_5(Izqu~+EE7oxks ze7H)3EDo6aJ51O&iFHL!HpV;R$$V+eUcjNN!3E2V0}d}O{n6qM1vYJQC}%+rTpVF| zYHWwACu6;T1fg2Vqa*-2P}Bw=0dS+xXyt6kH-7{*`O5JS&3bSm{`5s^S#o0nAEU5M zT1>WW_AhKnCYgQ=7FF27&fXkcB${&2zJbw_BQQ7UFNevjDbaSyPuuG4j9gAu!AuEe z2u8K0Q4lAR5%Q4LQkdfMcmX>0gufCcHHb!Pk8Q9A^ik!Q;b%=~Fk&TJ@7_a$!44=u zemF$yoI1WB>zJ)lJK8>bm*2+^d4V2z&jDtOlNI}EuP7)^n za2CF_c;6r#&_53BaifN$G{~*M4KGZ9E)H&0kWunWF{(~M4e`%9SKqu(xZDEpkxj>l zS)|Bd-^=zkkVEj{m;Xn@D zFFdyqE9l%o4iTRsqaLi|ypRwcm|gZj8@jDE^{EzB9QLN`nA;TsR>0vR`Ljf|p)|d} z+Z{k-|8o;XRU3{o$Uw;w8B;35>VZx!DA_tSiNe$M=jsRK;XPa^uF!{g|B7EJ3wPP) zay1DKy6{k>7|_oG8vfLHk}+LYJ1r8MU1-UEM(_;&05_6KvR80!JfF!thkf7g5r=cg z#$X?HLaqN!y(0E@;olCqBROq}nv(Z_W^o`Gi+4j(WmY3+!qZHM*kRVdP`;V0FHaTS zHIhbuVIVkYbT3gjeHe{y#a~{Yjb8NvnO=r(PnZU*;Q}7$e$Q?@IK_304U^5R@N6l_ zw`;5ZkmqX&~z0TJBJ1k*C1}H*Ow%Z-KZuj)KnQ5g+L`(dgye>%hI~$9Y8*V28}u)!(!b#d*HNm; z6bZrKB6Z@MpE!}OnSqsmph3_srl*o%i66%hTRO3SiVV)eiYE5uj780Oe`M38VVo7f zcB4X_Z#UdfXnjIN3E~2sK(o1*h$)XfZQMy%XxvOrkGKFYK+wMh*WZqLDEWx0=d#w0 z>Y@e>VRlNnG`=}UN5wOzaEsB5#qYpbn%Z|BhX{*0wW5aAyG6)N56xg5yU3bxO?rL+ zbxAqk<{G+Y=QPe|ox3#Y#7?1E*#Fhg&hgNGy<%DBsziajcrC(n8ngVxX+VSl!ms$dgiDYE zZK^YEE)2znUpYBC2lnYgo~pDP>gqdLBh$cS<^zp$1Pw1>G>ser+I>V=;uEmjUQaF# zZO|}MdJ^{^guzuJ>y7oILKbNP=iH$~Id+8A9nq(ex_!m2p%e#+GCM@_B;eOCzM@&o zkA%BgxJ8YY&u1fY09Bm+&?8u2IX19x#4liTp@0Cu{Y=WYrBQ-vlAoya<1auOtfi)b zhSmS`A12xmHt3w2MI#ZAQYSnKZsz%WC9W12k0? zI{LDy0uU&xRWJnQ`86VqEnP!f3xKo>omJ_08L)?Pl_!X!>oBt$c@_|@D*|j+2+_P1 z?nW8ta8sAEc#2cOZbxE#z;t*Dsmuu_l4BO{ORdu%fK^B;JzUuq-#a_GHaY z@is4OQqCS2a)mVRFg+Yb?x%O`o?&Vt9lQ01oc#C~u%(s=n)w2JEQ8h>w- zi4H0*2rqRwkkh5v7g0rXMfsL7onlI!H5S!aTskAgn>G~n>~>T>19O1vub@4EgTuVF;H9$}e-vUrgO(Ay3(VJD ztOq_-jf`?cqo(>fnUF}Rm`462D2-Qp#`l2~;h?j|SkT3rV9zZs83E=b=z5nN#`7Y8) zvvWzPfGD)(Zp+JHK#-`_4qWccGbpRvpsXP_JDp7Aca&#X-=;9S)h+W>oIQ#-K}#~Q z<6%Wxv2A*V*xhC+uUQs;m}{mhV{nZGQtpowv6M@zr#u-a6$#f)N*BP$GD^VI+3N%gk{ZdD5_=xoXNFImGMI^6DcQO`h|6u2Uf?e!w631apfv^>r6tcqvF;$na8U zc)jRbv=$I!HgHD~QHF8rQ)hgvflgDbHG?2S8dq;*31}R&bB|9LjXv^+I)@^1WdAWI z0ViBkl`|Qr=q^>rg)%n1o1*r*5H$l9Adg*aQR^u@YCYM-zeJZW@^&Bg<UME8-HYsSkj?#F4m6wO;WJv)1xrWjRgzS3 zBOWTQ!c}hV zQQLm`|XiRG(_Tv5ly+H1SVG?a~~Gy zJR@;o=rjiz8lF^%*pp(!+d~C(sFTV@RaGnm4lKxtH+|jby0-PNDXZN4Z7v2whwZcQ z0-a}%0R~=Bt_ribX72W#kt}CNe2tw2y^S)3CJ>a9#@1qYG+X1*J*cR9mvx166m0qw zV2cELZ2^6!%XANL37zS#XNW!o{dUBmQVgI__XnXV)I_`VA)_<^M*Ho;JZ#i`-lbz@ zVRWb@lk;RE_G}lJIL5h3=~_RTino{Ri#FfpGW6bEvDF}}wt~r8Gtnb_PxN-A~ ztZ^^(2N#>d)C&6T(42AxVY3Xh(e!fJ_Ii2}>Tk_~C+JeaW8t^B!KXxUz!r*qiRI6{ zIoqATuzZ*aC2HW$k3vTFKpQ!aCs(mYtG}=VcF8X85O*9~WE$ajiEBrNm0BBJ-w?i9qd?N-^lpxIdgmisQA{;0g>*$~!VC8S5p{kuzw z0Tped0U*#{4D=5G?LF-3$C)*R6KfC~79f-~00!fEaH4LFbF-Rd zIpj_^q#ux{SFR@#eAKckVhvrwXi)-g>wzY<3%-Wur_UgWnzsI+Lsqm_KmnA-c}=*n z&6|2TROu~?eyz8%Fd4ov+dbZP)u(~GS~Mz*fKVnyns$f++lVI-tGPHV7J_HCI#iBe zdZ8dZO?^c+l<$mC#Rg!dPN|0EY&UP)oQI}kZA$x00LVmd!*_TU3P2!%7^Ij|RTPlA z*&adn-XK<*;kX+ZZl{{4^)n{$``ul#nl55@IM|0Hdf^_m!9fS$RAuvw%a45^!Nm@~ z61fp_*FgtoouF`6ch%W|@j0Qc5WUOa@4qNo8z@yre9kbP$55e*!=MUbELYnesu4}g zYisN50@l#|o^NOv0C6C(9%|g-TJijpxa?&BaHPNl*Wm>l|3)A)G=Wx)B{TB{BY$@s`RI9YS1 z#$r;=W7*uA0hnhGSmvON%5Ov5Z9|~D*M+c72w^yKV#`TF_Us295krGyBGw|h)fKxi z6Ki!ebL=1&x&+?_iP);<$wq*Ok4JOZzFPqIljr{w@d)YmaIQY2#Jx zUZ0vU5?x9v9T&xPTBf7N0ZeJQiI^fRsU*}ABifR!%*Wid1J zDGc%K2<%9QdL>p{-viX9oep?KU{pAsiW$S)z%QrEC-{iA#on2D`g}~fS1EY({4MI0 z4-qez4q6s6+&-TyJuw4ejXrH`Pbd|i3I7Q+tG@-mkhy7Q$_dj9*OU$SV+;SF7*Xro zSGsdpTk8;3SAJn3Tq~8xFs;R`gT@1gGz?IQ1fbgmM%xbycg85uWW-8&#$e*8=)U_vgHFn9MJWnbhu=fS_QDCNs@>Q;?sV`J>-EWld}YrI4eZ08r}Z@Zz{QlL}EWJ@pLE< z^zsy|fbYxeD6KZ$tH+V>rJNmKZUWN9A25(EifUGK8QJ|PrE~{Y;r5-Z3VA>CHW3)( zC;&^a*CqiA-hISu(L=6XoH^J6+WOSEP%!lbyfK3w8DQ-TGGUXY30TZ$icwMJhph@2uy6J8EBjR(Z)k46yl2Y z(oYZ`u#Y;p8ZMIguTX96wa}>_nkI|vHd?to#O&e;yeSJqs5WxbpY3{8>I?r#_zDl;*5Znh&W`xn)4 zjzPb<+5h0s3w)GGgV|M}ET+_fRR%1_=8(EzHB7&gKF#&>N!XBNv`Y=+F`ur&oadtp+G?jT4bbG`^hRV)$1FPqeMUB-d z!W?{gQ*aiGj5ur;QHVL%)ef-rdK>Q*ekg7x?a<;sGtcFfZ{r4J^m3wfvo%<{5i`Kw z8;C{c+ymOMUp1j7f+Id*gk?A0MEUG%x{P9Juiz$oPW$|L`V8N9gR7}k)u*OBi`c!V zxCW>P32q!W2nJRO>-69KGd)qp~(F-F#Z`SaPuEiQe0V9BOwaz37e& z0UEfPk@V0tM9Nm4Os&t>0t*u>`(>#T(B&+QS_ErVjO%5u`)E;?vtZni3_F5Dwt$4( zq@ojfXiNEtuwH2oP($ljN9xTo$J`abQGNR=Oo2FC{Ef%C1K*4Q%Qg}xiCWpj&Z z{Yyo;7Bnzu25T5Q!Fkd!$Gc?&&7K^Q2Ko}B==3WiC1!L)@XEQmcUAz>0ZDGgy`D~D zDEz+46<8ux1S~G~9aRY&(S+BmD1~og$B8Vkb~6kttG?vJ=L8jFu*MFkx^zE1VZhzf z098b}8M_kq2+F(?ggMR|LP`?ml{miTneH-YHjK~^{K6I4ci98&;1Xp{OBc#KlWHin zY{r5@l2rNR5gYG6iqQSeRS5`5!_Rx<>1GxJOp}eXq&1km$ANM;>yF_R-$QE|*E(Zg zK{$BzR#5>C5bZ0^EFm zLPXq8RmU)h@mQS&RK?K4lnK3H#G-4JU1=PYMWlgUlH)Q#e#`)EH4V!NfoqEw#284S z*1QikmeiT$O?7xYrY(3eaMoxA?%gUZ`=ucQ*zK)QB%J$ee!nAlOTp!uVe^1ITq#Pe{Y}m9^^BTfTrRXBJhGf zCsX@7%_?bW7u9?+8$rn+jF=puX~n}fn#yBiOBHQnhI{oLc#_J8c_8HVO0!qXgp;A6 zze7mT!&?SdNB3!TNOoDSZQ+cRjIgA9pxA?TrZWu#;C=LFfiJqpu%JoRrFsijJ&2fM zqIN0lpeEa(Sa+w<J(G$u6u*T?Rp+KUPa9G={_MZtcA16F8^pD>o zKBCTpWw91v6WNUoh^@d;)LB}v;V7i!>l(^W*Orkc%@M1ZCKFpZBRyheLyw>fFi=RM zXU3lsb!Z=sLvv%v3 zaZbD%#e$7X)h1i9Kh_ZPLf2}uBoY=74S%ls-cFkGg8_>rTNo`>f?JQ&F3%KnPEkx2mO{?f)t(A}wab z>@p?ACzV@(%Bz_@8=1l|h@lB+y4W=E!C^H^ELn>mas>U2x?ktK)X&O-Mbb5Un40z>= z97QO`2>li(6mIseP`U?gcRg`4qvTBp``2Ust$99D#gmvb3_8J^Ju12ZramR+XfR5F zZ$oUVYRgY`w9r*z#3tnUoJFp&Lz*s~WOPp%`BOkCAFPxNmd!uphrIV`FlXpRWh|06 zpTkKOZ?CwZ8I4YXBgU)(rOBAfHTos{o~ zAmz6`0W3LMy=zdCnvDF46aN;Q%!#T7VmSPPWi(RL+TMp08m6lN@Os1!KEH@fHEi9j z6#N*gs47X@w1diqh2UJ2E{62<1{?vxo5~jSR9=C|BM@VKnb_+{_Qq!5Mw7TV&?T}x zgk~4yd_r&+Q=xQ0u~!$qxI41kSG*)^Hxn#^(k{)EJs9>`7H9Q#C|b9%aW$646zr7< z1v(bRZ42RG7sbBG4(^lyoirWBcnHa9sxY4@6?+(Ho9m>%MFonri=Lm>DyrL}(PB>s zO~;L~l$hw3J(ahu%Clj}-QF<;gK^;Kpm9=A-M%^D!>_Q)p65;6k?)#ZePVW)jqp_e z2>z0{<$#;Fm86QMIZ5^_WW(INb9enUFHyJNqwpklRXSJshle+@N-U8;@fx(pFS+XA zFEu(CwMeCASoH#OijfH=;r@)SzQa1EBb%nFAgkC~+k$0s27Sg*O+*&%0CY6X;oLA| zBDApLC#?FKD%EH39I9@R82RT5`_yt#6izbb39Rpp5qfOQ1OXBD-K=fn zGa;})e-XZce;~t2EX$}hDjG0Oy&W6K4>SdyWo0cC#XiKbOat5p^c?owa*L}F##&@d zqySbkVJiXcXi^P1Dct!%>UefUIvqzv!7oUjO*0%s&{Fa;0Ik%Es(6;D1zV0EOSNw@ zogu~HTUM&!$-;Y3T}(t;BMb$U|4af%vew)Y`vu+GxH8|PGfw;i48D^H%{lFfLN1af z5|#@i)~l`94jUvI7gxtKK~QrLTht6$KY2pSxy_A0Mppi(L~IVwVsLjj&A&hQL~1r?`S(L z5H75CR=INUU#WVm$sK2L=}{j=UyIlZj4;kYmkY7+07jmKNoS3P^=9z->rr;)>a7nf zZuND)L*WV{s8&)O87tX~o|vF)wB8FcysOAEypEd}Pj*EHtD-Gc5=j zZ7~QFtN=<}MH;Y08RE7^#Ip?kAJn$tPA8q{=O(HZ{Tv?G$ETi-^F%&_?ymU`8Y9u{ zw?8#0xVdN?=0T(Th@9xmQjc4LAI^*LwqC%QjH7Q^0hYVu)8-|R#YMR8W&*?*Q;gQg zf^<*^->v+Kj^h_GK>Nz31Q**A874%Ss}rmiuCpHLOg&=bTO_F^biU1|et1B#_lT(; zGGQ+{Mo>ia6c>XTcMwu$*yb)D6%tr6Ryam>WFFOZ(jjFIx{8OnMh}Ui2gq;xB*qAy z+amMHtg1s5n!sO!HP@j0iyut0}kB!;(~=lzB>UPLlLrTEfY3qr*cC!hkTI z2CwpQMwCGZd0Pwih8lOUV`&aWI3~F;P7Z=C-?9#{4yf!EzivA;%`R*azl&-g=|w66 zq{T$o=Swp={Oc;5E?E7C=@YD;v_&*1P+>mV2N;5*rCSqG68Q9(*LeV`l!#a|G)SlC z#yhDKWEU@3OtBk8qC3HvU|F&YP~jH`RzPcGpB$b$DunnfVAWa^HGW!ufd0oMnxj1u zB)PMka;4lFwkK_p?oO6_nA{*ZD6!01fd%E%B>=BrtBwBThEC>34 zCceh1;)tCADxJmJM^)7$lW&zF7);~w*6s8NVNX|=mW>CFu(@*6$U z1@!DrGuGf=vfa6i*`Uj|>I3hn0^Klt691sbx42HC{~8umOBQ&S3RS$KUoHqPBFw@1 zHmxuE_bLs{$^RSv)JQdd#1<3Ka!y(+g#6LJl+Du>~LH zgJ!BYs6dBRUOp{u42IcDSGZ{xaDKXAY&WhOQP3p_^VLx_kyk&mFB}`wP$7%ixq2Yc3<~6FoyifK4za3sq2hI}S)XfpU!1*vIQ9vvobIe*sI;4iHp>636qmwoUb&0(rM`Tk<{>67bSx{sO~`{RntH#b4IH55 zy_4GeN)-t?N&u=8*!}S$(kqPqBoz|#!yOaE0!(MWdgT*K!!Yx*MJ$3vsEusWr^yjz zNHs}pfH$9biZ1uEoB6-J$CWHv2EscPG$UJ~F& zrvBT#m6*@i7`RJOcO7aU6#DBeu%}jp?&G2|HoF7+D}n7cN#Ft3_$S6!o#15+D%%w) zYy613X##^}Zb0c85~R2!l_-`Ly5b|h`UsIeqi0b1yAp=0$VFTL)(~QbVO%Q`JFK(> zTfAQi4*<%Cc@v3+gems4{BJ5VtTHrb$Pz1(14ykrl{3eQf?Uhw#PTVHsge5fO6!a0Ce(2n zx~fDDn6r|gksVC4<9@LiNQ3LpqetvnN^vaKr$bKRAeZ`Q%+ZUeco;wg9uFbiJ8nfT zL6%&@7$i5sIy_K@*;#q82P_}b9>90gB|@R3rOU)sTsKB?cIO?>r0-~Qi;O%lB%FzQx?xh@uq(DgP z4o{j+Wtw`V8F!aw=br7trT`LJazlu!HMnrcR$0s)EeLwDOfV9#GSe7hHMpEdBgH$L zJM{5EEYvXh?yGsnvaXF2W*6oM<1%IkI5mbWEdn&R1v#Ygc!P1t=!}u!J${ikpf09U zG#2bpoXSZ-pNOJem8Bk8 z(Ulp<3ES2IB;1-haO(a55Y)0pQ*$PVae z;h=3ppJr93rpjYaDniXS4+IC0Y*7;-a^aBUKKFy2Iemx%x|T z7$fZZW?EyzrA##olGzU6JVZmyVejyMH{eiw7lf&K7M=XI8XGCiqMEBDDW`FFnP-Nk zYn%3cLvgs?k{F<~PbGn7*h*t8cqVfUk%R5CWrwufPzFqOAZN0P;VVzv0MKN!pgCwd zGKPTo!V8sK$CNzOoMg*z8;Y}ncQpLdeX!M!3!)mtWdxw{;D(84_O5YJl$I3Ibp?J% znCwdx?^xn|i;L?>fpUtcb|MKgjf4G&xiG~zI$b!Dw@GA! zWvD#vS5fHZ_v&y#5w0NBYbZOoRi=r1JlIwnOaR1ibD5AXnp(`13Qdq|kT*nkC7-zo ztT|7Mv}fFnC>*nNe!FW?;)e$?cxM^FE?1`cIzh~dCSGG9!yRe_vUaTz#cv9uBxXCN z6p@v2B1~BgUL@J-MD6*2nvY99v9=dA|AnYnRQ9Qv%WEk*js+b|ofo`y{{-X#NSuaP zc|cnvL!`h^>6y0oSd~k)t(;MJ9q8)7;UIlu5Z*)aPUkl(V<5EMX3{Ps$Bg#ErWd*l zLfsY{AgLvz8(iF<9(1GSnSH z6SVDK2jgXta(_Y9d6-yhB1hWV>hpL^x)sKaA@5Pg=>a)USnq7$>WaJ%H4~RYAV+te z^Fg7vpj$&lZ=MWDiSX{`s$-do?2yh~%gjkZ^dDw$w<>Xd(CANZC-fN=eYr}U*s{TJ z&3$sTb$@yU#{eR9D2t`r&~A?!1wy7sLOyz26YYTXpF(bx3XY|M>%#AJJXkD>s;)I% z<(z{tS7FjVb(0BQV1cQXCn0JV8B~CW2AO$~DGI9wZ5hxNqJjwV5O^+GigML+jL6jz ziv#uHkk!vNeiZX*uN?2=46gzF)8-YMLuRuYcX^I3JvR-fa6x7P!}ib0R;bgO8P>kC+ZQEI5f%zo8upqtsZcw|p(t zoCVR1Oz(F(AVFJC_OB0#lmNOyl75!L^JV(n^ck-@j!I*L(cj};x}o(Jw6QMWlYREf zK!;i{2ZsJU*>wuDv#BV=z5HHwRhHyrmHuH%92QWyAeU!4Nf{HnF)~p(s(fAOg{MUQ z9`j35SZW^ds^xNG5$x>(o&nHQp=i1z^aT&txPxnucVI`~-q_n#SWO&mq|Mp}dosC? zgDLi&Uk)&u?tgGQL6bJ1-!impW8Idg;0leUH>T{2WZwPFH7Gs#x#3jhW$wjN;}SAZ zVMk8C%6LN-BSw>;ai4XPU8Ot4eoYdn&jg z9XCzL__wLjSX#IqjT$vqVl1sj!{xKU4CaZZ!Hxdl1DmHQ16Sb0YNr|_qs1{K`RGX;i0>l@h=_)F&}r|``=A6lReHt%E~{kXd!s`G{g zHFu}LR1l0R1|mAqK&JYy9j)f{8%-Q?)pRlof?=&Vwdina6b*!8a5pj^47pF*@zfu) zjf3$lUqoY(Q8~z=YDtoxdu8`&_cn6v@!+XjF6LLsGFoEzpWr~YL}|WWoKXJr!KkoO zG0fvs+(s|xttHaNA;bgWpk$Yom~fO-R{XS$C5^+>QnLfW7OZtfb5FDA$PiMk)ktzl zj>C$^7>@%|@ygBw8)Mss{O3wQh^hki5DAssaG2B|v`+8}`em*bm!>;+S_F6yn*UNR zXj>q&`nY7}q4X2bkTp8{neK%`T!KW$IK(i(hgtmHYS;Ye3hV+dGO9rG8DWVc(`UZxc>jvv(ADcf1 zG>Bee7Z52myv?UxOQlp!-I%COLYnWOmz#yA{ck+YR@ji5t1v)9lJurlnQuxE^s0!g zhoTxIeKp7)%Ktc}&+Ifr7ZF!O#>REf8j0m#z0CQ14pJAxhhdEuavI?Gm5rqq=k1?3vf{7iF{g#Ia3>;(wP_kD(W7~z~ zdzMd-?;?@*ogN~52U^$*Hi0WL@K|b^wNfe7Ui^n-r)4T>#yXF?db9uKLDt+#$y|4V z4hQ|d>f|!UdLw;G7XHjV`MGwgzveKRH^r2RP&a)j zYt$NUt;PPRvp%Jl^#@J!Bh=6L%umhR1@V$-lnEv4pcE^!3BF{%h?<}IZ2n>vMS3)G(ZA7`ns%g|YBm%VxrEBLcfy<}?%>_l=i&D@*n1Wd@!qCa>L+G(bUiG3NVDyfT$0iAvNBZoW+Nhn)DP%3WyR z<5vpu`#<81wrce_r^2t6k*sn@_;ypATqkp)CAS%R2%i)@usoteqi4A3sGA!#C2XN* z7K?deH(}^;Y8_N&B;uT}))UnK-)4Ef-DCbyeTPE@kVG zV+0~`C)6=iWuR)c9PcXZ^P@j4t>_FRUS^6Nxs)Yclh{!)X14Ad%QaZ8B_??sAvF_$ zK9?Op;-0rigaJ$!9x^l?o-;284l&C%ERqB`E>#i^h-|t|M_B82{i#`UxyMnU*+suz5WO?$buzw zCL9ZwVI;HYKD$0WOfM?FC_P^yCfYV$~61%8%!4o5fHrjyQiPQ$!emGK8MAg(2xC z6BJI9!8JDR;B|JSpXIutoxMef-Rgk^^m&VCkt+`%v%r;we~?l}SF}}=LZt%D`-GIY z#z5#5;N8p=H+5FmcyZz+6jYaJfKaPE5YP}+wpLJRrNcuNj>E;KD!E~~t|j2eK^7cR zlADU}x4J6+30q!diyUfE^09T1Dn+)g&bc#;IU%PRA2gA+(!!7v(g}Mbr>I%@C#8sW zR8SejG1=zD?9n0sQ>YBBU-fDHw(NGLSDHtpe_kly8X&k_Gytt;Q(UGmos2SxZzJW#8vdL-0>doPeSUwH|T~mXDbfwm7 zz)x?!i?XI1W5fO_C@Wj!U*M2@WtjvP;Ue_aUoouN1FQe&N>1zP#zPP;3c5&*w&B?Fc^t(fWJYJ%w0&sA z=72}jNR6S7P6=Q1L%Y&G8hknjuhoWAI5e`|^h0~?X>F09WMDq)Ax){Zp3DL-+5XS& zA0~M{=(U^pm2B`UMzUx)kSOoE&yo4cXd`|B+ zLgC7d$?Q|fdv%tFi@j#-vPq*MvsjhFNPZi79E+J|t?DkBZF~1#qtHMcex4LDkdg5& z3Q-g38z$Q}e{cqoIL2-$qE;9Zfw2hJi6?E@`vaUZ9q!xUu(_OvDeAdO8m$qQpwtP` zPChGi`guHg+&XVpxGG`5ZfezLI#1KTP&dwR8tj@G7^?@&8DORu?|5n(@DXNumww`g z<{Yokf#ABH?Z@nG6QNPUSw$QK{69aPgjD$uBGY0Qkh-HHD?~G`+mE8Y4GTg`IaRE0 zw<^^-twI38gXIR6N-4x8`aQU2r>g_!AR8m261XhY<<(Sq;en?_|I?;5TIpX=#<0_^ zVvQg%crq4QWV8WRH3Eq#lon5?gEfu}w&}S>7y<<3d%Oa44x-;6PW)O;FpAxt{U)1b z`CR8qX6M54L$JAbRPYvrQaQQ&Lq)Kt!Ll_$5lc%gqg=pe(1IzDG;vj(bq>MQOschx zIG`txT=?}Kh82H@#u2E>=ecg$ia8lunI|-x{0|Ujf&^;mAEk_=pj8a~LmkM6F3f#p zMznKUy#RS*2q-#!<TMwLU#gWoVC*58s|6cS}3hw_C*(~jGI4y86 zMgbF~VzkhL^cLUDOjHxin+VKlf(~Iw(D)y_g?)D=b0CkyJH`>CqPb~k>~YA|xGL!0 zwwZ^$i0{MXlyWzgl{`qBkmd`TH1zQWr@l*hay@2+wiToW1>Xk%ec=rzpF2vHx!Sf% z#-D?WVDW7uHRvCQgWSC-z2MWo4F9hs!Qt>)vLBFn@&z)sM$TQ)g!Gtds&GPt<5a=X z+{0<>00!;#vrqs7WZM2V9Hg3U=2gPV#s_${+A3l;EH1XH`QCo7g}8ebLpA@*()R<^ z(|s1Ah?!5!nLwAUz5ySPQo6H0?AkT@VR~WS5mz22d1__g^=g`-JxOuh#*pta=gCt{w}pJC6> zKbJ5NhxO`)SlD@s&7eReW>sZA=FlkL?NqM0=~pLUhkd+~eUn0}a~C;`9gKXnPUm!B zU0P1Xunk)d0MY-?*%v5DlBBqz{Qqx$dUo5D5e`V;Ib(Kado$ftnGxM|LW3@as+?(^t4~!E*cATXD}@1b#7)i;#vt z`Kd1A4fbR*-pR4$!?HfLkNmUE@&UyKx>;{9Ex`2^V*WCjW<#vbFbt_>H@%aRkfq~O z=QkpjuMON@8LnuXSPd z?!1H?(Q_mNnn8!m8vEunLJsC|VkV#GPMRYxc6%@;4TxheRG6mudblzB!TN`5oysvg zp1Z3VoPO+rooJb2*tw&bg=OekQW+@oEl9B?3s`AC+%WwlZ@?C5&??%ifhG_9aK$n) zYrjerKEC3`cuN^qXglSf`@O z$L*%9QN3#G<0Mtkn709;Kfnb`TIvpL5jpkHzCs}IV-#5SESX66Ob|*ancgXm-Y_N@ zZj~k??wfI}#*ov`<-n_`c`Vnf>9%M#ik>^&vuW-V-tx$g_#iOEP|H>9h147aRg}bi z$doP>Kd~rh{#^dFkW4(8HB#4O%j8EXGXHsM;gM-QV^$R{d%bMFKgh)R`-%pL6S{XA#@Ak16>84sCiB zq$DMDRMe&A0I5W;n>K1xWSWaOn^F`COn0#Z8+tw#Y{2CsIzr~w^UJrv9LR-TN z>Xl~*ob?tae7lLSG}~dvm&3kUj6sE=hm0~UwE$BSq42iNI2=NyDX*vK?ITykb7{=5 z%T8Amvrl17j2Xglgk=tEA~N#SLC$RrIztXFVZUP#wyGB!z~3f0Sg``nva9YY0)@?j z7CK^rdTL}vsPq0SGa{1DWn9nPWMrE)Jz>UX+&t4RCE`Z04dNs%J8K8wrWr5Dxtu0c zk-aL8A*PM=<0<56uMLg&C3-WY8$o>8nxc4~Lm@OfmNXsfM?ju7f_*Vw_>lP(z}KT@ zCJG9VD)LiIh z4PszI&;GgWKw^@Jh)C@F7wwH>!+-s24h1#p<5P88n=!u8;jIsCgGjg}95RNeqBUXO5KN^qo1#+6?+bfyp2I({fyZeKdiI6rM1 zP&)TSQ-sG>eLV4iVLaj8-!AV=HG=NLk&)@k>>jhXvoha+Y6inZ*hH68?vtK#* zYZu!<^12p+vuIB)Qs;yKv#PePfoTb!6tC-=9dn9l5&N$Vq)Q>r3b>fv-U%$ ziP83d4jCCJKie86(*O}n6_TfwdA<_*Dsq1IvZU`iU*oMeqA3nKVH%WiQ3qv5hZoy$ zkw_!xx2}BFDES3ckV6;8WuqoZqyIddA_PnTk#l(xQQc<{>bqLbj}t?S^PLPk0KMtc zEEb~DEJ!R?9Tq`>MZA5UFa<+~p`V_S?JhbcQTUXBkYxr-B_D6oxlSP)xtPvVTuwZ} zPbGAZal?Nj%pn#!C`F+is8c7dK%!`eWdcVJPU|Js7vel7n6!n2Y}uCaF4sr@7T8Xi zDpAB%am3puSpF-I zfqS=%HD%~YVpWl=Ff&)&N)TDVK>pfLVK7EH5;62LGYe>4o`Q^clhV)#Q_gf3>o|zy z)f44Z&H1V|b(qsJt#>rDZZFyjTW_+-vct4P$x71^wTKgK$ebzJM#XWa?S2yY<>z7A ztT^r8y-qMa;!b>olY{&lH+%D+G;%AeLJ{!$GaAsW1^eGvdF!lQXO&$Lk(Y9KjiX+W;}0Lo34J>R4(+4eP@C>Z}OJD z5yJ;v=(=6})QW>+_>1-7r3b)xk*EXpr}ARRTrLKYZ+d=L?lpMRJJTAQ)3Jp~7nmb zg*y0Zh!@qG9VtEMku5fjM(IbxMdolYcwio@bH^~z5cU|IEE8t=kJ2!J z3wub5F}s`#62Xx}7EWvqc}uCnscO=Mb&WpezJZxa4=D^J2=`Gm&_4r;+*k6Mzs%Bx=cFsrXvRGt?y>3Kvjlg zDc&1oA14}{YXa%4v)VOw6K8dq8=uwUYyokewMJ$iw>pj5^N(wHX6<}Qk&1}kc_?7! zC#X0|TWw(2%hGj7K%Gp>lBTdjq0?I0azau%xepza?rIlY(n;udhFjOJtr=rhq$>=) zhdDbeE8D^!Sv#T&MmRT-_P&PcR+1LyJWa-j3F@?oT85UPd-^4!)^C+>jgsA zJ$q;eya&t+VP6QufSLRN+}-RYZXqf}BmBG@iuFfM4dI!99Dr;C>rYCWy0H@;VNWc+we(=;iP-8A3sdT3F;dG#?lg{6%u{| z9XG!$-#f1HRJL*@#FIMy^!n@u{T`LLwRE;fU90iz6tOy^Sd#$*E&S5PZemN-DH5#O z`<6dh!NrHwHul4%k{S96*|3NMo??#`d=B4*x2lt0aKY%jigllvXF3WPxiFes>>m;O z8Kz*x@SQOJS@*k1rtDT+W_@6=4;|IwaMLfshB{63{$U$tiqh1PQ19>G3?BG3Bhq=j zD3NYRvf&#{5pbx@)g}4+vB_PFja!~;bZyWUZE#boroP<|jq`gA(Yp?xbc~h>?DBfG zdI8U>8)L5|X1Q0qSy67GqWoVR!Y^jIL}#o)k*I||xaX?fMqx@5$Yc(inxOAc!(xZZ z6DPpfZG^_WQxMEv+nFD@OwlO?auDR)3$90P-vWNq-GSwo_S(b>C=-6B^eo`srG9Pf2 zsGykH!gL_cNhNNIRdivd6E(|t!IX&6xG%_yr&tQ=wXByoVjk_WYqpfC4Gin0uH^5{ z988-DBrYzd(NsK_MR*bI`pHLlb{@Le6G()TzI|H@v?#@T)lz~kDy8CautAKZvU;XiHj`u~2z=i-lzy;OE#)wufY35Oie zUUQFt<$0#x^h$qBspA1ZYiC1bH9Vkf!8GO~dI)m9^AX31dQb^(<|7Pln=d3(>r z0yUg+;70hf*RjJnzyL@*htt}_RTCw^{%@?|)s%yZ;eB9&f?BKDi!JEF*#$9%xf&&~ z3TGXLjcbSRNB0a#X5GuGlTo$NB1!0`kn@Oz*WPL}>^%R#X+F%!SqWlCgjC@pPb0g% zlCFf|&;tvjfiUiCV{vr)YQk}t;gy`(R0F@5zV))%lshYI1Ww@uwEjd=+Jcf>;lYN# z*_cfLQ&@kL;n-sR3lDY%FJ;Or`$|R_->Hs!j=kO+3KGbvv<#Gs;XAoQgfa#7Pr#4; ztD`|grcU3H?r2fUOb+PjL}-9I)r{~xpL>`&Z8aTJP!~qgYy;|Lg^eoNF(zfUAM8bk zb41Ivg(pthFQlB#(~Z{LLDYPbKpqejj2U(JN&wC6L9H_~zImyHE*uOED!xC!H5JEq zdF&QfDaP!2Q>|TbQfVO*=gAu~{|F8?v`(l^YOkL(yzGM3g1uf0%4~JK{0}CU_pXCf z3llPQjn!fee)x?Y`r%MIR!bx1={QTsi%s(=Bp6k__j`s|Cf~|dKk^2hG2fix zvV8xXA_+6GMub z-ZF$Zrk^HuuC=1uqp5NX27Q^;Aq{Bm*R1%ke-K?av0t1prHlCCuYjNBcBH7fBs(d@ zzOiAV@yNk_Hz|iPqf4WKSfEaY9#dzyXaU?%NOTd>K9&nicM>L$i-w$sqpb9>J8cPj zf&+`z^{(F<5*v*;1+N(BA!(LkJo}&yQVv1FwAB!0LKwtEf7*Az=x)8#*SlqsLOyxE z7U;EZwez6|!HaoS72yK(L}%$zzq>S?Tu}e7B;><^IV}>uubtuu>hFG|GsiZM_|*uv zoSxYM)#sTUVS7w0n#gPQi7V8pEW0r4$#>97^CHk;W1RN}LctiGG~n{(>U+b$pbIjv z7AFkh_CeXv@t7Zq2tZfsv^B6aD>fQcoRoe8V}0VZzlFH8&gm>JO_W#D*zYw&ufq&{ znc7%Lw_fr?qlf-^dGLXUbjR3eaK8zz%!Wg_A=?^U(`vGJN*RDwDgoyIE#vZB?AlZ} zN{rlVMveogA7e~~R(r5eAQ8;y<~^bUoJN&lv|9{_aj&2laKK10qaMj<G1rmXxbXS@>XhJXnT~sweB6v3EpJG=nmmGAU)3zs47x zV2>EnzlND-_0{l4!k_5U3}+xMu6r}NzCe5=(Y>U+-rw7~ct|b7)&xZ%vmAQt?xCR@ zjFhiWpNw*;L;=N^sb7~G50hn%*|jU;G*`e%tPUPx7nm zl=|>v&y7Wlp_I22^%>1~5{Cdt-&9M-uyFu=h$UftS=bLM9bjf#4H@=vqXy`Xb(t~4}CT?jIl$0LZ1}=^k{L?mk?~8>g;K@DN&)R4|{&vu)_WoAPnp$=SSly0>pB_6-O1MlT3W zr;QOpX!B7|Kl*UFSj@f6+V zH0$Zn?tdYVVyfaGtTodyuTTT?@SXc_?;4Jbs-Rd8s{GytnCh`R-oD3Z(=M45SB%{G zr#^l0mzo_UbVNOKV9Nqhsh@=<;rSU|e}{cdM-EL>QC3L2*GQH@k%BviY7kkv1JE2j zNA26XS|R!jr~KBxX!RGWx~|dC=bb(5ACx@;C!{lJR5bpABz@=p~!{sbM0iTjZ3Chw9?I=2>i<3akDw6X`x2fR3py8i^b9 zaGV_AYFdNedoK`$tiJT~+&(PL0AUlK;h+SO@d#_IRKzJ2uCdRY$KD7u0=e~uTi@dB z)(ky%U`|?R(p_@F^|`~`niBVlp{&nRGcXf1@N}TAW(mbW)NEs}qOk=C>zUeEn#r(w zO;Tpvql{*?^}xi9K_qZ(2R)>ZPmdHV6>kv z#&^9nuDImQu2cN3w4cigeH^Ew*8fu;iGn>-G&q*9ptk4};+biZUU< zdS&xWd)7ipVUwL$j(CN_=`ayp68rmKAIX%XS7WoHlp`$jwP+&NM@3Cn3(;}&O0Tiw z9Cp%rkdZ7-NjM>!jy|xw)i?bvjVlP55oN_uxsu{hHdEOcvlk@1>&!B|j~n97Gli{7 zGOni$0lzk#6J&)Zm>*9sk;+VIN}URdGjo5d1@pRSjP>jNR1SzXGxdjK1-aMo3H zjD) zh7)@8AkrqmY?!dm9kZi$omeOZN{Az*UeU|3+<#3Q7C=ZJ+gt)e-4^J1^oST1E3PIK z-9=ABB(;2Xnbz5I2r2bRS*N7r#E?vwh=ki4 z$Ha#ih5>0k4c_JBPAG$p^0pC-|Fyob^Wsc0MGaYAk>pynjRO-p*q?`r4E=uc=4*q=V(zI%IL*jm@X?JuB>E8@P2E$ed?&&nRa8{u z$VI3z%>&bFReV}3?lz2J6plAQL{YF^cZij0)*tC}GmeZo7N9W4SWp}0T+SY4UhYZk zs&}t0P%>D3V3IeoL{cXX{Z=Df?`F7?t68>cbE}Ggox)s6a4MjOWf~E>swjZhNZ8Y@ z7uV~?LM74B9mn2GSvrL8u?Adr5sig6)l;+|OsiHxFqpyPb4DEo?NP2orJ4hShlNC( zYF?022!{pClADVmOl@9a2;u4(*)^x}&A90MFtG}B%kbI!Lzz;KREtSy+E?qWlTcfX z&nH##%dPZBra9QZ&1g&w;aZ5t^tAO}Q~1JcT8x~Q(K^d24#Heuvoml72u&Vty9i}B zXk8|wr#h{T@IVzqX<+nf0kP|A92-m!@_aOdqAHcC!P8rvmTo}VvN>(zHN!s+L@=_$zRN#+*;;DW;Q#!!ig#PqR5u`YSqZokK}b z*)#Tu=qzg2e8qE8>jiuJgxn;=E(H=4w>DT6!$Twmi#GpQTfN)A2sul@ic0;m@Bac8 z1xv6MiB5g(KmK#PAey?2(nNFqpu#zBesv8T>CL)5q*tAc9AUGZyJ@T!|C^~ z&`|Sv2Y-}p;`*o1tph0H%&j#oH?%#k?2E+4vQ)?xc0MzaXch(Pw$7G_&4d_qi=iCl zp^rbP+e)rD+VisZrexf%cYRK zGW0`s<>dv*OQt25M#|WU{q3@gH1y(7Ufg1@L-g+MaHcz>=mcfF!ct(tAszV;xF2Se zVyI_%G<~Nfx=>EGN`>UFraQYCD(y?ei1PoQ6hm=0Kc$QEOMUx9?L%x@=!kFw4um|& zVyO4q+Q0={J}0Sjt`wg8#01k>J|0U8Cd9T=#DKkXeRy($2*6DCtFL?$X_$6i4#Xl< zghpgjewtiShIEs}L3s1UQ*^cObiCOf>#44Lyv{BX-t}gL?v()V#&l7!&<-RPnTX_~ zlCH*pK|}#e@hf<8ysXM+90J@&$%|cjA2j*v1K3kLLeID;%oY@D#Nt#W@Iq_+596yz z@b(P4+7&5#{7J!?ii2bwK$!*-pb`d^DV8?6;%mS98j(JMHu>-RSpZ1pB0m6j2(i;J zZWM_VSK7c9pD%@%06nblP*g~o0s)6xTPwr5PJ7ENu`@Y<^vY8;bKWSZy-aR;53uT8#t}Rgv+NRTv>8Q6_=JML; zi;SWKe9ed*CN_H3;|$SaOFkC~1U!d7v$`v+H0lTs~wxfS;x-f4moA+z)iHqP1VU!Nu>VNPuCN~#uQz$Hn!5(r<1Cpc@&Fpr=E#PN%`QeI2u9C z;1W{2X%k}exPrNW=~!2j_`Vnz{B_{zwSRQHb!j6tlY(L0%`B#?zLcD^pj3~)p%02c zBxM20Ddz)_3Cho8oG$MO>5k-8eGkz znc~x}1N!(NH)>dN_hCB&$@LSvY0zTbbtq@Mw6B=^`Fcuua+(U1N<5 zYb2><6G|a73xKldbW5MR!87cLQLL6fj?2cS3o_z|mIL!qM{)--eBP z%M(lg4%&wU0UFVTb0B*wQV9rh4Nz1?bWjquO&^QUmoO|4EYH%uCHe!I(P1^?q39at zA=|IDO%Gts%u*|SPl%7L&_K6B%N{TsQFgXE5a*BjP~@eQv_0NC3n&Ft|6Hi1kGl-i zBghVDv~V%Dp-;2wQ&ZKkHyxqjH!lJQkZMs_?oHE>V{E2Nz_$v_qGm z<1=59ON?+(laR`Uwq)v6kgRq9mmvme4ts~sy8*}II}oP!S#aUlN=T9AaU0CjzNEV23zyGAVRr=QJsCfykQZVeR^CJttG{D zeUd(;bD_p0Ku?Q8;G;Rp6?`L>7f*ls)Z({dsegg7dNEHeWE;w6_wF(0cm=*Z~3s-rYc z(|LM37!Z_9CQp&H%7x4us}@gM1pJ>EKeBvg&3AYPzr!=z=+z>1QHK_e-MK_hR z<~o$|S}%{;tskSe@R>QRtr>$5=%tksVLrG=`& zWj`ia>3Z^Lp(Kx!9H1hlHus7W*!buQj>eAXZlZX#ro@LV{}E}kxEP8lc#ZL<)+KxR z&QSZfUq_)w-)rIp1zbVt*HBJyt4b5kda&&_7y!gjdd5fy;npPn!tNBtKfIv9r^ors+WWgE9~D?R0C3^K6h&9NJLsDaY`tqj)mum8|7rl7*10;l<%TA9}RmKxv$5| zrckE4h*V^b4{MxRpOBIL>y$>-rTsUA!E~p1?L&EGc=mtC?^DW$ROQg2IfG|eEvl=0 z3(T>F=D5c45dN%_o_@;LSHctPy3ur1 zb1uPL!K8ERrV6^i5>rt(A^H><6u`s4%)HDLMO1^1444{G$;D;?o*`RNu6vG^xeD=c zpnf=H_jAOL+FtrT@xICMvygu}yy8g69G=G0o}(+z&A=%f$ZTZT`Lk*i>wDu+m9Y%G zxhW*s0IAQ!{N4KvggVxgFWqPrCoWgj;9|OiZq9anfbNi#KTDDMs(fzw89z-NwZR7CzsIL_Lz^vVu`b|?ea?@8 zjfs=gu5N}6gHF3C6Ue>YLQ^kE8 zO>xfra-q@m{DXTFG-U(kSB6n-Yk<;E za1y7Fw%$<9h%ppsJa?VsROxBx-HQY&w!$5o%uZ$zXfFGrx%A@s@gZq+#giUPm@ToW zj++)_{NGTeiL`Jt8XYxvVJz)Nvl$(*ZY&k?yI20;k2Wt`2Cl)07`GZTrTcVHo>!AB zTuI7Otclf|hSC|{(}w%|Epan#`}`V+@QWoV$U#L8-4p+V7^|-ao*pb4Q=%qbKK=O6 zgz`&(fBwRaE~F}JEx>OlR4i+w=$rAH(DV3W(8XqMHEw4?-7vLyAHIB9ooZX*POV|X zYeHk%5rHba+BP?gR-3v7G_2qmo+&sC+T6&Y!QUo7xxqJ=d}xU}*t(N5`f+zdbmuYJ zH1}LOd-T0hQRubGVpBVm#Jv3*0}lE0aZkE zmlaJgUKgbDmE8q4*0Bxw&!-R}stY(%Bvf|8X;XjkI>ArWFKf5B4c)oZBFKX@{8ws0 zI|5nV$E7L{WuAbhtTDyUaxdJup(Dd6ro)EKX{7HMV!^P?0dikwRa0Ov!I6@KnT|F^ z*O>1zz(XBT=gHz1(G0BHwf~E!FF|o8PHf{R6pbcIL`AyWF}qyyvh$$`MRYo6(Bzn# z>mitAr;>wtxAEQj_#11$@JuoZZT;A)iv`jRnFkL-0#eBoQJFG*fuYyBLHsz!=Fb5Q zqgT`gL`e-F^QoVuaw?}DOjIYK?03-H%|grm51Hm@Y)H>l7$TuqdQ-d14=o6KS48$h z(Swn`8stpn|0Sir-D!v^BE}%b#<&=bL^xWnYChkS)GhE~dBy>GNZNu9+tCr*T$GuV z^-EnKyyv%k5F#X?2zt<0nA1JS30}9H$R8dxLKeN-a3ts;snikNEF<8F^i#j0ZSDp|UxY!1uRj+*4wwuWJ zZl55(i)7kQd5H8oFw$PI1zfX%N2qPqE~O;=yU62na%bP6ZrLrZt*ch<&_G_N&(q*| z=ap$Pte$C7;1j3p31;`ccKFHVpg&ig3=^ym(WesO@6wasYp43pTsHGIOj!hV%MWEo zt?AZU?vJ|aQywd79ifY6IdY)RLt$bul-eb!0SBsfXi=_P=Bsw#A)E>3omrr8Ga92L!!Ckn~lPh3?9#@Jrq=!a?u^+hbCzYW4%vV zRVcYaXsYs{6APwmz@74~C8F9WM_CVdUqZFl4~b`-WPb}RW8sEIr>B8o8k-2*9Nj2e z5T(7z48as?0DA_dqv%ceU|;s<%r8esHLqEi>z1JTq)4ceUy|&p&V~?rTeP1|6;+6_ z&C*SEiz%ND2575HXjKR4{I_jyfb#uNk3U8HKUPx|sMkot@gX4z{n-TlMOiMl2@*Kx zL29CvX=P9+oG2Pq=XNO!7ucApvl9X(cVBUB-VR)* zv(eRjO9o;IS`g22@<2JuLT$hydOXQ4iRKmkLE&Y~{=!$j?ppB^;Zl;HWDVPFr&8y< z^R8^qoW9Y-BCTv(<>QJP>Cv@QT=~6$Mc$V^-+piS^Ws@j7mt5GDTCz&(SIQrO4?W5Qf;uYRP=Oi)El9S#fyF8>v3T-d!y0JBAF6UC~ z<3MP-|6}qRHz@M602pm`PssYybt2v;@h5diy`WEZ<>Um-J7wm%pqy)J_)88CwaFs& zCzDs;cDQ2%4;U&I72@{2JA6yI{?pqIJQXJI(~>kqK}#{%cNwuo0a+W_d0U}l(crLC zY^a+Zf3s|S93d&b(b275_f+`xGLl{H{2>m<$++1QZMDrZL->~9N#zlqlU>nM4Ab1s zz3DrLW#=kcbq;S4K8#$hnYM;fp_I;Y7#J@!-bUHcj1LGg)u;&Bz(?nh7I`#;r2l%O zgfrJPqG{;yG_3&hdN3hu9Z+?fshyWOWHlk**SIDORRpqteT7pH0R_1a^Zkm-@YQAbkCTZ2ap)+rh-yREfh640 zK6UPerru);s@l);fGSiKKI9S+)|P<)-t|uczH>2!YF=2Cm(CDYXan2w$-fn4vNic3 zjyhHor*y)#)X~#A_708jIqmY#OS>nnM!A}+PBY?dc9c!t_d!@NvMVz_+`cFkp@8%{ z)%pEj0;EC!77pLOA_t0Zsx!92f>?o%=BFeAKxnB)^BtJ^`fcZ)iq1z7dwMDh_I2o^ zgZ4@o@XP2aqvHOdNz-|Ipfeg<)QB#4{zJ;&5AF7|#4!d6EksCJU_b`ryA0hwl?s*3 z_sWPlzlr`X8i`Xyq&LeAaUzX+pHq%yJRNcJfTVLnvaYAA){XyFrRz@oMNoU$eQ4 zsPQ#MW0eKJ+~A$a`y9`7eq~`H_zL*3EC3*sIi}2^-+IHXzw`mtVf^_d-dWqSH!m7j zdcuX7tD+0o*`hBvBa;iV4g8A=8wCk{cdXz`EQH$c#prEMSJ5cDJbFy_uIuiF3EVl=3_I0HvNP% z3cHs@fZUMIH;J6@58BkUCd~M2!$3;G+c}&Z*Mu|3H#Elx)!;fIk3ng!XA9-d&8BQd zZ)asj%g=DExA)by3!YBap_ISek@R?K?Z(KixtM7v$<5S<-w-nK3hyT69W&%-g>hE+ zbL?(k_ftCn$TM}FYwpb*%5toI&W^I}X>T2A?f!U4U3r<~nlfu#OWJ0x4*B;w*Q9}2uR`1ca z9)3VyWsa0GbQWuw!qUmMZp-45#lL+1+Da#qZ~KY2?%xjq|%8rB&3*It{@iWMvnp-tvEgdZdY<3pVOF_P_swng|!rSMcw) zL|luvFYNxzFYf9N{DP<{${U!#TejV|NoBzpHg=M}#QN#W`?Uiuyec==N0LB&0m$4s zGz%|fgW@F8ydqN#p%?U({r8fY#8A+g5pO#%LhD`BE}JlwGwVF^cOff**FEH;dtz2M z5ki6?>Tjjw7RnSWRKrr3g?R;VUa);GtXAghZTm2h?goxt^I;$5y6~VGxY(@(EG>tC#nlhP*%FqKsu!n^hVYi9 zA~D|of!HMQL}FCmLrO#H_Di!@d^A`{OrHVuCuG5eENlC=J=&x>iu19VRhZfqa1nafV~psz8{Ek6TzVBO3^Zs5uuuSOwgMfGcKP%_*lJx zU@eQOC1g5CgF~wDY~*i|b^goP6VCB|C>kSH5AymSPM;r^?m=&v(2TEmVLlf9@N=V( zlelFY_rk#};duS2ojTj|QG>%RK1(f6o=KB}{%tJJ;m#vYuLD#+%PpWUlaCY!24Eh5zuiS!+Q?^Uh{{jTscNWIu!e zi)<)NSwsHOt0>bfKhch9;2*vLdxA3RoZ&m`Nt@+Z-BOju>4~aq5c>b|OEuA>t>V3m0^o9Q=N|P_1%9eMygzs$V_`4htr-jN;D$>cXS5|1rdD zAXG-tXqqJ4nXI-TA~Hsq%roC*Ft%{PXtH3R3}vKMR5BP|Q^r|d7@_NGAPwJ!!ih!H zSD<=QxPencPrMyT%e&Zk(#Y)GrlKewvvg% zxhF_H137lEip=*`>OHLDEG)_yF!u!L^JXY-A(b@vwg1Fs_Q&|Oiv=`r z$lIy`S5Mi}nUIEp6Qey{_g~5{F~c<|olK#IZv{Hjs`(Mr^uT8Y3T}|t=u1A0*P`cJ zF2iS;4M;1gGzy&@ljO$!84<}>P)*>a6krY43`vfbpTf!kiP;A#VpW_Iex>oPAip+fv<< zUBJW;G9r(BhR*QuG9mldDW&*^S#=ty@zk^64yJ&h?03vXzl&V?DM zOKaD_knY5qEhc9^(A-;IP>DT|tJTq6NvUQv!EV=5aKE;E?{CA z38wyAq}OfR7H6x!bPuF@7DdJbjBs9L2o+j%9fMJ&qvk;Llt?GL!(Jp(!!bzd^V1P& zNNM=vu;4UgAig)0H1V6}NCE zC{@^xicBpYDl)jKPA|lQxTI3UTidDL%f&yH=TkFOD<|J5&#}rt%`B4s_rolngIP#| z47_BbI0Cs2t_N{#$G>Q!Dmgkx3W7o74bgT;W>@)Res0M}_RPo_eJr7>i66v@{kh=# zGW!Su<@4^y6FwuGaO8uU-rJ`6X|yL`dpJ~)(2*$~C_%l4uaN;~R6K6PAB3%r*RHFG zmEg6XN(8qn!WtB8L3@M0G!CxOF@9tq<#DMzEL>FVYk{gdfzc>%bxFGouLMJO;36-w z_2Qm;9R82a5P*D!DC35{;h?`i(0RpwgGnRt!r~Z;14JD^I+u(!D-st3XDDsk3!bp5 z>i%_hQ5EzKbghN~jGGtq@p!o#*qVd4D@3$Y^(oSx^XfQsL~B;jzzvP0+OQNt2)~t2 zMG?4wW{LsHM}7yORLrBLvrxF~Q!0L{0}^c{`=Y(fV!e1C=UZ_Aovq?}A?7K2X=2YI zaOPAr5_r6@)B!4S#1ml|p_H`i57WIsEDVQ>_{ydpnk%pT5sy2ga4PR8YV_~5ze2jG z#qV2dmxLU#BSRWL83kC)E=h$^>>s>S=IBYK*>Lnp4hjbn8p#26Tlyw;zhyKL>RbpLqbYFW)ro_QnR0t zPNXr#L5kzm3}XmSi&;bVtLDP`XKodukjcnzH=I?71`q|rs%cW63WpmnTyn$!#*J_+ zqgvqfj3P{}9vM$z5Q9OBfQD0Np~&THT0PxvfYU`I!T&7g5BcaPqg*j3k5eP(Jc3#% zRJdJ23RKRx>=!!8H$S}BGxj%4>Me+86KGHdSsBH0QK1J%$duzEyiBlYvE-7(Z8o0P zI->yAl$xEYZ*f$v(pX3+p?YeoOsk#}@YVi4zBQ$9@F@DmhcCyk(kFP~}fExfJ+Uv57ne3dv@MM3gw^ z=jNv7l>oU649-QVi6yBi3gww484B*6z5ywEsq8@UubwWBAsXjPC!a1nY{27kzvxEH zEMGxQp4o?)Ox{kcoW%C1PVw)be>3g4dw6f?NVgbn*!Vjz#~{HVXS(AT?gghx9x@#; zyUn(Otzg4~8vzd#-o$7wQ477&a>c|y*;qZN*4btt9r6yF5ef!>-TGo&2=N1%{pg=DxW(l_rL1#Md|*afTT`XrDs{njWV{Je0yuc zjbp|MJCDt|x$E~wUGbWo$Aw=90#oipfkMX9)z4*}Q$iB} DJ9MLM diff --git a/doc/source/mimic/static/grycap.css b/doc/source/mimic/static/grycap.css deleted file mode 100644 index d03214ae7..000000000 --- a/doc/source/mimic/static/grycap.css +++ /dev/null @@ -1,259 +0,0 @@ -* { - padding: 0; - margin: 0; -} -body { - text-align: center; - background-color: #aaa; - background: url(bg2.jpg) no-repeat; -} -#main-wrapper { - width:900px; - margin:0 auto; - margin-top: 32px; -} -div.break { - clear: both; -} -div.hole { - background: transparent; - height: 24px; -} - -#code{ - margin-left: 40px; - padding-left: 10px; - width: 800px; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #FFF; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; - position: relative; - top: 3px; - left: 0px; - text-align: justify; - box-shadow: 0px 0px 40px #acc; - color: #034d80; - font-family: Courier; - font-size: 14px; -} - -#logo { - width: 900px; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #AAAA; - height: 128px; - box-shadow: 0px 0px 40px #acc; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; -} -#logo .img { - float: left; - position: relative; - top: 15px; - left: 52px; - padding-right: 10px; -} -#logo .text { - position: relative; - top: 3px; - left: 52px; - text-align: left; - height: 98px; - color: #034d80; - font-family: Helvetica, Arial, Verdana, sans-serif; - font-size: 90px; - text-shadow: 2px 2px 4px #575; -} -#logo .min { - position: relative; - left: -75px; - top: 21px; - padding-top: 21px; /* para explorer */ - font-size: 14px; - height: 32px; -} - - -#menu { - width: 100%; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #fff; - text-align: left; - height: 48px; - background: url(fondobarra2.png) -} -#menu-text { - position: relative; - top: 14px; - margin: 0px 10px; - text-align: center; -} -#menu-text a { - color: #fff; - font-family: Helvetica, Arial, Verdana, sans-serif; - font-size: 16px; - text-decoration: none; -} -#menu-text li { - display: inline; - padding: 0 16px; - border-right: 1px solid #aca; -} -#menu-text li.last { - border-right: 0px; -} -#menu-text ul { - list-style-type: none; - display: block; -} -#content { - width: 100%; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #fff; - font-family: Helvetica, Arial, Verdana, sans-serif; - font-size: 14px; - color: #367; - text-decoration: none; - text-align: left; - background: #fff no-repeat 0 0; - box-shadow: 0px 0px 40px #acc; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; -} -#content h1 { - font-size: 32px; - padding: 48px 52px 16px 52px; -} -#content h2 { - font-size: 24px; - padding: 16px 52px 0px 52px; -} -#content h3 { - padding: 16px 52px 0px 52px; -} -#content p, #content dd { - font-size: 14px; - padding: 8px 52px; - text-align: justify; - line-height: 22px; - with: 100%; -} -#content dd table { - margin: 5px 0px; -} -#content ol, #content ul { - font-size: 14px; - padding: 0px 56px; - text-align: justify; - line-height: 22px; - width: 100%; -} -#content dd p, #content dd dl, #content li pre, #content dd ul { - padding: 0px; - margin: 0px; -} - -#content li { - /*padding-right: 128px; /* los 72 + 56 */ - padding-right: 124px; - padding-bottom: 8px; - /*padding-top: 8px;*/ - position: relative; - left: 16px; -} -#content li p { - padding: 8px 0px; -} -#content a { - color: #367; - font-weight: bold; - /*text-decoration: underline;*/ -} - -#content table { - /* padding-left: 30px; */ -} -#content table thead td { - text-align: center; - font-weight: bold; -} -/*#content tbody td { - text-align: right; - padding: 5px 5px; -}*/ -#content th { - color:#fff; - background-color: #367; - padding: 7px 7px; - border-radius: 10px; - -moz-border-radius: 10px; - box-shadow: 0px 0px 40px #acc; - -moz-box-shadow: 0px 0px 40px #acc; - -webkit-box-shadow: 0px 0px 40px #acc; -} -.credits { - font-size: 11px; - text-align: center; - padding-top: 32px; -} -.italic { - font-style: italic; -} -.destacado { - font-weight: bold; -} -#content img { - /* padding: 16px 52px; */ - border: 0px; - margin: 16px 52px; -} -#content div { - /*padding: 16px 52px;*/ -} -#content .right { - float: right; -} -#content .center { - clear: both; - text-align: center; -} -#content .left { - float: left; -} -.sup { - vertical-align: baseline; - font-size: 60%; - position: relative; - top: -0.4em; -} -.foot { -} -#content p.legend { - text-align: center; - font-size: 10px; - padding-top: 0; - padding-bottom: 0; -} -#content input { - width: 300px; -} -#content textarea { - width: 300px; - height: 200px; -} -#content input.button { - width: 100px; -} -.figure { - text-align: center; -} -#content p.caption { - text-align: center; - font-weight: bold; -} - diff --git a/doc/source/mimic/static/headerbg.png b/doc/source/mimic/static/headerbg.png deleted file mode 100644 index 0c5b3657c8538e47be88b49daf91600a7936d9f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^j6i&XgAGV(e(L@T5-1LGcVbv~PUa<$!O>_%)r1c48n{Iv*t(u1=&kHeO=jau(EMTTkVrxeh(-lTjCl~;+&tG zo0?a`;9QiNSdyBeP@Y+mp%9Xhs^ISF8}L3wH4mt;(bL5-MC1J2NsfF+3^;%INZK0?I;tz}`+&p{Dl`~u9(3Asz)WZ+$`CX=Y+Iks# z_Whqtry{(T3QcIKdUb!%sly?2pUm6rIDvn&vC*!SxH-S>#$<{eGMi$)bL;X?i*&U79cByE8EyMeEq2?`bm%ar$hQZU-&t;ucLK6Vy|8TDW diff --git a/doc/source/mimic/static/logo.png b/doc/source/mimic/static/logo.png deleted file mode 100644 index 6dc222dfece0b1d32198839d132d4af693b67404..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15002 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 diff --git a/doc/source/mimic/static/metal.png b/doc/source/mimic/static/metal.png deleted file mode 100644 index 97166f131933d6808f8098ba09075c4f973dbe6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21543 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 NPFq9&ezh7R;y>T5HH!cM diff --git a/doc/source/mimic/static/navigation.png b/doc/source/mimic/static/navigation.png deleted file mode 100644 index 1e248d4d755d58f2853b3d2b9ffab262ba2580c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^j6kfx!2~2XTwzxL2^0spJ29*~C-V}>;VkfoEM{Qf z76xHPhFNnYfP(BLp1!W^H(1#?)wo;Ux8?$cBuiW)N}Tg^b5rw57@Uhz6H8K46v{J8 zG8EiBeFMT9`NV;W+&oSK!JzF5va%UOMF_{=LUyQn{?j#u3pt+cxkl_0X34dAH}yUGwerZ-)GxITKIalimh2k-^i|&t;uc GLK6TV%tagk diff --git a/doc/source/mimic/static/print.css b/doc/source/mimic/static/print.css deleted file mode 100644 index 715d90ab6..000000000 --- a/doc/source/mimic/static/print.css +++ /dev/null @@ -1,7 +0,0 @@ -@media print { - div.header, div.relnav, #toc { display: none; } - #contentwrapper { padding: 0; margin: 0; border: none; } - body { color: black; background-color: white; } - div.footer { border-top: 1px solid #888; color: #888; margin-top: 1cm; } - div.footer a { text-decoration: none; } -} diff --git a/doc/source/mimic/static/scrolls.css_t b/doc/source/mimic/static/scrolls.css_t deleted file mode 100644 index 9e171b7f0..000000000 --- a/doc/source/mimic/static/scrolls.css_t +++ /dev/null @@ -1,434 +0,0 @@ -/* - * scrolls.css_t - * ~~~~~~~~~~~~~ - * - * Sphinx stylesheet -- scrolls theme. - * - * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -body { - background-color: #222; - margin: 0; - padding: 0; - font-family: 'Georgia', serif; - font-size: 15px; - color: #eee; -} - -div.footer { - color: black; - padding: 8px; - font-size: 11px; - text-align: center; - letter-spacing: 0.5px; -} - -div.header { - margin: 0 -15px 0 -15px; - background: url(headerbg.png) repeat-x; - border-top: 6px solid {{ theme_headerbordercolor }}; -} - -div.relnav { - border-bottom: 1px solid #111; - background: url(navigation.png); - margin: 0 -15px 0 -15px; - padding: 2px 20px 0 28px; - line-height: 25px; - color: #aaa; - font-size: 12px; - text-align: center; -} - -div.relnav a { - color: #eee; - font-weight: bold; - text-decoration: none; -} - -div.relnav a:hover { - text-decoration: underline; -} - -/* -#content { - background-color: white; - color: #111; - border-bottom: 1px solid black; - background: url(watermark.png) center 0; - padding: 0 15px 0 15px; - margin: 0; -} - -h1 { - margin: 0; - padding: 15px 0 0 0; -} - -h1.heading { - margin: 0; - padding: 0; - height: 80px; -} - -h1.heading:hover { - background: #222; -} - -h1.heading a { - background: url({{ logo if logo else 'logo.png' }}) no-repeat center 0; - display: block; - width: 100%; - height: 80px; -} - -h1.heading a:focus { - -moz-outline: none; - outline: none; -} - -h1.heading span { - display: none; -} - -#contentwrapper { - max-width: 680px; - padding: 0 18px 20px 18px; - margin: 0 auto 0 auto; - border-right: 1px solid #eee; - border-left: 1px solid #eee; - background: url(watermark_blur.png) center -114px; -} - - -#contentwrapper h2, -#contentwrapper h2 a { - color: #222; - font-size: 24px; - margin: 20px 0 0 0; -} - -#contentwrapper h3, -#contentwrapper h3 a { - color: {{ theme_subheadlinecolor }}; - font-size: 20px; - margin: 20px 0 0 0; -} -*/ - -table.docutils { - border-collapse: collapse; - /* border: 2px solid #aaa; */ - margin: 5px 52px; -} - -table.docutils td { - padding: 2px; - /* border: 1px solid #ddd; */ -} - -p, li, dd, dt, blockquote { - color: #333; -} - -blockquote { - margin: 10px 0 10px 20px; -} - -/* -p { - line-height: 20px; - margin-bottom: 0; - margin-top: 10px; -} -*/ -hr { - border-top: 1px solid #ccc; - border-bottom: 0; - border-right: 0; - border-left: 0; - margin-bottom: 10px; - margin-top: 20px; -} - -dl { - margin-left: 52px; -} - -li, dt { - margin-top: 5px; -} - -dt { - font-weight: bold; - color: #000; -} - -dd { - line-height: 20px; -} - -th { - text-align: center; - padding: 3px; - background-color: #f2f2f2; -} - -a { - color: {{ theme_linkcolor }}; -} - -a:hover { - color: {{ theme_visitedlinkcolor }}; -} - -pre { - background: #ededed url(metal.png); - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; - padding: 5px 5px; - margin: 0px 52px; - font-size: 13px; - font-family: 'Bitstream Vera Sans Mono', 'Monaco', monospace; -} - -tt { - font-size: 13px; - font-family: 'Bitstream Vera Sans Mono', 'Monaco', monospace; - color: black; - padding: 1px 2px 1px 2px; - /*background-color: #fafafa; */ -} - -a.reference:hover tt { - border-bottom-color: #aaa; -} - -cite { - /* abusing , it's generated by ReST for `x` */ - font-size: 13px; - font-family: 'Bitstream Vera Sans Mono', 'Monaco', monospace; - font-weight: bold; - font-style: normal; -} - -div.admonition { - margin: 0px 52px; - border: 1px solid #ccc; -} - -div.admonition p.admonition-title { - background-color: {{ theme_admonitioncolor }}; - color: white; - font-weight: bold; - font-size: 15px; -} - -div.admonition p.admonition-title a { - color: white!important; -} - -a.headerlink { - color: #B4B4B4!important; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none!important; - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -a.headerlink:hover { - background-color: #B4B4B4; - color: #F0F0F0!important; -} - -table.indextable { - width: 100%; -} - -table.genindextable td { - vertical-align: top; - width: 50%; -} - -table.indextable dl dd { - font-size: 11px; -} - -table.indextable dl dd a { - color: #000; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -table.modindextable { - width: 100%; - border: none; -} - -table.modindextable img.toggler { - margin-right: 10px; -} - -dl.function dt, -dl.class dt, -dl.exception dt, -dl.method dt, -dl.attribute dt { - font-weight: normal; -} - -dt .descname { - font-weight: bold; - margin-right: 4px; -} - -dt .sig-paren { - font-size: larger; -} - -dt .descname, dt .descclassname { - padding: 0; - background: transparent; -} - -dt .descclassname { - margin-left: 2px; -} - -dl dt big { - font-size: 100%; -} - -ul.search { - margin: 10px 0 0 30px; - padding: 0; -} - -ul.search li { - margin: 10px 0 0 0; - padding: 0; -} - -ul.search div.context { - font-size: 12px; - padding: 4px 0 0 20px; - color: #888; -} - -span.highlight { - background-color: #eee; - border: 1px solid #ccc; -} - -div.highlight { - background: none repeat scroll 0% 0% #FFF; -} - -#toc { - margin: 0 -17px 0 -17px; - display: none; -} - -#toc h3 { - float: right; - margin: 5px 5px 0 0; - padding: 0; - font-size: 12px; - color: #777; -} - -#toc h3:hover { - color: #333; - cursor: pointer; -} - -.expandedtoc { - background: #222 url(darkmetal.png); - border-bottom: 1px solid #111; - outline-bottom: 1px solid #000; - padding: 5px; -} - -.expandedtoc h3 { - color: #aaa; - margin: 0!important; -} - -.expandedtoc h3:hover { - color: white!important; -} - -#tod h3:hover { - color: white; -} - -#toc a { - color: #ddd; - text-decoration: none; -} - -#toc a:hover { - color: white; - text-decoration: underline; -} - -#toc ul { - margin: 5px 0 12px 17px; - padding: 0 7px 0 7px; -} - -#toc ul ul { - margin-bottom: 0; -} - -#toc ul li { - margin: 2px 0 0 0; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: 'Georgia', serif; -} - -div.viewcode-block:target { - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; - margin: -1px -5px; - padding: 0 5px; -} diff --git a/doc/source/mimic/static/theme_extras.js b/doc/source/mimic/static/theme_extras.js deleted file mode 100644 index a21bff59b..000000000 --- a/doc/source/mimic/static/theme_extras.js +++ /dev/null @@ -1,26 +0,0 @@ -$(function() { - -/* var - toc = $('#toc').show(), - items = $('#toc > ul').hide(); - - $('#toc h3') - .click(function() { - if (items.is(':visible')) { - items.animate({ - height: 'hide', - opacity: 'hide' - }, 300, function() { - toc.removeClass('expandedtoc'); - }); - } - else { - items.animate({ - height: 'show', - opacity: 'show' - }, 400); - toc.addClass('expandedtoc'); - } - }); -*/ -}); diff --git a/doc/source/mimic/static/watermark.png b/doc/source/mimic/static/watermark.png deleted file mode 100644 index eb1b6be957b2f137a0667b35f43c0d4cb6291cf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107625 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 Ii5my~Kde%1k^lez diff --git a/doc/source/mimic/static/watermark_blur.png b/doc/source/mimic/static/watermark_blur.png deleted file mode 100644 index 563f6cde06f058a9e1dcec895265c0445822a5df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14470 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 C8W-6B diff --git a/doc/source/mimic/theme.conf b/doc/source/mimic/theme.conf deleted file mode 100644 index 4e7800f90..000000000 --- a/doc/source/mimic/theme.conf +++ /dev/null @@ -1,11 +0,0 @@ -[theme] -inherit = basic -stylesheet = scrolls.css -pygments_style = tango - -[options] -headerbordercolor = #1752b4 -subheadlinecolor = #0d306b -linkcolor = #1752b4 -visitedlinkcolor = #444 -admonitioncolor = #28437f From 1bf0abfb5f6e97b234786fa151d8041530898055 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 5 Nov 2015 10:48:50 +0100 Subject: [PATCH 025/509] Merge devel branch --- IM/InfrastructureManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 7ac5aed03..640b75e3b 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -37,6 +37,7 @@ from IM.tosca.Tosca import Tosca from config import Config +from IM.VirtualMachine import VirtualMachine if Config.MAX_SIMULTANEOUS_LAUNCHES > 1: from multiprocessing.pool import ThreadPool From 4cd8a292738f26a79536da80b70edf48b6390dee Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 5 Nov 2015 10:48:50 +0100 Subject: [PATCH 026/509] Merge devel branch --- IM/InfrastructureManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 7ac5aed03..640b75e3b 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -37,6 +37,7 @@ from IM.tosca.Tosca import Tosca from config import Config +from IM.VirtualMachine import VirtualMachine if Config.MAX_SIMULTANEOUS_LAUNCHES > 1: from multiprocessing.pool import ThreadPool From 948bea78f66a1369d0b161655c5755974f44cec9 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 6 Nov 2015 11:26:35 +0100 Subject: [PATCH 027/509] Complete the ElasticCluster definition and associated recipes --- IM/InfrastructureManager.py | 2 + IM/tosca/Tosca.py | 123 ++++++++++------ IM/tosca/artifacts/ec3/ec3_configure.yml | 131 ++++++++++++++++++ IM/tosca/artifacts/ec3/ec3_install.yml | 57 ++++++++ IM/tosca/artifacts/ec3/ec3_start.yml | 33 +++++ IM/tosca/artifacts/lrms/torque_configure.yml | 23 +++ IM/tosca/artifacts/lrms/torque_install.yml | 13 ++ IM/tosca/artifacts/lrms/torque_start.yml | 11 ++ .../artifacts/lrms/torque_wn_configure.yml | 21 +++ IM/tosca/artifacts/lrms/torque_wn_install.yml | 13 ++ IM/tosca/artifacts/lrms/torque_wn_start.yml | 11 ++ IM/tosca/custom_types.yaml | 125 +++++++++++------ examples/clues_tosca.yml | 62 +++++++-- examples/galaxy_tosca.yml | 2 +- 14 files changed, 534 insertions(+), 93 deletions(-) create mode 100644 IM/tosca/artifacts/ec3/ec3_configure.yml create mode 100644 IM/tosca/artifacts/ec3/ec3_install.yml create mode 100644 IM/tosca/artifacts/ec3/ec3_start.yml create mode 100644 IM/tosca/artifacts/lrms/torque_configure.yml create mode 100644 IM/tosca/artifacts/lrms/torque_install.yml create mode 100644 IM/tosca/artifacts/lrms/torque_start.yml create mode 100644 IM/tosca/artifacts/lrms/torque_wn_configure.yml create mode 100644 IM/tosca/artifacts/lrms/torque_wn_install.yml create mode 100644 IM/tosca/artifacts/lrms/torque_wn_start.yml diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 640b75e3b..913d273c8 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -810,6 +810,8 @@ def GetInfrastructureState(inf_id, auth): state = None for vm in sel_inf.get_vm_list(): + # First try yo update the status of the VM + vm.update_status(auth) if vm.state == VirtualMachine.FAILED: state = VirtualMachine.FAILED break diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index dbb87db56..dba3bf2c7 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -4,13 +4,63 @@ import copy import tempfile -from toscaparser.tosca_template import ToscaTemplate +import toscaparser.imports +from toscaparser.tosca_template import ToscaTemplate as toscaparser_ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize from toscaparser.utils.yamlparser import load_yaml +class ToscaTemplate(toscaparser_ToscaTemplate): + + CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" + CUSTOM_IMPORT_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_import_types.yaml" + + def __init__(self, path, parsed_params=None, a_file=True): + # Load custom data + custom_def = load_yaml(self.CUSTOM_TYPES_FILE) + # and update tosca_def with the data + EntityType.TOSCA_DEF.update(custom_def) + + super(ToscaTemplate, self).__init__(path, parsed_params, a_file) + + def _get_custom_types(self, type_definitions, imports=None): + """Handle custom types defined in imported template files + + This method loads the custom type definitions referenced in "imports" + section of the TOSCA YAML template. + """ + + custom_defs = {} + type_defs = [] + if not isinstance(type_definitions, list): + type_defs.append(type_definitions) + else: + type_defs = type_definitions + + if not imports: + imports = self._tpl_imports() + + # Enable to add INDIGO custom definitions + if not imports: + imports = [{"indigo_defs": self.CUSTOM_IMPORT_FILE}] + else: + imports.append({"indigo_defs": self.CUSTOM_IMPORT_FILE}) + + if imports: + custom_defs = toscaparser.imports.\ + ImportsLoader(imports, self.path, + type_defs).get_custom_defs() + + # Handle custom types defined in current template file + for type_def in type_defs: + if type_def != "imports": + inner_custom_types = self.tpl.get(type_def) or {} + if inner_custom_types: + custom_defs.update(inner_custom_types) + return custom_defs + class Tosca: """ Class to translate a TOSCA document to an RADL object. @@ -20,16 +70,10 @@ class Tosca: """ ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" - CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" logger = logging.getLogger('InfrastructureManager') def __init__(self, yaml_str): - # Load custom data - custom_def = load_yaml(Tosca.CUSTOM_TYPES_FILE) - # and update tosca_def with the data - EntityType.TOSCA_DEF.update(custom_def) - self.tosca = None # write the contents to a file as ToscaTemplate needs with tempfile.NamedTemporaryFile(suffix=".yaml") as f: @@ -87,8 +131,16 @@ def to_radl(self): sys = Tosca._gen_system(node, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system - dep = deploy(sys.name, 1) - radl.deploys.append(dep) + min_instances, _, default_instances = Tosca._get_scalable_properties(node) + if default_instances is not None: + num_instances = default_instances + elif min_instances is not None: + num_instances = min_instances + else: + num_instances = 1 + if num_instances > 0: + dep = deploy(sys.name, num_instances) + radl.deploys.append(dep) compute = node else: # Select the host to host this element @@ -103,13 +155,30 @@ def to_radl(self): if conf: level = Tosca._get_dependency_level(node) radl.configures.append(conf) - cont_intems.append(contextualize_item(compute.name, conf.name, level)) + if compute: + cont_intems.append(contextualize_item(compute.name, conf.name, level)) if cont_intems: radl.contextualize = contextualize(cont_intems) return self._complete_radl_networks(radl) + @staticmethod + def _get_scalable_properties(node): + min_instances = max_instances = default_instances = None + scalable = node.get_capability("scalable") + if scalable: + for prop in scalable.get_properties_objects(): + if prop.value is not None: + if prop.name == "max_instances": + max_instances = prop.value + elif prop.name == "min_instances": + min_instances = prop.value + elif prop.name == "default_instances": + default_instances = prop.value + + return min_instances, max_instances, default_instances + @staticmethod def _get_relationship_template(rel, src, trgt): rel_tpls = src.get_relationship_template() @@ -464,7 +533,7 @@ def _node_fulfill_filter(node, node_filter): for cap_type in ['os', 'host']: if node.get_capability(cap_type): for prop in node.get_capability(cap_type).get_properties_objects(): - if prop.value: + if prop.value is not None: unit = None value = prop.value if prop.name in ['disk_size', 'mem_size']: @@ -690,35 +759,7 @@ def _gen_system(node, nodetemplates): @staticmethod def _get_bind_networks(node, nodetemplates): nets = [] - count = 0 - for requires in node.requirements: - for value in requires.values(): - name = None - ip = None - dns_name = None - if isinstance(value, dict): - if 'relationship' in value: - rel = value.get('relationship') - - rel_type = None - if isinstance(rel, dict) and 'type' in rel: - rel_type = rel.get('type') - else: - rel_type = rel - - if rel_type and rel_type.endswith("BindsTo"): - if isinstance(rel, dict) and 'properties' in rel: - prop = rel.get('properties') - if isinstance(prop, dict): - ip = prop.get('ip', None) - dns_name = prop.get('dns_name', None) - - name = value.values()[0] - nets.append((name, ip, dns_name, count)) - count += 1 - else: - Tosca.logger.error("ERROR: expected dict in requires values.") - + for port in nodetemplates: root_type = Tosca._get_root_parent_type(port).type if root_type == "tosca.nodes.network.Port": @@ -731,7 +772,7 @@ def _get_bind_networks(node, nodetemplates): if binding == node.name: ip = port.get_property_value('ip_address') order = port.get_property_value('order') - dns_name = None + dns_name = port.get_property_value('dns_name') nets.append((link, ip, dns_name, order)) return nets diff --git a/IM/tosca/artifacts/ec3/ec3_configure.yml b/IM/tosca/artifacts/ec3/ec3_configure.yml new file mode 100644 index 000000000..dce601e2e --- /dev/null +++ b/IM/tosca/artifacts/ec3/ec3_configure.yml @@ -0,0 +1,131 @@ +--- +- handlers: + - name: restart cluesd + service: name=cluesd state=restarted + + vars: + TORQUE_PATH: /var/spool/torque + PBS_SERVER_CONF: | + create queue batch + set queue batch queue_type = Execution + set queue batch resources_default.nodes = 1 + set queue batch enabled = True + set queue batch started = True + set server default_queue = batch + set server scheduling = True + set server scheduler_iteration = 20 + set server node_check_rate = 40 + set server resources_default.neednodes = 1 + set server resources_default.nodect = 1 + set server resources_default.nodes = 1 + set server query_other_jobs = True + set server node_pack = False + set server job_stat_rate = 30 + set server mom_job_sync = True + set server poll_jobs = True + set tcp_timeout = 600 + + tasks: + # PBS configuration + - file: src=/var/lib/torque dest=/var/spool/torque state=link + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + + - command: hostname torqueserver + when: clues_queue_system == 'torque' + + - shell: | + for i in `seq 1 {{max_instances}}`; do + item="{{wn_name}}${i}"; + grep -q "\<${item}\>" /etc/hosts || echo "127.0.0.1 ${item}.localdomain ${item}" >> /etc/hosts; + done + when: clues_queue_system == 'torque' + + - copy: dest=/etc/torque/server_name content=torqueserver + when: clues_queue_system == 'torque' + - copy: + content: | + {% for number in range(1, max_instances|int + 1) %} + vnode{{number}} + {% endfor %} + dest: "{{TORQUE_PATH}}/server_priv/nodes" + when: clues_queue_system == 'torque' + + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + + - shell: echo "{{PBS_SERVER_CONF}}" | qmgr creates={{TORQUE_PATH}}/server_priv/queues/batch + when: clues_queue_system == 'torque' + + # CLUES2 Config file + - file: path=/etc/clues2 state=directory mode=755 + + - copy: src=/etc/clues2/clues2.cfg-full-example dest=/etc/clues2/clues2.cfg force=no + notify: restart cluesd + + - ini_file: dest=/etc/clues2/clues2.cfg section={{ item.section }} option={{ item.option }} value="{{ item.value }}" + with_items: + - { section: 'general', option: 'POWERMANAGER_CLASS', value: 'cluesplugins.im' } + - { section: 'scheduler_power_off_idle', option: 'IDLE_TIME', value: '300' } + - { section: 'monitoring', option: 'MAX_WAIT_POWERON', value: '2000' } + - { section: 'monitoring', option: 'MAX_WAIT_POWEROFF', value: '600' } + - { section: 'monitoring', option: 'PERIOD_LIFECYCLE', value: '10' } + - { section: 'general', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } + - { section: 'client', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } + - { section: 'client', option: 'CLUES_REQUEST_WAIT_TIMEOUT', value: '0' } + notify: restart cluesd + + # CLUES IM configuration + - file: path=/usr/local/ec3 state=directory mode=755 + - copy: dest=/usr/local/ec3/auth.dat content="type = InfrastructureManager; username = user; password = pass" + - copy: dest=/usr/local/ec3/wn_info.yml content={{wn_host_info}}\n-\n{{wn_os_info}}\n-\n{{wn_name}}\n-\n{{wn_node_type}} + + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs + notify: restart cluesd + + # CLUES PBS configuration + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs + notify: restart cluesd + when: clues_queue_system == 'torque' + - copy: src=/etc/clues2/conf.d/plugin-pbs.cfg-example dest=/etc/clues2/conf.d/plugin-pbs.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'torque' + - ini_file: dest=/etc/clues2/conf.d/plugin-pbs.cfg section=PBS option=PBS_SERVER value=torqueserver + notify: restart cluesd + when: clues_queue_system == 'torque' + - lineinfile: dest={{TORQUE_PATH}}/torque.cfg regexp=^SUBMITFILTER line='SUBMITFILTER /usr/local/bin/clues-pbs-wrapper' create=yes mode=644 + when: clues_queue_system == 'torque' + + # CLUES SGE configuration + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.sge + notify: restart cluesd + when: clues_queue_system == 'sge' + - copy: src=/etc/clues2/conf.d/plugin-sge.cfg-example dest=/etc/clues2/conf.d/plugin-sge.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'sge' + - copy: src=/etc/clues2/conf.d/wrapper-sge.cfg-example dest=/etc/clues2/conf.d/wrapper-sge.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'sge' + - lineinfile: dest={{SGE_ROOT}}/default/common/sge_request regexp='^-jsv' line="-jsv /usr/local/bin/clues-sge-wrapper" create=yes mode=644 + when: clues_queue_system == 'sge' + - lineinfile: dest=/etc/profile.d/sge_vars.sh regexp='SGE_JSV_TIMEOUT' line="export SGE_JSV_TIMEOUT=600" create=yes mode=755 + when: clues_queue_system == 'sge' + + # CLUES SLURM configuration + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.slurm + notify: restart cluesd + when: clues_queue_system == 'slurm' + - copy: src=/etc/clues2/conf.d/plugin-slurm.cfg-example dest=/etc/clues2/conf.d/plugin-slurm.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'slurm' + - ini_file: dest=/etc/clues2/conf.d/plugin-slurm.cfg section=SLURM option=SLURM_SERVER value=slurmserverpublic + notify: restart cluesd + when: clues_queue_system == 'slurm' + - command: mv /usr/local/bin/sbatch /usr/local/bin/sbatch.o creates=/usr/local/bin/sbatch.o + when: clues_queue_system == 'slurm' + - command: mv /usr/local/bin/clues-slurm-wrapper /usr/local/bin/sbatch creates=/usr/local/bin/sbatch + when: clues_queue_system == 'slurm' + + + \ No newline at end of file diff --git a/IM/tosca/artifacts/ec3/ec3_install.yml b/IM/tosca/artifacts/ec3/ec3_install.yml new file mode 100644 index 000000000..ad9c88024 --- /dev/null +++ b/IM/tosca/artifacts/ec3/ec3_install.yml @@ -0,0 +1,57 @@ +--- +- tasks: + # General task + - name: create epel.repo + template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo + when: ansible_os_family == "RedHat" + + # CLUES2 requirements + - name: Apt install CLUES2 requirements in Deb system + apt: pkg=python-sqlite,unzip + when: ansible_os_family == "Debian" + + - name: Yum install CLUES2 requirements in REL system + yum: pkg=python-sqlite2,unzip + when: ansible_os_family == "RedHat" + + - name: Install CLUES pip requirements + pip: name={{item}} + with_items: + - web.py + - ply + + - get_url: url=https://github.com/grycap/{{item}}/archive/master.zip dest=/tmp/{{item}}.zip + register: result + until: result|success + retries: 5 + delay: 1 + with_items: + - clues + - cpyutils + + # CLUES2 installation + - unarchive: src=/tmp/{{item}}.zip dest=/tmp copy=no + with_items: + - clues + - cpyutils + + - command: python setup.py install chdir=/tmp/clues-master creates=/usr/local/bin/cluesserver + - command: python setup.py install chdir=/tmp/cpyutils-master + + # IM installation + - apt: name=python-soappy update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + - yum: name=SOAPpy + when: ansible_os_family == "RedHat" + - pip: name=IM + - file: path=/etc/init.d/im mode=0755 + + + # PBS installation + - name: Apt install Torque in Deb system + apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + + - name: Yum install Torque in REL system + yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' diff --git a/IM/tosca/artifacts/ec3/ec3_start.yml b/IM/tosca/artifacts/ec3/ec3_start.yml new file mode 100644 index 000000000..cc1334ea6 --- /dev/null +++ b/IM/tosca/artifacts/ec3/ec3_start.yml @@ -0,0 +1,33 @@ +--- +- tasks: + # Launch CLUES + - service: name=cluesd state=started + # Launch IM + - service: name=im state=started + + # Launch PBS + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=torque-server state=restarted pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=pbs_server state=restarted pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + + - command: sleep 5 + when: clues_queue_system == 'torque' + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_configure.yml b/IM/tosca/artifacts/lrms/torque_configure.yml new file mode 100644 index 000000000..7d2a67d8b --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_configure.yml @@ -0,0 +1,23 @@ +--- + - vars: + PBS_SERVER_CONF: | + create queue batch + set queue batch queue_type = Execution + set queue batch resources_default.nodes = 1 + set queue batch enabled = True + set queue batch started = True + set server default_queue = batch + set server scheduling = True + set server scheduler_iteration = 20 + set server node_check_rate = 40 + set server resources_default.neednodes = 1 + set server resources_default.nodect = 1 + set server resources_default.nodes = 1 + set server query_other_jobs = True + set server node_pack = False + set server job_stat_rate = 30 + set server mom_job_sync = True + set server poll_jobs = True + set tcp_timeout = 600 + + \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_install.yml b/IM/tosca/artifacts/lrms/torque_install.yml new file mode 100644 index 000000000..7c76883c7 --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_install.yml @@ -0,0 +1,13 @@ +--- + - tasks: + - name: create epel.repo + template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo + when: ansible_os_family == "RedHat" + + - name: Apt install Torque in Deb system + apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + + - name: Yum install Torque in REL system + yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_start.yml b/IM/tosca/artifacts/lrms/torque_start.yml new file mode 100644 index 000000000..55c9f708b --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_start.yml @@ -0,0 +1,11 @@ +--- + - tasks: + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_configure.yml b/IM/tosca/artifacts/lrms/torque_wn_configure.yml new file mode 100644 index 000000000..bbe936985 --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_wn_configure.yml @@ -0,0 +1,21 @@ +--- + - vars: + PBS_SERVER_CONF: | + create queue batch + set queue batch queue_type = Execution + set queue batch resources_default.nodes = 1 + set queue batch enabled = True + set queue batch started = True + set server default_queue = batch + set server scheduling = True + set server scheduler_iteration = 20 + set server node_check_rate = 40 + set server resources_default.neednodes = 1 + set server resources_default.nodect = 1 + set server resources_default.nodes = 1 + set server query_other_jobs = True + set server node_pack = False + set server job_stat_rate = 30 + set server mom_job_sync = True + set server poll_jobs = True + set tcp_timeout = 600 \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_install.yml b/IM/tosca/artifacts/lrms/torque_wn_install.yml new file mode 100644 index 000000000..7c76883c7 --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_wn_install.yml @@ -0,0 +1,13 @@ +--- + - tasks: + - name: create epel.repo + template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo + when: ansible_os_family == "RedHat" + + - name: Apt install Torque in Deb system + apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + + - name: Yum install Torque in REL system + yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_start.yml b/IM/tosca/artifacts/lrms/torque_wn_start.yml new file mode 100644 index 000000000..55c9f708b --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_wn_start.yml @@ -0,0 +1,11 @@ +--- + - tasks: + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml index 4a2457328..6e4b8c66d 100644 --- a/IM/tosca/custom_types.yaml +++ b/IM/tosca/custom_types.yaml @@ -1,5 +1,14 @@ tosca_definitions_version: tosca_simple_yaml_1_0 +tosca.nodes.indigo.network.Port: + derived_from: tosca.nodes.network.Port + properties: + dns_name: + type: string + required: no + description: > + Allow the user to set a specific dns name. + tosca.nodes.Database.MySQL: derived_from: tosca.nodes.Database properties: @@ -112,7 +121,7 @@ tosca.nodes.indigo.GalaxyPortal: galaxy_install_path: { get_property: [ SELF, install_path ] } -tosca.nodes.indigo.GalaxyTool: +tosca.nodes.indigo.GalaxyShedTool: derived_from: tosca.nodes.WebApplication properties: name: @@ -144,58 +153,92 @@ tosca.nodes.indigo.GalaxyTool: galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } -tosca.capabilities.LRMS: + + + + + +tosca.capabilities.indigo.LRMS: derived_from: tosca.capabilities.Root properties: - lrms_type: + type: type: string required: true - -tosca.capabilities.LRMS.Torque: - derived_from: tosca.capabilities.LRMS - properties: - lrms_type: torque - + constraints: + - valid_values: [ torque, slurm, sge ] -tosca.nodes.indigo.LRMS: - derived_from: tosca.nodes.SoftwareComponent - -tosca.nodes.indigo.LRMS.FrontEnd: - derived_from: tosca.nodes.indigo.LRMS - capabilities: - cluster_endpoint: - type: tosca.capabilities.Endpoint - -tosca.nodes.indigo.LRMS.FrontEnd.Torque: - derived_from: tosca.nodes.indigo.LRMS.FrontEnd - capabilities: - lrms_front_end: - type: tosca.capabilities.LRMS.Torque - interfaces: - Standard: - create: lrms/torque_install.yml - configure: lrms/torque_configure.yml - start: lrms/torque_start.yml - -tosca.nodes.indigo.CLUES: - derived_from: tosca.nodes.SoftwareComponent + +tosca.nodes.indigo.ElasticCluster: + derived_from: tosca.nodes.Root properties: secret_token: type: string - description: Token to access the web interface + description: Token to access CLUES web interface default: not_very_secret_token required: false - requirements: - - lrms_front_end: - capability: tosca.capabilities.LRMS - node: tosca.nodes.indigo.LRMS.FrontEnd - relationship: tosca.relationships.HostedOn interfaces: Standard: - create: clues/clues_install.yml + create: ec3/ec3_install.yml configure: - implementation: clues/clues_configure.yml + implementation: ec3/ec3_configure.yml inputs: clues_secret_token: { get_property: [ SELF, secret_token ] } - clues_queue_system: { get_property: [ SELF, lrms_front_end, lrms_type ] } - start: clues/clues_start.yml \ No newline at end of file + clues_queue_system: { get_property: [ SELF, lrms, type ] } + max_instances: { get_property: [ SELF, scalable, max_instances] } + wn_host_info: { get_property: [ SELF, wn, host ] } + wn_os_info: { get_property: [ SELF, wn, os ] } + wn_name: { get_property: [ SELF, wn, name ] } + wn_node_type: { get_property: [ SELF, wn, type ] } + start: ec3/ec3_start.yml + capabilities: + scalable: + type: tosca.capabilities.Scalable + lrms: + type: tosca.capabilities.indigo.LRMS + requirements: + - wn: + capability: tosca.capabilities.indigo.WorkerNode + node: tosca.nodes.indigo.ElasticCluster.WorkerNode + relationship: tosca.relationships.indigo.Manages + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.Compute + relationship: tosca.relationships.HostedOn + + +tosca.nodes.indigo.ElasticCluster.WorkerNode: + derived_from: tosca.nodes.Root + capabilities: + wn: + type: tosca.capabilities.indigo.WorkerNode + valid_source_types: [tosca.nodes.indigo.ElasticCluster.FrontEnd] + + +tosca.nodes.indigo.LRMS.WorkerNode.Torque: + derived_from: tosca.nodes.Compute + interfaces: + Standard: + create: lrms/torque_wn_install.yml + configure: lrms/torque_wn_configure.yml + start: lrms/torque_wn_start.yml + + +tosca.capabilities.indigo.WorkerNode: + derived_from: tosca.capabilities.Root + properties: + name: + required: yes + type: string + type: + required: yes + type: string + host: + required: no + type: HostInfo + os: + required: no + type: OSInfo + + +tosca.relationships.indigo.Manages: + derived_from: tosca.relationships.Root \ No newline at end of file diff --git a/examples/clues_tosca.yml b/examples/clues_tosca.yml index 7c902ddbb..6bb783897 100644 --- a/examples/clues_tosca.yml +++ b/examples/clues_tosca.yml @@ -6,25 +6,56 @@ topology_template: node_templates: - clues: - type: tosca.nodes.indigo.CLUES + private: + type: tosca.nodes.network.Network + properties: + network_type: private + + public: + type: tosca.nodes.network.Network + properties: + network_type: public + + fe_public_net_port: + type: tosca.nodes.indigo.network.Port + properties: + order: 0 + dns_name: publicname + requirements: + - link: public + - binding: torque_server + + fe_private_net_port: + type: tosca.nodes.indigo.network.Port + properties: + order: 1 + dns_name: torqueserver requirements: - - lrms_front_end: front_end_torque + - link: private + - binding: torque_server - front_end_torque: - type: tosca.nodes.indigo.LRMS.FrontEnd.Torque + elastic_cluster: + type: tosca.nodes.indigo.ElasticCluster + capabilities: + lrms: + properties: + type: torque + scalable: + properties: + max_instances: 5 + min_instances: 0 + default_instances: 0 requirements: - - host: front_end_server - - front_end_server: + - host: torque_server + - wn: wn_node + + torque_server: type: tosca.nodes.Compute capabilities: - # Host container properties host: properties: num_cpus: 1 mem_size: 1 GB - # Guest Operating System properties os: properties: # host Operating System image properties @@ -32,3 +63,14 @@ topology_template: #distribution: scientific #version: 6.6 + wn_node: + type: tosca.nodes.indigo.ElasticCluster.WorkerNode + capabilities: + wn: + properties: + name: vnode + type: tosca.nodes.indigo.LRMS.WorkerNode.Torque + host: + num_cpus: 1 + os: + type: linux \ No newline at end of file diff --git a/examples/galaxy_tosca.yml b/examples/galaxy_tosca.yml index dc8e7a7e8..7912ea296 100644 --- a/examples/galaxy_tosca.yml +++ b/examples/galaxy_tosca.yml @@ -7,7 +7,7 @@ topology_template: node_templates: bowtie2_galaxy_tool: - type: tosca.nodes.indigo.GalaxyTool + type: tosca.nodes.indigo.GalaxyShedTool properties: name: bowtie2 owner: devteam From 1b3082aa38ec6de4831521fa612377707a49e039 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 6 Nov 2015 11:26:35 +0100 Subject: [PATCH 028/509] Complete the ElasticCluster definition and associated recipes --- IM/InfrastructureManager.py | 2 + IM/tosca/Tosca.py | 123 ++++++++++------ IM/tosca/artifacts/ec3/ec3_configure.yml | 131 ++++++++++++++++++ IM/tosca/artifacts/ec3/ec3_install.yml | 57 ++++++++ IM/tosca/artifacts/ec3/ec3_start.yml | 33 +++++ IM/tosca/artifacts/lrms/torque_configure.yml | 23 +++ IM/tosca/artifacts/lrms/torque_install.yml | 13 ++ IM/tosca/artifacts/lrms/torque_start.yml | 11 ++ .../artifacts/lrms/torque_wn_configure.yml | 21 +++ IM/tosca/artifacts/lrms/torque_wn_install.yml | 13 ++ IM/tosca/artifacts/lrms/torque_wn_start.yml | 11 ++ IM/tosca/custom_types.yaml | 125 +++++++++++------ examples/clues_tosca.yml | 62 +++++++-- examples/galaxy_tosca.yml | 2 +- 14 files changed, 534 insertions(+), 93 deletions(-) create mode 100644 IM/tosca/artifacts/ec3/ec3_configure.yml create mode 100644 IM/tosca/artifacts/ec3/ec3_install.yml create mode 100644 IM/tosca/artifacts/ec3/ec3_start.yml create mode 100644 IM/tosca/artifacts/lrms/torque_configure.yml create mode 100644 IM/tosca/artifacts/lrms/torque_install.yml create mode 100644 IM/tosca/artifacts/lrms/torque_start.yml create mode 100644 IM/tosca/artifacts/lrms/torque_wn_configure.yml create mode 100644 IM/tosca/artifacts/lrms/torque_wn_install.yml create mode 100644 IM/tosca/artifacts/lrms/torque_wn_start.yml diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 640b75e3b..913d273c8 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -810,6 +810,8 @@ def GetInfrastructureState(inf_id, auth): state = None for vm in sel_inf.get_vm_list(): + # First try yo update the status of the VM + vm.update_status(auth) if vm.state == VirtualMachine.FAILED: state = VirtualMachine.FAILED break diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index dbb87db56..dba3bf2c7 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -4,13 +4,63 @@ import copy import tempfile -from toscaparser.tosca_template import ToscaTemplate +import toscaparser.imports +from toscaparser.tosca_template import ToscaTemplate as toscaparser_ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize from toscaparser.utils.yamlparser import load_yaml +class ToscaTemplate(toscaparser_ToscaTemplate): + + CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" + CUSTOM_IMPORT_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_import_types.yaml" + + def __init__(self, path, parsed_params=None, a_file=True): + # Load custom data + custom_def = load_yaml(self.CUSTOM_TYPES_FILE) + # and update tosca_def with the data + EntityType.TOSCA_DEF.update(custom_def) + + super(ToscaTemplate, self).__init__(path, parsed_params, a_file) + + def _get_custom_types(self, type_definitions, imports=None): + """Handle custom types defined in imported template files + + This method loads the custom type definitions referenced in "imports" + section of the TOSCA YAML template. + """ + + custom_defs = {} + type_defs = [] + if not isinstance(type_definitions, list): + type_defs.append(type_definitions) + else: + type_defs = type_definitions + + if not imports: + imports = self._tpl_imports() + + # Enable to add INDIGO custom definitions + if not imports: + imports = [{"indigo_defs": self.CUSTOM_IMPORT_FILE}] + else: + imports.append({"indigo_defs": self.CUSTOM_IMPORT_FILE}) + + if imports: + custom_defs = toscaparser.imports.\ + ImportsLoader(imports, self.path, + type_defs).get_custom_defs() + + # Handle custom types defined in current template file + for type_def in type_defs: + if type_def != "imports": + inner_custom_types = self.tpl.get(type_def) or {} + if inner_custom_types: + custom_defs.update(inner_custom_types) + return custom_defs + class Tosca: """ Class to translate a TOSCA document to an RADL object. @@ -20,16 +70,10 @@ class Tosca: """ ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" - CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" logger = logging.getLogger('InfrastructureManager') def __init__(self, yaml_str): - # Load custom data - custom_def = load_yaml(Tosca.CUSTOM_TYPES_FILE) - # and update tosca_def with the data - EntityType.TOSCA_DEF.update(custom_def) - self.tosca = None # write the contents to a file as ToscaTemplate needs with tempfile.NamedTemporaryFile(suffix=".yaml") as f: @@ -87,8 +131,16 @@ def to_radl(self): sys = Tosca._gen_system(node, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system - dep = deploy(sys.name, 1) - radl.deploys.append(dep) + min_instances, _, default_instances = Tosca._get_scalable_properties(node) + if default_instances is not None: + num_instances = default_instances + elif min_instances is not None: + num_instances = min_instances + else: + num_instances = 1 + if num_instances > 0: + dep = deploy(sys.name, num_instances) + radl.deploys.append(dep) compute = node else: # Select the host to host this element @@ -103,13 +155,30 @@ def to_radl(self): if conf: level = Tosca._get_dependency_level(node) radl.configures.append(conf) - cont_intems.append(contextualize_item(compute.name, conf.name, level)) + if compute: + cont_intems.append(contextualize_item(compute.name, conf.name, level)) if cont_intems: radl.contextualize = contextualize(cont_intems) return self._complete_radl_networks(radl) + @staticmethod + def _get_scalable_properties(node): + min_instances = max_instances = default_instances = None + scalable = node.get_capability("scalable") + if scalable: + for prop in scalable.get_properties_objects(): + if prop.value is not None: + if prop.name == "max_instances": + max_instances = prop.value + elif prop.name == "min_instances": + min_instances = prop.value + elif prop.name == "default_instances": + default_instances = prop.value + + return min_instances, max_instances, default_instances + @staticmethod def _get_relationship_template(rel, src, trgt): rel_tpls = src.get_relationship_template() @@ -464,7 +533,7 @@ def _node_fulfill_filter(node, node_filter): for cap_type in ['os', 'host']: if node.get_capability(cap_type): for prop in node.get_capability(cap_type).get_properties_objects(): - if prop.value: + if prop.value is not None: unit = None value = prop.value if prop.name in ['disk_size', 'mem_size']: @@ -690,35 +759,7 @@ def _gen_system(node, nodetemplates): @staticmethod def _get_bind_networks(node, nodetemplates): nets = [] - count = 0 - for requires in node.requirements: - for value in requires.values(): - name = None - ip = None - dns_name = None - if isinstance(value, dict): - if 'relationship' in value: - rel = value.get('relationship') - - rel_type = None - if isinstance(rel, dict) and 'type' in rel: - rel_type = rel.get('type') - else: - rel_type = rel - - if rel_type and rel_type.endswith("BindsTo"): - if isinstance(rel, dict) and 'properties' in rel: - prop = rel.get('properties') - if isinstance(prop, dict): - ip = prop.get('ip', None) - dns_name = prop.get('dns_name', None) - - name = value.values()[0] - nets.append((name, ip, dns_name, count)) - count += 1 - else: - Tosca.logger.error("ERROR: expected dict in requires values.") - + for port in nodetemplates: root_type = Tosca._get_root_parent_type(port).type if root_type == "tosca.nodes.network.Port": @@ -731,7 +772,7 @@ def _get_bind_networks(node, nodetemplates): if binding == node.name: ip = port.get_property_value('ip_address') order = port.get_property_value('order') - dns_name = None + dns_name = port.get_property_value('dns_name') nets.append((link, ip, dns_name, order)) return nets diff --git a/IM/tosca/artifacts/ec3/ec3_configure.yml b/IM/tosca/artifacts/ec3/ec3_configure.yml new file mode 100644 index 000000000..dce601e2e --- /dev/null +++ b/IM/tosca/artifacts/ec3/ec3_configure.yml @@ -0,0 +1,131 @@ +--- +- handlers: + - name: restart cluesd + service: name=cluesd state=restarted + + vars: + TORQUE_PATH: /var/spool/torque + PBS_SERVER_CONF: | + create queue batch + set queue batch queue_type = Execution + set queue batch resources_default.nodes = 1 + set queue batch enabled = True + set queue batch started = True + set server default_queue = batch + set server scheduling = True + set server scheduler_iteration = 20 + set server node_check_rate = 40 + set server resources_default.neednodes = 1 + set server resources_default.nodect = 1 + set server resources_default.nodes = 1 + set server query_other_jobs = True + set server node_pack = False + set server job_stat_rate = 30 + set server mom_job_sync = True + set server poll_jobs = True + set tcp_timeout = 600 + + tasks: + # PBS configuration + - file: src=/var/lib/torque dest=/var/spool/torque state=link + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + + - command: hostname torqueserver + when: clues_queue_system == 'torque' + + - shell: | + for i in `seq 1 {{max_instances}}`; do + item="{{wn_name}}${i}"; + grep -q "\<${item}\>" /etc/hosts || echo "127.0.0.1 ${item}.localdomain ${item}" >> /etc/hosts; + done + when: clues_queue_system == 'torque' + + - copy: dest=/etc/torque/server_name content=torqueserver + when: clues_queue_system == 'torque' + - copy: + content: | + {% for number in range(1, max_instances|int + 1) %} + vnode{{number}} + {% endfor %} + dest: "{{TORQUE_PATH}}/server_priv/nodes" + when: clues_queue_system == 'torque' + + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + + - shell: echo "{{PBS_SERVER_CONF}}" | qmgr creates={{TORQUE_PATH}}/server_priv/queues/batch + when: clues_queue_system == 'torque' + + # CLUES2 Config file + - file: path=/etc/clues2 state=directory mode=755 + + - copy: src=/etc/clues2/clues2.cfg-full-example dest=/etc/clues2/clues2.cfg force=no + notify: restart cluesd + + - ini_file: dest=/etc/clues2/clues2.cfg section={{ item.section }} option={{ item.option }} value="{{ item.value }}" + with_items: + - { section: 'general', option: 'POWERMANAGER_CLASS', value: 'cluesplugins.im' } + - { section: 'scheduler_power_off_idle', option: 'IDLE_TIME', value: '300' } + - { section: 'monitoring', option: 'MAX_WAIT_POWERON', value: '2000' } + - { section: 'monitoring', option: 'MAX_WAIT_POWEROFF', value: '600' } + - { section: 'monitoring', option: 'PERIOD_LIFECYCLE', value: '10' } + - { section: 'general', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } + - { section: 'client', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } + - { section: 'client', option: 'CLUES_REQUEST_WAIT_TIMEOUT', value: '0' } + notify: restart cluesd + + # CLUES IM configuration + - file: path=/usr/local/ec3 state=directory mode=755 + - copy: dest=/usr/local/ec3/auth.dat content="type = InfrastructureManager; username = user; password = pass" + - copy: dest=/usr/local/ec3/wn_info.yml content={{wn_host_info}}\n-\n{{wn_os_info}}\n-\n{{wn_name}}\n-\n{{wn_node_type}} + + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs + notify: restart cluesd + + # CLUES PBS configuration + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs + notify: restart cluesd + when: clues_queue_system == 'torque' + - copy: src=/etc/clues2/conf.d/plugin-pbs.cfg-example dest=/etc/clues2/conf.d/plugin-pbs.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'torque' + - ini_file: dest=/etc/clues2/conf.d/plugin-pbs.cfg section=PBS option=PBS_SERVER value=torqueserver + notify: restart cluesd + when: clues_queue_system == 'torque' + - lineinfile: dest={{TORQUE_PATH}}/torque.cfg regexp=^SUBMITFILTER line='SUBMITFILTER /usr/local/bin/clues-pbs-wrapper' create=yes mode=644 + when: clues_queue_system == 'torque' + + # CLUES SGE configuration + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.sge + notify: restart cluesd + when: clues_queue_system == 'sge' + - copy: src=/etc/clues2/conf.d/plugin-sge.cfg-example dest=/etc/clues2/conf.d/plugin-sge.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'sge' + - copy: src=/etc/clues2/conf.d/wrapper-sge.cfg-example dest=/etc/clues2/conf.d/wrapper-sge.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'sge' + - lineinfile: dest={{SGE_ROOT}}/default/common/sge_request regexp='^-jsv' line="-jsv /usr/local/bin/clues-sge-wrapper" create=yes mode=644 + when: clues_queue_system == 'sge' + - lineinfile: dest=/etc/profile.d/sge_vars.sh regexp='SGE_JSV_TIMEOUT' line="export SGE_JSV_TIMEOUT=600" create=yes mode=755 + when: clues_queue_system == 'sge' + + # CLUES SLURM configuration + - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.slurm + notify: restart cluesd + when: clues_queue_system == 'slurm' + - copy: src=/etc/clues2/conf.d/plugin-slurm.cfg-example dest=/etc/clues2/conf.d/plugin-slurm.cfg force=no + notify: restart cluesd + when: clues_queue_system == 'slurm' + - ini_file: dest=/etc/clues2/conf.d/plugin-slurm.cfg section=SLURM option=SLURM_SERVER value=slurmserverpublic + notify: restart cluesd + when: clues_queue_system == 'slurm' + - command: mv /usr/local/bin/sbatch /usr/local/bin/sbatch.o creates=/usr/local/bin/sbatch.o + when: clues_queue_system == 'slurm' + - command: mv /usr/local/bin/clues-slurm-wrapper /usr/local/bin/sbatch creates=/usr/local/bin/sbatch + when: clues_queue_system == 'slurm' + + + \ No newline at end of file diff --git a/IM/tosca/artifacts/ec3/ec3_install.yml b/IM/tosca/artifacts/ec3/ec3_install.yml new file mode 100644 index 000000000..ad9c88024 --- /dev/null +++ b/IM/tosca/artifacts/ec3/ec3_install.yml @@ -0,0 +1,57 @@ +--- +- tasks: + # General task + - name: create epel.repo + template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo + when: ansible_os_family == "RedHat" + + # CLUES2 requirements + - name: Apt install CLUES2 requirements in Deb system + apt: pkg=python-sqlite,unzip + when: ansible_os_family == "Debian" + + - name: Yum install CLUES2 requirements in REL system + yum: pkg=python-sqlite2,unzip + when: ansible_os_family == "RedHat" + + - name: Install CLUES pip requirements + pip: name={{item}} + with_items: + - web.py + - ply + + - get_url: url=https://github.com/grycap/{{item}}/archive/master.zip dest=/tmp/{{item}}.zip + register: result + until: result|success + retries: 5 + delay: 1 + with_items: + - clues + - cpyutils + + # CLUES2 installation + - unarchive: src=/tmp/{{item}}.zip dest=/tmp copy=no + with_items: + - clues + - cpyutils + + - command: python setup.py install chdir=/tmp/clues-master creates=/usr/local/bin/cluesserver + - command: python setup.py install chdir=/tmp/cpyutils-master + + # IM installation + - apt: name=python-soappy update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + - yum: name=SOAPpy + when: ansible_os_family == "RedHat" + - pip: name=IM + - file: path=/etc/init.d/im mode=0755 + + + # PBS installation + - name: Apt install Torque in Deb system + apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + + - name: Yum install Torque in REL system + yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' diff --git a/IM/tosca/artifacts/ec3/ec3_start.yml b/IM/tosca/artifacts/ec3/ec3_start.yml new file mode 100644 index 000000000..cc1334ea6 --- /dev/null +++ b/IM/tosca/artifacts/ec3/ec3_start.yml @@ -0,0 +1,33 @@ +--- +- tasks: + # Launch CLUES + - service: name=cluesd state=started + # Launch IM + - service: name=im state=started + + # Launch PBS + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=torque-server state=restarted pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=pbs_server state=restarted pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + + - command: sleep 5 + when: clues_queue_system == 'torque' + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" and clues_queue_system == 'torque' \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_configure.yml b/IM/tosca/artifacts/lrms/torque_configure.yml new file mode 100644 index 000000000..7d2a67d8b --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_configure.yml @@ -0,0 +1,23 @@ +--- + - vars: + PBS_SERVER_CONF: | + create queue batch + set queue batch queue_type = Execution + set queue batch resources_default.nodes = 1 + set queue batch enabled = True + set queue batch started = True + set server default_queue = batch + set server scheduling = True + set server scheduler_iteration = 20 + set server node_check_rate = 40 + set server resources_default.neednodes = 1 + set server resources_default.nodect = 1 + set server resources_default.nodes = 1 + set server query_other_jobs = True + set server node_pack = False + set server job_stat_rate = 30 + set server mom_job_sync = True + set server poll_jobs = True + set tcp_timeout = 600 + + \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_install.yml b/IM/tosca/artifacts/lrms/torque_install.yml new file mode 100644 index 000000000..7c76883c7 --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_install.yml @@ -0,0 +1,13 @@ +--- + - tasks: + - name: create epel.repo + template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo + when: ansible_os_family == "RedHat" + + - name: Apt install Torque in Deb system + apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + + - name: Yum install Torque in REL system + yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_start.yml b/IM/tosca/artifacts/lrms/torque_start.yml new file mode 100644 index 000000000..55c9f708b --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_start.yml @@ -0,0 +1,11 @@ +--- + - tasks: + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_configure.yml b/IM/tosca/artifacts/lrms/torque_wn_configure.yml new file mode 100644 index 000000000..bbe936985 --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_wn_configure.yml @@ -0,0 +1,21 @@ +--- + - vars: + PBS_SERVER_CONF: | + create queue batch + set queue batch queue_type = Execution + set queue batch resources_default.nodes = 1 + set queue batch enabled = True + set queue batch started = True + set server default_queue = batch + set server scheduling = True + set server scheduler_iteration = 20 + set server node_check_rate = 40 + set server resources_default.neednodes = 1 + set server resources_default.nodect = 1 + set server resources_default.nodes = 1 + set server query_other_jobs = True + set server node_pack = False + set server job_stat_rate = 30 + set server mom_job_sync = True + set server poll_jobs = True + set tcp_timeout = 600 \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_install.yml b/IM/tosca/artifacts/lrms/torque_wn_install.yml new file mode 100644 index 000000000..7c76883c7 --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_wn_install.yml @@ -0,0 +1,13 @@ +--- + - tasks: + - name: create epel.repo + template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo + when: ansible_os_family == "RedHat" + + - name: Apt install Torque in Deb system + apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + + - name: Yum install Torque in REL system + yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_start.yml b/IM/tosca/artifacts/lrms/torque_wn_start.yml new file mode 100644 index 000000000..55c9f708b --- /dev/null +++ b/IM/tosca/artifacts/lrms/torque_wn_start.yml @@ -0,0 +1,11 @@ +--- + - tasks: + - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "Debian" + - service: name=torque-server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "Debian" + + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched + when: ansible_os_family == "RedHat" + - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml index 4a2457328..6e4b8c66d 100644 --- a/IM/tosca/custom_types.yaml +++ b/IM/tosca/custom_types.yaml @@ -1,5 +1,14 @@ tosca_definitions_version: tosca_simple_yaml_1_0 +tosca.nodes.indigo.network.Port: + derived_from: tosca.nodes.network.Port + properties: + dns_name: + type: string + required: no + description: > + Allow the user to set a specific dns name. + tosca.nodes.Database.MySQL: derived_from: tosca.nodes.Database properties: @@ -112,7 +121,7 @@ tosca.nodes.indigo.GalaxyPortal: galaxy_install_path: { get_property: [ SELF, install_path ] } -tosca.nodes.indigo.GalaxyTool: +tosca.nodes.indigo.GalaxyShedTool: derived_from: tosca.nodes.WebApplication properties: name: @@ -144,58 +153,92 @@ tosca.nodes.indigo.GalaxyTool: galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } -tosca.capabilities.LRMS: + + + + + +tosca.capabilities.indigo.LRMS: derived_from: tosca.capabilities.Root properties: - lrms_type: + type: type: string required: true - -tosca.capabilities.LRMS.Torque: - derived_from: tosca.capabilities.LRMS - properties: - lrms_type: torque - + constraints: + - valid_values: [ torque, slurm, sge ] -tosca.nodes.indigo.LRMS: - derived_from: tosca.nodes.SoftwareComponent - -tosca.nodes.indigo.LRMS.FrontEnd: - derived_from: tosca.nodes.indigo.LRMS - capabilities: - cluster_endpoint: - type: tosca.capabilities.Endpoint - -tosca.nodes.indigo.LRMS.FrontEnd.Torque: - derived_from: tosca.nodes.indigo.LRMS.FrontEnd - capabilities: - lrms_front_end: - type: tosca.capabilities.LRMS.Torque - interfaces: - Standard: - create: lrms/torque_install.yml - configure: lrms/torque_configure.yml - start: lrms/torque_start.yml - -tosca.nodes.indigo.CLUES: - derived_from: tosca.nodes.SoftwareComponent + +tosca.nodes.indigo.ElasticCluster: + derived_from: tosca.nodes.Root properties: secret_token: type: string - description: Token to access the web interface + description: Token to access CLUES web interface default: not_very_secret_token required: false - requirements: - - lrms_front_end: - capability: tosca.capabilities.LRMS - node: tosca.nodes.indigo.LRMS.FrontEnd - relationship: tosca.relationships.HostedOn interfaces: Standard: - create: clues/clues_install.yml + create: ec3/ec3_install.yml configure: - implementation: clues/clues_configure.yml + implementation: ec3/ec3_configure.yml inputs: clues_secret_token: { get_property: [ SELF, secret_token ] } - clues_queue_system: { get_property: [ SELF, lrms_front_end, lrms_type ] } - start: clues/clues_start.yml \ No newline at end of file + clues_queue_system: { get_property: [ SELF, lrms, type ] } + max_instances: { get_property: [ SELF, scalable, max_instances] } + wn_host_info: { get_property: [ SELF, wn, host ] } + wn_os_info: { get_property: [ SELF, wn, os ] } + wn_name: { get_property: [ SELF, wn, name ] } + wn_node_type: { get_property: [ SELF, wn, type ] } + start: ec3/ec3_start.yml + capabilities: + scalable: + type: tosca.capabilities.Scalable + lrms: + type: tosca.capabilities.indigo.LRMS + requirements: + - wn: + capability: tosca.capabilities.indigo.WorkerNode + node: tosca.nodes.indigo.ElasticCluster.WorkerNode + relationship: tosca.relationships.indigo.Manages + - host: + capability: tosca.capabilities.Container + node: tosca.nodes.Compute + relationship: tosca.relationships.HostedOn + + +tosca.nodes.indigo.ElasticCluster.WorkerNode: + derived_from: tosca.nodes.Root + capabilities: + wn: + type: tosca.capabilities.indigo.WorkerNode + valid_source_types: [tosca.nodes.indigo.ElasticCluster.FrontEnd] + + +tosca.nodes.indigo.LRMS.WorkerNode.Torque: + derived_from: tosca.nodes.Compute + interfaces: + Standard: + create: lrms/torque_wn_install.yml + configure: lrms/torque_wn_configure.yml + start: lrms/torque_wn_start.yml + + +tosca.capabilities.indigo.WorkerNode: + derived_from: tosca.capabilities.Root + properties: + name: + required: yes + type: string + type: + required: yes + type: string + host: + required: no + type: HostInfo + os: + required: no + type: OSInfo + + +tosca.relationships.indigo.Manages: + derived_from: tosca.relationships.Root \ No newline at end of file diff --git a/examples/clues_tosca.yml b/examples/clues_tosca.yml index 7c902ddbb..6bb783897 100644 --- a/examples/clues_tosca.yml +++ b/examples/clues_tosca.yml @@ -6,25 +6,56 @@ topology_template: node_templates: - clues: - type: tosca.nodes.indigo.CLUES + private: + type: tosca.nodes.network.Network + properties: + network_type: private + + public: + type: tosca.nodes.network.Network + properties: + network_type: public + + fe_public_net_port: + type: tosca.nodes.indigo.network.Port + properties: + order: 0 + dns_name: publicname + requirements: + - link: public + - binding: torque_server + + fe_private_net_port: + type: tosca.nodes.indigo.network.Port + properties: + order: 1 + dns_name: torqueserver requirements: - - lrms_front_end: front_end_torque + - link: private + - binding: torque_server - front_end_torque: - type: tosca.nodes.indigo.LRMS.FrontEnd.Torque + elastic_cluster: + type: tosca.nodes.indigo.ElasticCluster + capabilities: + lrms: + properties: + type: torque + scalable: + properties: + max_instances: 5 + min_instances: 0 + default_instances: 0 requirements: - - host: front_end_server - - front_end_server: + - host: torque_server + - wn: wn_node + + torque_server: type: tosca.nodes.Compute capabilities: - # Host container properties host: properties: num_cpus: 1 mem_size: 1 GB - # Guest Operating System properties os: properties: # host Operating System image properties @@ -32,3 +63,14 @@ topology_template: #distribution: scientific #version: 6.6 + wn_node: + type: tosca.nodes.indigo.ElasticCluster.WorkerNode + capabilities: + wn: + properties: + name: vnode + type: tosca.nodes.indigo.LRMS.WorkerNode.Torque + host: + num_cpus: 1 + os: + type: linux \ No newline at end of file diff --git a/examples/galaxy_tosca.yml b/examples/galaxy_tosca.yml index dc8e7a7e8..7912ea296 100644 --- a/examples/galaxy_tosca.yml +++ b/examples/galaxy_tosca.yml @@ -7,7 +7,7 @@ topology_template: node_templates: bowtie2_galaxy_tool: - type: tosca.nodes.indigo.GalaxyTool + type: tosca.nodes.indigo.GalaxyShedTool properties: name: bowtie2 owner: devteam From e568d14868034b58acda53980f72db402d18d115 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 6 Nov 2015 11:28:03 +0100 Subject: [PATCH 029/509] change setup to tosca version --- MANIFEST.in | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index b1e4e7dab..82d4b7eb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,8 @@ recursive-exclude test * recursive-include contextualization * +recursive-include IM/tosca/artifacts * +include IM/tosca/custom_types.yaml +include IM/tosca/custom_import_types.yaml include scripts/im include etc/im.cfg include LICENSE diff --git a/setup.py b/setup.py index b52c1b7ba..4d0a6d7c9 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ author='GRyCAP - Universitat Politecnica de Valencia', author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', - packages=['IM', 'IM.radl', 'IM.ansible','connectors'], + packages=['IM','IM.tosca', 'IM.radl', 'IM.ansible','connectors'], scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From 020ebf04a0207644aad397a253543cbf8c0ed24f Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 6 Nov 2015 11:28:03 +0100 Subject: [PATCH 030/509] change setup to tosca version --- MANIFEST.in | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index b1e4e7dab..82d4b7eb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,8 @@ recursive-exclude test * recursive-include contextualization * +recursive-include IM/tosca/artifacts * +include IM/tosca/custom_types.yaml +include IM/tosca/custom_import_types.yaml include scripts/im include etc/im.cfg include LICENSE diff --git a/setup.py b/setup.py index b52c1b7ba..4d0a6d7c9 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ author='GRyCAP - Universitat Politecnica de Valencia', author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', - packages=['IM', 'IM.radl', 'IM.ansible','connectors'], + packages=['IM','IM.tosca', 'IM.radl', 'IM.ansible','connectors'], scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From fa380ae070c4ebb207e76ec124606257d77033da Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 6 Nov 2015 13:47:46 +0100 Subject: [PATCH 031/509] Complete EC3 case in TOSCA --- IM/tosca/Tosca.py | 68 +++++++++++++++++++- IM/tosca/artifacts/ec3/ec3_configure.yml | 8 +++ IM/tosca/artifacts/lrms/torque_configure.yml | 23 ------- IM/tosca/artifacts/lrms/torque_install.yml | 13 ---- IM/tosca/artifacts/lrms/torque_start.yml | 11 ---- IM/tosca/custom_types.yaml | 11 ++-- examples/clues_tosca.yml | 32 +-------- 7 files changed, 83 insertions(+), 83 deletions(-) delete mode 100644 IM/tosca/artifacts/lrms/torque_configure.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_install.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_start.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index dba3bf2c7..a54f343eb 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -126,9 +126,11 @@ def to_radl(self): net = Tosca._gen_network(node) radl.networks.append(net) else: - if root_type == "tosca.nodes.Compute": + if root_type == "tosca.nodes.Compute": # Add the system RADL element sys = Tosca._gen_system(node, self.tosca.nodetemplates) + # add networks using the simple method with the public_ip property + Tosca._add_node_nets(node, radl, sys) radl.systems.append(sys) # Add the deploy element for this system min_instances, _, default_instances = Tosca._get_scalable_properties(node) @@ -163,6 +165,70 @@ def to_radl(self): return self._complete_radl_networks(radl) + @staticmethod + def _add_node_nets(node, radl, system): + public_ip = False + node_props = node.get_properties_objects() + if node_props: + for prop in node_props: + if prop.name == "public_ip": + public_ip = prop.value + break + + # If the node needs a public IP + if public_ip: + public_nets = [] + for net in radl.networks: + if net.isPublic(): + public_nets.append(net) + + if public_nets: + public_net = None + for net in public_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + public_net = net + break + + if not public_net: + # There are a public net but it has not been used in this VM + public_net = public_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + public_net = network.createNetwork("public_net.", True) + radl.networks.append(public_net) + num_net = system.getNumNetworkIfaces() + + system.setValue('net_interface.' + str(num_net) + '.connection',public_net.id) + + # The private net is allways added + private_nets = [] + for net in radl.networks: + if not net.isPublic(): + private_nets.append(net) + + if private_nets: + private_net = None + for net in private_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + private_net = net + break + + if not private_net: + # There are a public net but it has not been used in this VM + private_net = private_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + private_net = network.createNetwork("private_net.", False) + radl.networks.append(private_net) + num_net = system.getNumNetworkIfaces() + + system.setValue('net_interface.' + str(num_net) + '.connection',private_net.id) + + @staticmethod def _get_scalable_properties(node): min_instances = max_instances = default_instances = None diff --git a/IM/tosca/artifacts/ec3/ec3_configure.yml b/IM/tosca/artifacts/ec3/ec3_configure.yml index dce601e2e..919f3d4fc 100644 --- a/IM/tosca/artifacts/ec3/ec3_configure.yml +++ b/IM/tosca/artifacts/ec3/ec3_configure.yml @@ -26,6 +26,13 @@ set tcp_timeout = 600 tasks: + # /etc/hosts configuration + - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{IM_NODE_NET_1_IP}} torqueserver' + when: IM_NODE_NET_1_IP is defined + + - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{ansible_default_ipv4.address}} torqueserver' + when: IM_NODE_NET_1_IP is undefined + # PBS configuration - file: src=/var/lib/torque dest=/var/spool/torque state=link when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' @@ -78,6 +85,7 @@ # CLUES IM configuration - file: path=/usr/local/ec3 state=directory mode=755 + # TODO: check this - copy: dest=/usr/local/ec3/auth.dat content="type = InfrastructureManager; username = user; password = pass" - copy: dest=/usr/local/ec3/wn_info.yml content={{wn_host_info}}\n-\n{{wn_os_info}}\n-\n{{wn_name}}\n-\n{{wn_node_type}} diff --git a/IM/tosca/artifacts/lrms/torque_configure.yml b/IM/tosca/artifacts/lrms/torque_configure.yml deleted file mode 100644 index 7d2a67d8b..000000000 --- a/IM/tosca/artifacts/lrms/torque_configure.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- - - vars: - PBS_SERVER_CONF: | - create queue batch - set queue batch queue_type = Execution - set queue batch resources_default.nodes = 1 - set queue batch enabled = True - set queue batch started = True - set server default_queue = batch - set server scheduling = True - set server scheduler_iteration = 20 - set server node_check_rate = 40 - set server resources_default.neednodes = 1 - set server resources_default.nodect = 1 - set server resources_default.nodes = 1 - set server query_other_jobs = True - set server node_pack = False - set server job_stat_rate = 30 - set server mom_job_sync = True - set server poll_jobs = True - set tcp_timeout = 600 - - \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_install.yml b/IM/tosca/artifacts/lrms/torque_install.yml deleted file mode 100644 index 7c76883c7..000000000 --- a/IM/tosca/artifacts/lrms/torque_install.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- - - tasks: - - name: create epel.repo - template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo - when: ansible_os_family == "RedHat" - - - name: Apt install Torque in Deb system - apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - - name: Yum install Torque in REL system - yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_start.yml b/IM/tosca/artifacts/lrms/torque_start.yml deleted file mode 100644 index 55c9f708b..000000000 --- a/IM/tosca/artifacts/lrms/torque_start.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- - - tasks: - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml index 6e4b8c66d..d01f6442a 100644 --- a/IM/tosca/custom_types.yaml +++ b/IM/tosca/custom_types.yaml @@ -1,13 +1,12 @@ tosca_definitions_version: tosca_simple_yaml_1_0 -tosca.nodes.indigo.network.Port: - derived_from: tosca.nodes.network.Port +tosca.nodes.indigo.Compute: + derived_from: tosca.nodes.Compute properties: - dns_name: - type: string + public_ip: + type: boolean required: no - description: > - Allow the user to set a specific dns name. + default: no tosca.nodes.Database.MySQL: derived_from: tosca.nodes.Database diff --git a/examples/clues_tosca.yml b/examples/clues_tosca.yml index 6bb783897..41e9e1cc1 100644 --- a/examples/clues_tosca.yml +++ b/examples/clues_tosca.yml @@ -6,34 +6,6 @@ topology_template: node_templates: - private: - type: tosca.nodes.network.Network - properties: - network_type: private - - public: - type: tosca.nodes.network.Network - properties: - network_type: public - - fe_public_net_port: - type: tosca.nodes.indigo.network.Port - properties: - order: 0 - dns_name: publicname - requirements: - - link: public - - binding: torque_server - - fe_private_net_port: - type: tosca.nodes.indigo.network.Port - properties: - order: 1 - dns_name: torqueserver - requirements: - - link: private - - binding: torque_server - elastic_cluster: type: tosca.nodes.indigo.ElasticCluster capabilities: @@ -50,7 +22,9 @@ topology_template: - wn: wn_node torque_server: - type: tosca.nodes.Compute + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes capabilities: host: properties: From 20212f05bdea423464e383cd14bf52ed4cac5b50 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 6 Nov 2015 13:47:46 +0100 Subject: [PATCH 032/509] Complete EC3 case in TOSCA --- IM/tosca/Tosca.py | 68 +++++++++++++++++++- IM/tosca/artifacts/ec3/ec3_configure.yml | 8 +++ IM/tosca/artifacts/lrms/torque_configure.yml | 23 ------- IM/tosca/artifacts/lrms/torque_install.yml | 13 ---- IM/tosca/artifacts/lrms/torque_start.yml | 11 ---- IM/tosca/custom_types.yaml | 11 ++-- examples/clues_tosca.yml | 32 +-------- 7 files changed, 83 insertions(+), 83 deletions(-) delete mode 100644 IM/tosca/artifacts/lrms/torque_configure.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_install.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_start.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index dba3bf2c7..a54f343eb 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -126,9 +126,11 @@ def to_radl(self): net = Tosca._gen_network(node) radl.networks.append(net) else: - if root_type == "tosca.nodes.Compute": + if root_type == "tosca.nodes.Compute": # Add the system RADL element sys = Tosca._gen_system(node, self.tosca.nodetemplates) + # add networks using the simple method with the public_ip property + Tosca._add_node_nets(node, radl, sys) radl.systems.append(sys) # Add the deploy element for this system min_instances, _, default_instances = Tosca._get_scalable_properties(node) @@ -163,6 +165,70 @@ def to_radl(self): return self._complete_radl_networks(radl) + @staticmethod + def _add_node_nets(node, radl, system): + public_ip = False + node_props = node.get_properties_objects() + if node_props: + for prop in node_props: + if prop.name == "public_ip": + public_ip = prop.value + break + + # If the node needs a public IP + if public_ip: + public_nets = [] + for net in radl.networks: + if net.isPublic(): + public_nets.append(net) + + if public_nets: + public_net = None + for net in public_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + public_net = net + break + + if not public_net: + # There are a public net but it has not been used in this VM + public_net = public_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + public_net = network.createNetwork("public_net.", True) + radl.networks.append(public_net) + num_net = system.getNumNetworkIfaces() + + system.setValue('net_interface.' + str(num_net) + '.connection',public_net.id) + + # The private net is allways added + private_nets = [] + for net in radl.networks: + if not net.isPublic(): + private_nets.append(net) + + if private_nets: + private_net = None + for net in private_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + private_net = net + break + + if not private_net: + # There are a public net but it has not been used in this VM + private_net = private_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + private_net = network.createNetwork("private_net.", False) + radl.networks.append(private_net) + num_net = system.getNumNetworkIfaces() + + system.setValue('net_interface.' + str(num_net) + '.connection',private_net.id) + + @staticmethod def _get_scalable_properties(node): min_instances = max_instances = default_instances = None diff --git a/IM/tosca/artifacts/ec3/ec3_configure.yml b/IM/tosca/artifacts/ec3/ec3_configure.yml index dce601e2e..919f3d4fc 100644 --- a/IM/tosca/artifacts/ec3/ec3_configure.yml +++ b/IM/tosca/artifacts/ec3/ec3_configure.yml @@ -26,6 +26,13 @@ set tcp_timeout = 600 tasks: + # /etc/hosts configuration + - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{IM_NODE_NET_1_IP}} torqueserver' + when: IM_NODE_NET_1_IP is defined + + - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{ansible_default_ipv4.address}} torqueserver' + when: IM_NODE_NET_1_IP is undefined + # PBS configuration - file: src=/var/lib/torque dest=/var/spool/torque state=link when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' @@ -78,6 +85,7 @@ # CLUES IM configuration - file: path=/usr/local/ec3 state=directory mode=755 + # TODO: check this - copy: dest=/usr/local/ec3/auth.dat content="type = InfrastructureManager; username = user; password = pass" - copy: dest=/usr/local/ec3/wn_info.yml content={{wn_host_info}}\n-\n{{wn_os_info}}\n-\n{{wn_name}}\n-\n{{wn_node_type}} diff --git a/IM/tosca/artifacts/lrms/torque_configure.yml b/IM/tosca/artifacts/lrms/torque_configure.yml deleted file mode 100644 index 7d2a67d8b..000000000 --- a/IM/tosca/artifacts/lrms/torque_configure.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- - - vars: - PBS_SERVER_CONF: | - create queue batch - set queue batch queue_type = Execution - set queue batch resources_default.nodes = 1 - set queue batch enabled = True - set queue batch started = True - set server default_queue = batch - set server scheduling = True - set server scheduler_iteration = 20 - set server node_check_rate = 40 - set server resources_default.neednodes = 1 - set server resources_default.nodect = 1 - set server resources_default.nodes = 1 - set server query_other_jobs = True - set server node_pack = False - set server job_stat_rate = 30 - set server mom_job_sync = True - set server poll_jobs = True - set tcp_timeout = 600 - - \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_install.yml b/IM/tosca/artifacts/lrms/torque_install.yml deleted file mode 100644 index 7c76883c7..000000000 --- a/IM/tosca/artifacts/lrms/torque_install.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- - - tasks: - - name: create epel.repo - template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo - when: ansible_os_family == "RedHat" - - - name: Apt install Torque in Deb system - apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - - name: Yum install Torque in REL system - yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_start.yml b/IM/tosca/artifacts/lrms/torque_start.yml deleted file mode 100644 index 55c9f708b..000000000 --- a/IM/tosca/artifacts/lrms/torque_start.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- - - tasks: - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml index 6e4b8c66d..d01f6442a 100644 --- a/IM/tosca/custom_types.yaml +++ b/IM/tosca/custom_types.yaml @@ -1,13 +1,12 @@ tosca_definitions_version: tosca_simple_yaml_1_0 -tosca.nodes.indigo.network.Port: - derived_from: tosca.nodes.network.Port +tosca.nodes.indigo.Compute: + derived_from: tosca.nodes.Compute properties: - dns_name: - type: string + public_ip: + type: boolean required: no - description: > - Allow the user to set a specific dns name. + default: no tosca.nodes.Database.MySQL: derived_from: tosca.nodes.Database diff --git a/examples/clues_tosca.yml b/examples/clues_tosca.yml index 6bb783897..41e9e1cc1 100644 --- a/examples/clues_tosca.yml +++ b/examples/clues_tosca.yml @@ -6,34 +6,6 @@ topology_template: node_templates: - private: - type: tosca.nodes.network.Network - properties: - network_type: private - - public: - type: tosca.nodes.network.Network - properties: - network_type: public - - fe_public_net_port: - type: tosca.nodes.indigo.network.Port - properties: - order: 0 - dns_name: publicname - requirements: - - link: public - - binding: torque_server - - fe_private_net_port: - type: tosca.nodes.indigo.network.Port - properties: - order: 1 - dns_name: torqueserver - requirements: - - link: private - - binding: torque_server - elastic_cluster: type: tosca.nodes.indigo.ElasticCluster capabilities: @@ -50,7 +22,9 @@ topology_template: - wn: wn_node torque_server: - type: tosca.nodes.Compute + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes capabilities: host: properties: From b42e5869f9e7b3b42436d3f7ec75ad19407b7f20 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Mon, 9 Nov 2015 18:27:11 +0100 Subject: [PATCH 033/509] Added TOSCA Dockerfile --- docker/Dockerfile-tosca | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docker/Dockerfile-tosca diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca new file mode 100644 index 000000000..8bb79df26 --- /dev/null +++ b/docker/Dockerfile-tosca @@ -0,0 +1,31 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.3.2" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +RUN cd tmp \ +# && git clone https://github.com/grycap/im.git \ + && git clone -b tosca https://github.com/grycap/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg +CMD im_service.py From 9bd59cbd28371264deba580e56a089d694867274 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Mon, 9 Nov 2015 18:27:11 +0100 Subject: [PATCH 034/509] Added TOSCA Dockerfile --- docker/Dockerfile-tosca | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docker/Dockerfile-tosca diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca new file mode 100644 index 000000000..8bb79df26 --- /dev/null +++ b/docker/Dockerfile-tosca @@ -0,0 +1,31 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.3.2" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +RUN cd tmp \ +# && git clone https://github.com/grycap/im.git \ + && git clone -b tosca https://github.com/grycap/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg +CMD im_service.py From 37b565e405b7c81fefbe575f24df069a1bb2f2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Mon, 9 Nov 2015 18:28:46 +0100 Subject: [PATCH 035/509] Update Dockerfile-tosca Removed line not needed --- docker/Dockerfile-tosca | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca index 8bb79df26..c2aa314d1 100644 --- a/docker/Dockerfile-tosca +++ b/docker/Dockerfile-tosca @@ -23,7 +23,6 @@ RUN cd tmp \ && python setup.py install RUN cd tmp \ -# && git clone https://github.com/grycap/im.git \ && git clone -b tosca https://github.com/grycap/im.git \ && cd im \ && python setup.py install From ed98492484386446ad44eea18d3621efb80d7fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Mon, 9 Nov 2015 18:28:46 +0100 Subject: [PATCH 036/509] Update Dockerfile-tosca Removed line not needed --- docker/Dockerfile-tosca | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca index 8bb79df26..c2aa314d1 100644 --- a/docker/Dockerfile-tosca +++ b/docker/Dockerfile-tosca @@ -23,7 +23,6 @@ RUN cd tmp \ && python setup.py install RUN cd tmp \ -# && git clone https://github.com/grycap/im.git \ && git clone -b tosca https://github.com/grycap/im.git \ && cd im \ && python setup.py install From 3d149c3d82493764c56fabd06246edbd86556cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Tue, 10 Nov 2015 13:57:12 +0100 Subject: [PATCH 037/509] Update Dockerfile-tosca Added comments to the dockerfile Added command to activate the REST services of the IM server --- docker/Dockerfile-tosca | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca index c2aa314d1..ecb7d6d47 100644 --- a/docker/Dockerfile-tosca +++ b/docker/Dockerfile-tosca @@ -3,7 +3,7 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 +# Update and install all the neccesary packages RUN apt-get update && apt-get install -y \ gcc \ python-dev \ @@ -17,14 +17,21 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install tosca-parser RUN cd tmp \ && git clone https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install +# Install im - 'tosca' branch RUN cd tmp \ && git clone -b tosca https://github.com/grycap/im.git \ && cd im \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +EXPOSE 8899 CMD im_service.py From 2239acd01040a0e2398f5396d9496dbb7909c0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Tue, 10 Nov 2015 13:57:12 +0100 Subject: [PATCH 038/509] Update Dockerfile-tosca Added comments to the dockerfile Added command to activate the REST services of the IM server --- docker/Dockerfile-tosca | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca index c2aa314d1..ecb7d6d47 100644 --- a/docker/Dockerfile-tosca +++ b/docker/Dockerfile-tosca @@ -3,7 +3,7 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 +# Update and install all the neccesary packages RUN apt-get update && apt-get install -y \ gcc \ python-dev \ @@ -17,14 +17,21 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install tosca-parser RUN cd tmp \ && git clone https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install +# Install im - 'tosca' branch RUN cd tmp \ && git clone -b tosca https://github.com/grycap/im.git \ && cd im \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +EXPOSE 8899 CMD im_service.py From 5a1a5e45f9c62fe71bee8ee45ec6a9fc38e88c75 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Tue, 10 Nov 2015 16:57:39 +0100 Subject: [PATCH 039/509] Updated tosca docker folder --- .../Dockerfile-tosca => docker-tosca/Dockerfile | 0 docker-tosca/ansible.cfg | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) rename docker/Dockerfile-tosca => docker-tosca/Dockerfile (100%) create mode 100644 docker-tosca/ansible.cfg diff --git a/docker/Dockerfile-tosca b/docker-tosca/Dockerfile similarity index 100% rename from docker/Dockerfile-tosca rename to docker-tosca/Dockerfile diff --git a/docker-tosca/ansible.cfg b/docker-tosca/ansible.cfg new file mode 100644 index 000000000..3cfba7837 --- /dev/null +++ b/docker-tosca/ansible.cfg @@ -0,0 +1,17 @@ +[defaults] +transport = smart +host_key_checking = False +sudo_user = root +sudo_exe = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True From fa5eac45430311048549fd29a51360e47e47a204 Mon Sep 17 00:00:00 2001 From: Alfonso Date: Tue, 10 Nov 2015 16:57:39 +0100 Subject: [PATCH 040/509] Updated tosca docker folder --- .../Dockerfile-tosca => docker-tosca/Dockerfile | 0 docker-tosca/ansible.cfg | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) rename docker/Dockerfile-tosca => docker-tosca/Dockerfile (100%) create mode 100644 docker-tosca/ansible.cfg diff --git a/docker/Dockerfile-tosca b/docker-tosca/Dockerfile similarity index 100% rename from docker/Dockerfile-tosca rename to docker-tosca/Dockerfile diff --git a/docker-tosca/ansible.cfg b/docker-tosca/ansible.cfg new file mode 100644 index 000000000..3cfba7837 --- /dev/null +++ b/docker-tosca/ansible.cfg @@ -0,0 +1,17 @@ +[defaults] +transport = smart +host_key_checking = False +sudo_user = root +sudo_exe = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True From cb1c4ff6d9ef1481a0bdad22c52ce805b191fe68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 10:22:06 +0100 Subject: [PATCH 041/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index ecb7d6d47..d7c5fd12a 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,35 +3,14 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 # Update and install all the neccesary packages -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-pbr \ - python-dateutil \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - +RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* # Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install - +RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install # Install im - 'tosca' branch -RUN cd tmp \ - && git clone -b tosca https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install +RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg - # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - -EXPOSE 8899 CMD im_service.py From 5c16368f611c49deb44f7d2b7b0c7b9b7a297ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 10:22:06 +0100 Subject: [PATCH 042/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index ecb7d6d47..d7c5fd12a 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,35 +3,14 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 # Update and install all the neccesary packages -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-pbr \ - python-dateutil \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - +RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* # Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install - +RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install # Install im - 'tosca' branch -RUN cd tmp \ - && git clone -b tosca https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install +RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg - # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - -EXPOSE 8899 CMD im_service.py From 2a8c30b508e064fd7c161af121a9fb728c05c399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 10:37:52 +0100 Subject: [PATCH 043/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index d7c5fd12a..569a3d962 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,14 +3,19 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 + # Update and install all the neccesary packages RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* + # Install tosca-parser RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install + # Install im - 'tosca' branch RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +EXPOSE 8899 CMD im_service.py From 4ed2765650fae9b98454f0d328df61827f46b4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 10:37:52 +0100 Subject: [PATCH 044/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index d7c5fd12a..569a3d962 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,14 +3,19 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 + # Update and install all the neccesary packages RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* + # Install tosca-parser RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install + # Install im - 'tosca' branch RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +EXPOSE 8899 CMD im_service.py From 87534809656a948b4794d2cc9f7c0a574ab1ffcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 10:48:19 +0100 Subject: [PATCH 045/509] Update tosca dockerfile Remove blank lines --- docker-tosca/Dockerfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 569a3d962..8498a250e 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,19 +3,14 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" - # Update and install all the neccesary packages RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* - # Install tosca-parser RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install - # Install im - 'tosca' branch RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg - # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - EXPOSE 8899 CMD im_service.py From 953171d620c8e3c6cef909302ff88da9b0bf24cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 10:48:19 +0100 Subject: [PATCH 046/509] Update tosca dockerfile Remove blank lines --- docker-tosca/Dockerfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 569a3d962..8498a250e 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,19 +3,14 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" - # Update and install all the neccesary packages RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* - # Install tosca-parser RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install - # Install im - 'tosca' branch RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg - # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - EXPOSE 8899 CMD im_service.py From 6abb930365f8952cc95b7f4d1c80a2063ccb0335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 11:49:58 +0100 Subject: [PATCH 047/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 8498a250e..bdb45aff8 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,14 +3,31 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 # Update and install all the neccesary packages -RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* # Install tosca-parser -RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install # Install im - 'tosca' branch -RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install +RUN cd tmp \ + && git clone -b tosca https://github.com/grycap/im.git \ + && cd im \ + && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -EXPOSE 8899 CMD im_service.py From 1d3c1e1ad92a4f9125863d12eb4adbe4929c1a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 11:49:58 +0100 Subject: [PATCH 048/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 8498a250e..bdb45aff8 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,14 +3,31 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 # Update and install all the neccesary packages -RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy python-pbr python-dateutil openssh-client sshpass git && apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* # Install tosca-parser -RUN cd tmp && git clone https://github.com/indigo-dc/tosca-parser.git && cd tosca-parser && python setup.py install +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install # Install im - 'tosca' branch -RUN cd tmp && git clone -b tosca https://github.com/grycap/im.git && cd im && python setup.py install +RUN cd tmp \ + && git clone -b tosca https://github.com/grycap/im.git \ + && cd im \ + && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -EXPOSE 8899 CMD im_service.py From 3eebf2b1b47d1379ca3620cb000afa3cb8239897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 12:25:26 +0100 Subject: [PATCH 049/509] Create Dockerfile-tosca --- docker/Dockerfile-tosca | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docker/Dockerfile-tosca diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca new file mode 100644 index 000000000..bdb45aff8 --- /dev/null +++ b/docker/Dockerfile-tosca @@ -0,0 +1,33 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.3.2" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* +# Install tosca-parser +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install +# Install im - 'tosca' branch +RUN cd tmp \ + && git clone -b tosca https://github.com/grycap/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg +CMD im_service.py From 49c6bde058e4cedfb936bf6bdfe60d8c9781bbf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 12:25:26 +0100 Subject: [PATCH 050/509] Create Dockerfile-tosca --- docker/Dockerfile-tosca | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docker/Dockerfile-tosca diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca new file mode 100644 index 000000000..bdb45aff8 --- /dev/null +++ b/docker/Dockerfile-tosca @@ -0,0 +1,33 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.3.2" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +EXPOSE 8899 +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* +# Install tosca-parser +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install +# Install im - 'tosca' branch +RUN cd tmp \ + && git clone -b tosca https://github.com/grycap/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg +CMD im_service.py From 5bcc759f9b72ea1e2af51ffae1f75b24328557de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 12:53:45 +0100 Subject: [PATCH 051/509] Delete Dockerfile-tosca Dockerhub doesn't like having two Dockerfiles in the same folder --- docker/Dockerfile-tosca | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 docker/Dockerfile-tosca diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca deleted file mode 100644 index bdb45aff8..000000000 --- a/docker/Dockerfile-tosca +++ /dev/null @@ -1,33 +0,0 @@ -# Dockerfile to create a container with the IM service and TOSCA support -FROM ubuntu:14.04 -MAINTAINER Miguel Caballer -LABEL version="1.3.2" -LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 -# Update and install all the neccesary packages -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-pbr \ - python-dateutil \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* -# Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install -# Install im - 'tosca' branch -RUN cd tmp \ - && git clone -b tosca https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install -COPY ansible.cfg /etc/ansible/ansible.cfg -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -CMD im_service.py From b2fee30e518e2649aab7c777ac9fde62b9e4ee30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 12:53:45 +0100 Subject: [PATCH 052/509] Delete Dockerfile-tosca Dockerhub doesn't like having two Dockerfiles in the same folder --- docker/Dockerfile-tosca | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 docker/Dockerfile-tosca diff --git a/docker/Dockerfile-tosca b/docker/Dockerfile-tosca deleted file mode 100644 index bdb45aff8..000000000 --- a/docker/Dockerfile-tosca +++ /dev/null @@ -1,33 +0,0 @@ -# Dockerfile to create a container with the IM service and TOSCA support -FROM ubuntu:14.04 -MAINTAINER Miguel Caballer -LABEL version="1.3.2" -LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 -# Update and install all the neccesary packages -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-pbr \ - python-dateutil \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* -# Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install -# Install im - 'tosca' branch -RUN cd tmp \ - && git clone -b tosca https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install -COPY ansible.cfg /etc/ansible/ansible.cfg -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -CMD im_service.py From 648b297a3e6e25a30dd54c8656eef88426b26ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 12:54:58 +0100 Subject: [PATCH 053/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index bdb45aff8..42f9bb5bf 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -28,6 +28,7 @@ RUN cd tmp \ && cd im \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg CMD im_service.py From 6f308a2a0267d82b4a642ca146e51e9c86161910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 12:54:58 +0100 Subject: [PATCH 054/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index bdb45aff8..42f9bb5bf 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -28,6 +28,7 @@ RUN cd tmp \ && cd im \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg CMD im_service.py From b859a74201b24d440d9e4ce2836ddcb7d785b5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 13:04:20 +0100 Subject: [PATCH 055/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 42f9bb5bf..6ba4ff7d0 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,7 +3,9 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + EXPOSE 8899 + # Update and install all the neccesary packages RUN apt-get update && apt-get install -y \ gcc \ @@ -17,11 +19,13 @@ RUN apt-get update && apt-get install -y \ git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* + # Install tosca-parser RUN cd tmp \ && git clone https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install + # Install im - 'tosca' branch RUN cd tmp \ && git clone -b tosca https://github.com/grycap/im.git \ @@ -31,4 +35,5 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + CMD im_service.py From 58e61ede449975852f7684468c33e1d15a2071e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 13:04:20 +0100 Subject: [PATCH 056/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 42f9bb5bf..6ba4ff7d0 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -3,7 +3,9 @@ FROM ubuntu:14.04 MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + EXPOSE 8899 + # Update and install all the neccesary packages RUN apt-get update && apt-get install -y \ gcc \ @@ -17,11 +19,13 @@ RUN apt-get update && apt-get install -y \ git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* + # Install tosca-parser RUN cd tmp \ && git clone https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install + # Install im - 'tosca' branch RUN cd tmp \ && git clone -b tosca https://github.com/grycap/im.git \ @@ -31,4 +35,5 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + CMD im_service.py From 562c08f090d8113a18b9d1a46645916fb2a623fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 13:17:37 +0100 Subject: [PATCH 057/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 6ba4ff7d0..da2744b53 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -4,8 +4,6 @@ MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 - # Update and install all the neccesary packages RUN apt-get update && apt-get install -y \ gcc \ @@ -36,4 +34,6 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg +EXPOSE 8899 + CMD im_service.py From ef1554b3492c12c3a5a7d23fe9604f3ced15d9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 13:17:37 +0100 Subject: [PATCH 058/509] Update tosca dockerfile --- docker-tosca/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index 6ba4ff7d0..da2744b53 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -4,8 +4,6 @@ MAINTAINER Miguel Caballer LABEL version="1.3.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -EXPOSE 8899 - # Update and install all the neccesary packages RUN apt-get update && apt-get install -y \ gcc \ @@ -36,4 +34,6 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg +EXPOSE 8899 + CMD im_service.py From df075d33f4a22049a7692759e6b376d84b009ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 13:27:42 +0100 Subject: [PATCH 059/509] Update tosca dockerfile Added more comments --- docker-tosca/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index da2744b53..a8d43ce7e 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -34,6 +34,8 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg +# Expose the IM port EXPOSE 8899 +# Launch the service at the beginning of the container CMD im_service.py From 47dd21d2fb24e31a9259edcf0c82817b1d01d68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 11 Nov 2015 13:27:42 +0100 Subject: [PATCH 060/509] Update tosca dockerfile Added more comments --- docker-tosca/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index da2744b53..a8d43ce7e 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -34,6 +34,8 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg +# Expose the IM port EXPOSE 8899 +# Launch the service at the beginning of the container CMD im_service.py From 2c0245c1e5261f08a5351f856fd1ad7b32316d52 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 18 Nov 2015 10:31:41 +0100 Subject: [PATCH 061/509] Set custom types as an external submodule --- .gitmodules | 3 + IM/tosca/custom_types.yaml | 243 ------------------------------------- IM/tosca/tosca-types | 1 + 3 files changed, 4 insertions(+), 243 deletions(-) create mode 100644 .gitmodules delete mode 100644 IM/tosca/custom_types.yaml create mode 160000 IM/tosca/tosca-types diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e21c15d9e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "IM/tosca/tosca-types"] + path = IM/tosca/tosca-types + url = https://github.com/indigo-dc/tosca-types diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml deleted file mode 100644 index d01f6442a..000000000 --- a/IM/tosca/custom_types.yaml +++ /dev/null @@ -1,243 +0,0 @@ -tosca_definitions_version: tosca_simple_yaml_1_0 - -tosca.nodes.indigo.Compute: - derived_from: tosca.nodes.Compute - properties: - public_ip: - type: boolean - required: no - default: no - -tosca.nodes.Database.MySQL: - derived_from: tosca.nodes.Database - properties: - password: - type: string - required: true - name: - type: string - required: true - user: - type: string - required: true - root_password: - type: string - required: true - requirements: - - host: - capability: tosca.capabilities.Container - relationship: tosca.relationships.HostedOn - node: tosca.nodes.DBMS.MySQL - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_configure.yml - inputs: - password: { get_property: [ SELF, password ] } - name: { get_property: [ SELF, name ] } - user: { get_property: [ SELF, user ] } - root_password: { get_property: [ SELF, root_password ] } - - -tosca.nodes.DBMS.MySQL: - derived_from: tosca.nodes.DBMS - properties: - port: - type: integer - description: reflect the default MySQL server port - default: 3306 - root_password: - type: string - # MySQL requires a root_password for configuration - required: true - capabilities: - # Further constrain the ‘host’ capability to only allow MySQL databases - host: - type: tosca.capabilities.Container - valid_source_types: [ tosca.nodes.Database.MySQL ] - interfaces: - Standard: - create: mysql/mysql_install.yml - configure: - implementation: mysql/mysql_configure.yml - inputs: - root_password: { get_property: [ SELF, root_password ] } - port: { get_property: [ SELF, port ] } - -tosca.nodes.WebServer.Apache: - derived_from: tosca.nodes.WebServer - interfaces: - Standard: - create: apache/apache_install.yml - -# INDIGO non normative types - -tosca.nodes.indigo.GalaxyPortal: - derived_from: tosca.nodes.WebServer - properties: - admin: - type: string - description: email of the admin user - default: admin@admin.com - required: false - admin_api_key: - type: string - description: key to access the API with admin role - default: not_very_secret_api_key - required: false - user: - type: string - description: username to launch the galaxy daemon - default: galaxy - required: false - install_path: - type: string - description: path to install the galaxy tool - default: /home/galaxy/galaxy - required: false - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.Compute - relationship: tosca.relationships.HostedOn - interfaces: - Standard: - create: - implementation: galaxy/galaxy_install.yml - inputs: - galaxy_install_path: { get_property: [ SELF, install_path ] } - configure: - implementation: galaxy/galaxy_configure.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - galaxy_admin: { get_property: [ SELF, admin ] } - galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } - start: - implementation: galaxy/galaxy_start.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - - -tosca.nodes.indigo.GalaxyShedTool: - derived_from: tosca.nodes.WebApplication - properties: - name: - type: string - description: name of the tool - required: true - owner: - type: string - description: developer of the tool - required: true - tool_panel_section_id: - type: string - description: panel section to install the tool - required: true - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.indigo.GalaxyPortal - relationship: tosca.relationships.HostedOn - interfaces: - Standard: - create: - implementation: galaxy/galaxy_tools_configure.yml - inputs: - galaxy_install_path: { get_property: [ HOST, install_path ] } - galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } - galaxy_tool_name: { get_property: [ SELF, name ] } - galaxy_tool_owner: { get_property: [ SELF, owner ] } - galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } - - - - - - - -tosca.capabilities.indigo.LRMS: - derived_from: tosca.capabilities.Root - properties: - type: - type: string - required: true - constraints: - - valid_values: [ torque, slurm, sge ] - - -tosca.nodes.indigo.ElasticCluster: - derived_from: tosca.nodes.Root - properties: - secret_token: - type: string - description: Token to access CLUES web interface - default: not_very_secret_token - required: false - interfaces: - Standard: - create: ec3/ec3_install.yml - configure: - implementation: ec3/ec3_configure.yml - inputs: - clues_secret_token: { get_property: [ SELF, secret_token ] } - clues_queue_system: { get_property: [ SELF, lrms, type ] } - max_instances: { get_property: [ SELF, scalable, max_instances] } - wn_host_info: { get_property: [ SELF, wn, host ] } - wn_os_info: { get_property: [ SELF, wn, os ] } - wn_name: { get_property: [ SELF, wn, name ] } - wn_node_type: { get_property: [ SELF, wn, type ] } - start: ec3/ec3_start.yml - capabilities: - scalable: - type: tosca.capabilities.Scalable - lrms: - type: tosca.capabilities.indigo.LRMS - requirements: - - wn: - capability: tosca.capabilities.indigo.WorkerNode - node: tosca.nodes.indigo.ElasticCluster.WorkerNode - relationship: tosca.relationships.indigo.Manages - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.Compute - relationship: tosca.relationships.HostedOn - - -tosca.nodes.indigo.ElasticCluster.WorkerNode: - derived_from: tosca.nodes.Root - capabilities: - wn: - type: tosca.capabilities.indigo.WorkerNode - valid_source_types: [tosca.nodes.indigo.ElasticCluster.FrontEnd] - - -tosca.nodes.indigo.LRMS.WorkerNode.Torque: - derived_from: tosca.nodes.Compute - interfaces: - Standard: - create: lrms/torque_wn_install.yml - configure: lrms/torque_wn_configure.yml - start: lrms/torque_wn_start.yml - - -tosca.capabilities.indigo.WorkerNode: - derived_from: tosca.capabilities.Root - properties: - name: - required: yes - type: string - type: - required: yes - type: string - host: - required: no - type: HostInfo - os: - required: no - type: OSInfo - - -tosca.relationships.indigo.Manages: - derived_from: tosca.relationships.Root \ No newline at end of file diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types new file mode 160000 index 000000000..d1ad9a7ef --- /dev/null +++ b/IM/tosca/tosca-types @@ -0,0 +1 @@ +Subproject commit d1ad9a7efe30ece97a6e821938d0335e6b88736a From f9ad0c68e861f8173f6fb6d6306aa6a55af96c40 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 18 Nov 2015 10:31:41 +0100 Subject: [PATCH 062/509] Set custom types as an external submodule --- .gitmodules | 3 + IM/tosca/custom_types.yaml | 243 ------------------------------------- IM/tosca/tosca-types | 1 + 3 files changed, 4 insertions(+), 243 deletions(-) create mode 100644 .gitmodules delete mode 100644 IM/tosca/custom_types.yaml create mode 160000 IM/tosca/tosca-types diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e21c15d9e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "IM/tosca/tosca-types"] + path = IM/tosca/tosca-types + url = https://github.com/indigo-dc/tosca-types diff --git a/IM/tosca/custom_types.yaml b/IM/tosca/custom_types.yaml deleted file mode 100644 index d01f6442a..000000000 --- a/IM/tosca/custom_types.yaml +++ /dev/null @@ -1,243 +0,0 @@ -tosca_definitions_version: tosca_simple_yaml_1_0 - -tosca.nodes.indigo.Compute: - derived_from: tosca.nodes.Compute - properties: - public_ip: - type: boolean - required: no - default: no - -tosca.nodes.Database.MySQL: - derived_from: tosca.nodes.Database - properties: - password: - type: string - required: true - name: - type: string - required: true - user: - type: string - required: true - root_password: - type: string - required: true - requirements: - - host: - capability: tosca.capabilities.Container - relationship: tosca.relationships.HostedOn - node: tosca.nodes.DBMS.MySQL - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_configure.yml - inputs: - password: { get_property: [ SELF, password ] } - name: { get_property: [ SELF, name ] } - user: { get_property: [ SELF, user ] } - root_password: { get_property: [ SELF, root_password ] } - - -tosca.nodes.DBMS.MySQL: - derived_from: tosca.nodes.DBMS - properties: - port: - type: integer - description: reflect the default MySQL server port - default: 3306 - root_password: - type: string - # MySQL requires a root_password for configuration - required: true - capabilities: - # Further constrain the ‘host’ capability to only allow MySQL databases - host: - type: tosca.capabilities.Container - valid_source_types: [ tosca.nodes.Database.MySQL ] - interfaces: - Standard: - create: mysql/mysql_install.yml - configure: - implementation: mysql/mysql_configure.yml - inputs: - root_password: { get_property: [ SELF, root_password ] } - port: { get_property: [ SELF, port ] } - -tosca.nodes.WebServer.Apache: - derived_from: tosca.nodes.WebServer - interfaces: - Standard: - create: apache/apache_install.yml - -# INDIGO non normative types - -tosca.nodes.indigo.GalaxyPortal: - derived_from: tosca.nodes.WebServer - properties: - admin: - type: string - description: email of the admin user - default: admin@admin.com - required: false - admin_api_key: - type: string - description: key to access the API with admin role - default: not_very_secret_api_key - required: false - user: - type: string - description: username to launch the galaxy daemon - default: galaxy - required: false - install_path: - type: string - description: path to install the galaxy tool - default: /home/galaxy/galaxy - required: false - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.Compute - relationship: tosca.relationships.HostedOn - interfaces: - Standard: - create: - implementation: galaxy/galaxy_install.yml - inputs: - galaxy_install_path: { get_property: [ SELF, install_path ] } - configure: - implementation: galaxy/galaxy_configure.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - galaxy_admin: { get_property: [ SELF, admin ] } - galaxy_admin_api_key: { get_property: [ SELF, admin_api_key ] } - start: - implementation: galaxy/galaxy_start.yml - inputs: - galaxy_user: { get_property: [ SELF, user ] } - galaxy_install_path: { get_property: [ SELF, install_path ] } - - -tosca.nodes.indigo.GalaxyShedTool: - derived_from: tosca.nodes.WebApplication - properties: - name: - type: string - description: name of the tool - required: true - owner: - type: string - description: developer of the tool - required: true - tool_panel_section_id: - type: string - description: panel section to install the tool - required: true - requirements: - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.indigo.GalaxyPortal - relationship: tosca.relationships.HostedOn - interfaces: - Standard: - create: - implementation: galaxy/galaxy_tools_configure.yml - inputs: - galaxy_install_path: { get_property: [ HOST, install_path ] } - galaxy_admin_api_key: { get_property: [ HOST, admin_api_key ] } - galaxy_tool_name: { get_property: [ SELF, name ] } - galaxy_tool_owner: { get_property: [ SELF, owner ] } - galaxy_tool_panel_section_id: { get_property: [ SELF, tool_panel_section_id ] } - - - - - - - -tosca.capabilities.indigo.LRMS: - derived_from: tosca.capabilities.Root - properties: - type: - type: string - required: true - constraints: - - valid_values: [ torque, slurm, sge ] - - -tosca.nodes.indigo.ElasticCluster: - derived_from: tosca.nodes.Root - properties: - secret_token: - type: string - description: Token to access CLUES web interface - default: not_very_secret_token - required: false - interfaces: - Standard: - create: ec3/ec3_install.yml - configure: - implementation: ec3/ec3_configure.yml - inputs: - clues_secret_token: { get_property: [ SELF, secret_token ] } - clues_queue_system: { get_property: [ SELF, lrms, type ] } - max_instances: { get_property: [ SELF, scalable, max_instances] } - wn_host_info: { get_property: [ SELF, wn, host ] } - wn_os_info: { get_property: [ SELF, wn, os ] } - wn_name: { get_property: [ SELF, wn, name ] } - wn_node_type: { get_property: [ SELF, wn, type ] } - start: ec3/ec3_start.yml - capabilities: - scalable: - type: tosca.capabilities.Scalable - lrms: - type: tosca.capabilities.indigo.LRMS - requirements: - - wn: - capability: tosca.capabilities.indigo.WorkerNode - node: tosca.nodes.indigo.ElasticCluster.WorkerNode - relationship: tosca.relationships.indigo.Manages - - host: - capability: tosca.capabilities.Container - node: tosca.nodes.Compute - relationship: tosca.relationships.HostedOn - - -tosca.nodes.indigo.ElasticCluster.WorkerNode: - derived_from: tosca.nodes.Root - capabilities: - wn: - type: tosca.capabilities.indigo.WorkerNode - valid_source_types: [tosca.nodes.indigo.ElasticCluster.FrontEnd] - - -tosca.nodes.indigo.LRMS.WorkerNode.Torque: - derived_from: tosca.nodes.Compute - interfaces: - Standard: - create: lrms/torque_wn_install.yml - configure: lrms/torque_wn_configure.yml - start: lrms/torque_wn_start.yml - - -tosca.capabilities.indigo.WorkerNode: - derived_from: tosca.capabilities.Root - properties: - name: - required: yes - type: string - type: - required: yes - type: string - host: - required: no - type: HostInfo - os: - required: no - type: OSInfo - - -tosca.relationships.indigo.Manages: - derived_from: tosca.relationships.Root \ No newline at end of file diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types new file mode 160000 index 000000000..d1ad9a7ef --- /dev/null +++ b/IM/tosca/tosca-types @@ -0,0 +1 @@ +Subproject commit d1ad9a7efe30ece97a6e821938d0335e6b88736a From e6d7219216be9a74f364e2d1b925f38c263670f0 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 18 Nov 2015 10:32:10 +0100 Subject: [PATCH 063/509] Code improvements --- IM/tosca/Tosca.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a54f343eb..16381241a 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -14,8 +14,7 @@ class ToscaTemplate(toscaparser_ToscaTemplate): - CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" - CUSTOM_IMPORT_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_import_types.yaml" + CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/custom_types.yaml" def __init__(self, path, parsed_params=None, a_file=True): # Load custom data @@ -41,12 +40,6 @@ def _get_custom_types(self, type_definitions, imports=None): if not imports: imports = self._tpl_imports() - - # Enable to add INDIGO custom definitions - if not imports: - imports = [{"indigo_defs": self.CUSTOM_IMPORT_FILE}] - else: - imports.append({"indigo_defs": self.CUSTOM_IMPORT_FILE}) if imports: custom_defs = toscaparser.imports.\ From 2a89f87f31d227e9fe9b397f1a1e265f90880fb9 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 18 Nov 2015 10:32:10 +0100 Subject: [PATCH 064/509] Code improvements --- IM/tosca/Tosca.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a54f343eb..16381241a 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -14,8 +14,7 @@ class ToscaTemplate(toscaparser_ToscaTemplate): - CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_types.yaml" - CUSTOM_IMPORT_FILE = os.path.dirname(os.path.realpath(__file__)) + "/custom_import_types.yaml" + CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/custom_types.yaml" def __init__(self, path, parsed_params=None, a_file=True): # Load custom data @@ -41,12 +40,6 @@ def _get_custom_types(self, type_definitions, imports=None): if not imports: imports = self._tpl_imports() - - # Enable to add INDIGO custom definitions - if not imports: - imports = [{"indigo_defs": self.CUSTOM_IMPORT_FILE}] - else: - imports.append({"indigo_defs": self.CUSTOM_IMPORT_FILE}) if imports: custom_defs = toscaparser.imports.\ From d9386a2f88e00d5b93203466fe88b318d9cd9c59 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 18 Nov 2015 10:54:09 +0100 Subject: [PATCH 065/509] Update dockerfile and manifest to support the tosca-types submodule --- MANIFEST.in | 5 ++--- docker-tosca/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 82d4b7eb3..da5e2d07e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,10 @@ recursive-exclude test * recursive-include contextualization * recursive-include IM/tosca/artifacts * -include IM/tosca/custom_types.yaml -include IM/tosca/custom_import_types.yaml +include IM/tosca/tosca-types/custom_types.yaml include scripts/im include etc/im.cfg include LICENSE include INSTALL include NOTICE -include changelog +include changelog \ No newline at end of file diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index a8d43ce7e..b6e2a81e7 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.3.2" +LABEL version="1.4.0" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages @@ -26,7 +26,7 @@ RUN cd tmp \ # Install im - 'tosca' branch RUN cd tmp \ - && git clone -b tosca https://github.com/grycap/im.git \ + && git clone -b tosca --recursive https://github.com/grycap/im.git \ && cd im \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg From 55f332d0b5d513cdcbc2ad8e2204365f891e4cf9 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 18 Nov 2015 10:54:09 +0100 Subject: [PATCH 066/509] Update dockerfile and manifest to support the tosca-types submodule --- MANIFEST.in | 5 ++--- docker-tosca/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 82d4b7eb3..da5e2d07e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,10 @@ recursive-exclude test * recursive-include contextualization * recursive-include IM/tosca/artifacts * -include IM/tosca/custom_types.yaml -include IM/tosca/custom_import_types.yaml +include IM/tosca/tosca-types/custom_types.yaml include scripts/im include etc/im.cfg include LICENSE include INSTALL include NOTICE -include changelog +include changelog \ No newline at end of file diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile index a8d43ce7e..b6e2a81e7 100644 --- a/docker-tosca/Dockerfile +++ b/docker-tosca/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.3.2" +LABEL version="1.4.0" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages @@ -26,7 +26,7 @@ RUN cd tmp \ # Install im - 'tosca' branch RUN cd tmp \ - && git clone -b tosca https://github.com/grycap/im.git \ + && git clone -b tosca --recursive https://github.com/grycap/im.git \ && cd im \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg From 55f30eab4b3cbee1e61d49cfe463f0059036ef3c Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 23 Nov 2015 15:57:04 +0100 Subject: [PATCH 067/509] delete docker-devel files --- docker-devel/Dockerfile | 39 --------------------------------------- docker-devel/ansible.cfg | 17 ----------------- 2 files changed, 56 deletions(-) delete mode 100644 docker-devel/Dockerfile delete mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile deleted file mode 100644 index 11f24160d..000000000 --- a/docker-devel/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# Dockerfile to create a container with the IM service -FROM ubuntu:14.04 -MAINTAINER Miguel Caballer -LABEL version="1.3.2" -LABEL description="Container image to run the IM service. (http://www.grycap.upv.es/im)" - -EXPOSE 8899 - -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-dateutil \ - python-pbr \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install - -# Install im - 'devel' branch -RUN cd tmp \ - && git clone -b devel https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install - -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - -COPY ansible.cfg /etc/ansible/ansible.cfg - -CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg deleted file mode 100644 index 3cfba7837..000000000 --- a/docker-devel/ansible.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[defaults] -transport = smart -host_key_checking = False -sudo_user = root -sudo_exe = sudo - -[paramiko_connection] - -record_host_keys=False - -[ssh_connection] - -# Only in systems with OpenSSH support to ControlPersist -ssh_args = -o ControlMaster=auto -o ControlPersist=900s -# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) -#ssh_args = -pipelining = True From 62d5d3d50b7e1e276ce1b0977ebd9bab6a3292f0 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 23 Nov 2015 15:57:04 +0100 Subject: [PATCH 068/509] delete docker-devel files --- docker-devel/Dockerfile | 39 --------------------------------------- docker-devel/ansible.cfg | 17 ----------------- 2 files changed, 56 deletions(-) delete mode 100644 docker-devel/Dockerfile delete mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile deleted file mode 100644 index 11f24160d..000000000 --- a/docker-devel/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# Dockerfile to create a container with the IM service -FROM ubuntu:14.04 -MAINTAINER Miguel Caballer -LABEL version="1.3.2" -LABEL description="Container image to run the IM service. (http://www.grycap.upv.es/im)" - -EXPOSE 8899 - -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-dateutil \ - python-pbr \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install - -# Install im - 'devel' branch -RUN cd tmp \ - && git clone -b devel https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install - -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - -COPY ansible.cfg /etc/ansible/ansible.cfg - -CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg deleted file mode 100644 index 3cfba7837..000000000 --- a/docker-devel/ansible.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[defaults] -transport = smart -host_key_checking = False -sudo_user = root -sudo_exe = sudo - -[paramiko_connection] - -record_host_keys=False - -[ssh_connection] - -# Only in systems with OpenSSH support to ControlPersist -ssh_args = -o ControlMaster=auto -o ControlPersist=900s -# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) -#ssh_args = -pipelining = True From 15d16e8a6ecc61ba529a7286834c1608b2cb0007 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 26 Nov 2015 11:41:54 +0100 Subject: [PATCH 069/509] Minor bugfixes and new recipes --- IM/tosca/Tosca.py | 12 +++++++----- IM/tosca/artifacts/apache/apache_install.yml | 11 ++--------- IM/tosca/artifacts/apache/apache_start.yml | 8 ++++++++ IM/tosca/artifacts/mysql/mysql_db_configure.yml | 10 ++++++---- IM/tosca/artifacts/mysql/mysql_db_import.yml | 4 ++++ 5 files changed, 27 insertions(+), 18 deletions(-) create mode 100755 IM/tosca/artifacts/apache/apache_start.yml create mode 100644 IM/tosca/artifacts/mysql/mysql_db_import.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 16381241a..fda32fa81 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -189,7 +189,7 @@ def _add_node_nets(node, radl, system): num_net = system.getNumNetworkIfaces() else: # There no public net, create one - public_net = network.createNetwork("public_net.", True) + public_net = network.createNetwork("public_net", True) radl.networks.append(public_net) num_net = system.getNumNetworkIfaces() @@ -215,7 +215,7 @@ def _add_node_nets(node, radl, system): num_net = system.getNumNetworkIfaces() else: # There no public net, create one - private_net = network.createNetwork("private_net.", False) + private_net = network.createNetwork("private_net", False) radl.networks.append(private_net) num_net = system.getNumNetworkIfaces() @@ -284,8 +284,9 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if self._is_artifact(param_value): artifact_uri = self._get_artifact_uri(param_value, node) - val = remote_artifacts_path + "/" + os.path.basename(artifact_uri) - artifacts.append(artifact_uri) + if artifact_uri: + val = remote_artifacts_path + "/" + os.path.basename(artifact_uri) + artifacts.append(artifact_uri) else: val = self._final_function_result(param_value, node) @@ -381,7 +382,8 @@ def _get_artifact_uri(function, node): if isinstance(artifacts, dict): for artifact_name, value in artifacts.iteritems(): if artifact_name == name: - return value['implementation'] + #return value['implementation'] + return value['file'] return None diff --git a/IM/tosca/artifacts/apache/apache_install.yml b/IM/tosca/artifacts/apache/apache_install.yml index db572f85b..b44ff9a62 100755 --- a/IM/tosca/artifacts/apache/apache_install.yml +++ b/IM/tosca/artifacts/apache/apache_install.yml @@ -1,3 +1,4 @@ +- tasks: # Disable IPv6 - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" with_items: @@ -13,14 +14,6 @@ apt: pkg=apache2 update_cache=yes when: ansible_os_family == "Debian" - - name: Start Apache service - service: name=apache2 state=started - when: ansible_os_family == "Debian" - - name: Apache | Make sure the Apache packages are installed apt: yum=httpd - when: ansible_os_family == "RedHat" - - - name: Start Apache service - service: name=httpd state=started - when: ansible_os_family == "RedHat" + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/apache/apache_start.yml b/IM/tosca/artifacts/apache/apache_start.yml new file mode 100755 index 000000000..a39b28a1d --- /dev/null +++ b/IM/tosca/artifacts/apache/apache_start.yml @@ -0,0 +1,8 @@ +- tasks: + - name: Start Apache service + service: name=apache2 state=started + when: ansible_os_family == "Debian" + + - name: Start Apache service + service: name=httpd state=started + when: ansible_os_family == "RedHat" diff --git a/IM/tosca/artifacts/mysql/mysql_db_configure.yml b/IM/tosca/artifacts/mysql/mysql_db_configure.yml index 1217d59b5..fb3e23f7b 100644 --- a/IM/tosca/artifacts/mysql/mysql_db_configure.yml +++ b/IM/tosca/artifacts/mysql/mysql_db_configure.yml @@ -1,5 +1,7 @@ - - name: Create DB {{name}} - mysql_db: name={{name}} state=present login_user=root login_password={{root_password}} +--- +- tasks: + - name: Create DB {{db_name}} + mysql_db: name={{db_name}} state=present login_user=root login_password={{db_root_password}} - - name: Create user {{user}} for the DB {{name}} - mysql_user: name={{user}} password={{password}} login_user=root login_password={{root_password}} priv={{name}}.*:ALL,GRANT state=present + - name: Create user {{db_user}} for the DB {{db_name}} + mysql_user: name={{db_user}} password={{db_password}} login_user=root login_password={{db_root_password}} priv={{db_name}}.*:ALL,GRANT state=present diff --git a/IM/tosca/artifacts/mysql/mysql_db_import.yml b/IM/tosca/artifacts/mysql/mysql_db_import.yml new file mode 100644 index 000000000..21cec3fd5 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_db_import.yml @@ -0,0 +1,4 @@ +--- +- tasks: + - name: Import the DB + mysql_db: name={{db_name}} state=import target={{db_data}} login_user={{db_user}} login_password={{db_password}} \ No newline at end of file From 48f83ea650bd075444097c8f75cc0a2bd439d143 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 26 Nov 2015 11:41:54 +0100 Subject: [PATCH 070/509] Minor bugfixes and new recipes --- IM/tosca/Tosca.py | 12 +++++++----- IM/tosca/artifacts/apache/apache_install.yml | 11 ++--------- IM/tosca/artifacts/apache/apache_start.yml | 8 ++++++++ IM/tosca/artifacts/mysql/mysql_db_configure.yml | 10 ++++++---- IM/tosca/artifacts/mysql/mysql_db_import.yml | 4 ++++ 5 files changed, 27 insertions(+), 18 deletions(-) create mode 100755 IM/tosca/artifacts/apache/apache_start.yml create mode 100644 IM/tosca/artifacts/mysql/mysql_db_import.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 16381241a..fda32fa81 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -189,7 +189,7 @@ def _add_node_nets(node, radl, system): num_net = system.getNumNetworkIfaces() else: # There no public net, create one - public_net = network.createNetwork("public_net.", True) + public_net = network.createNetwork("public_net", True) radl.networks.append(public_net) num_net = system.getNumNetworkIfaces() @@ -215,7 +215,7 @@ def _add_node_nets(node, radl, system): num_net = system.getNumNetworkIfaces() else: # There no public net, create one - private_net = network.createNetwork("private_net.", False) + private_net = network.createNetwork("private_net", False) radl.networks.append(private_net) num_net = system.getNumNetworkIfaces() @@ -284,8 +284,9 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if self._is_artifact(param_value): artifact_uri = self._get_artifact_uri(param_value, node) - val = remote_artifacts_path + "/" + os.path.basename(artifact_uri) - artifacts.append(artifact_uri) + if artifact_uri: + val = remote_artifacts_path + "/" + os.path.basename(artifact_uri) + artifacts.append(artifact_uri) else: val = self._final_function_result(param_value, node) @@ -381,7 +382,8 @@ def _get_artifact_uri(function, node): if isinstance(artifacts, dict): for artifact_name, value in artifacts.iteritems(): if artifact_name == name: - return value['implementation'] + #return value['implementation'] + return value['file'] return None diff --git a/IM/tosca/artifacts/apache/apache_install.yml b/IM/tosca/artifacts/apache/apache_install.yml index db572f85b..b44ff9a62 100755 --- a/IM/tosca/artifacts/apache/apache_install.yml +++ b/IM/tosca/artifacts/apache/apache_install.yml @@ -1,3 +1,4 @@ +- tasks: # Disable IPv6 - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" with_items: @@ -13,14 +14,6 @@ apt: pkg=apache2 update_cache=yes when: ansible_os_family == "Debian" - - name: Start Apache service - service: name=apache2 state=started - when: ansible_os_family == "Debian" - - name: Apache | Make sure the Apache packages are installed apt: yum=httpd - when: ansible_os_family == "RedHat" - - - name: Start Apache service - service: name=httpd state=started - when: ansible_os_family == "RedHat" + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/apache/apache_start.yml b/IM/tosca/artifacts/apache/apache_start.yml new file mode 100755 index 000000000..a39b28a1d --- /dev/null +++ b/IM/tosca/artifacts/apache/apache_start.yml @@ -0,0 +1,8 @@ +- tasks: + - name: Start Apache service + service: name=apache2 state=started + when: ansible_os_family == "Debian" + + - name: Start Apache service + service: name=httpd state=started + when: ansible_os_family == "RedHat" diff --git a/IM/tosca/artifacts/mysql/mysql_db_configure.yml b/IM/tosca/artifacts/mysql/mysql_db_configure.yml index 1217d59b5..fb3e23f7b 100644 --- a/IM/tosca/artifacts/mysql/mysql_db_configure.yml +++ b/IM/tosca/artifacts/mysql/mysql_db_configure.yml @@ -1,5 +1,7 @@ - - name: Create DB {{name}} - mysql_db: name={{name}} state=present login_user=root login_password={{root_password}} +--- +- tasks: + - name: Create DB {{db_name}} + mysql_db: name={{db_name}} state=present login_user=root login_password={{db_root_password}} - - name: Create user {{user}} for the DB {{name}} - mysql_user: name={{user}} password={{password}} login_user=root login_password={{root_password}} priv={{name}}.*:ALL,GRANT state=present + - name: Create user {{db_user}} for the DB {{db_name}} + mysql_user: name={{db_user}} password={{db_password}} login_user=root login_password={{db_root_password}} priv={{db_name}}.*:ALL,GRANT state=present diff --git a/IM/tosca/artifacts/mysql/mysql_db_import.yml b/IM/tosca/artifacts/mysql/mysql_db_import.yml new file mode 100644 index 000000000..21cec3fd5 --- /dev/null +++ b/IM/tosca/artifacts/mysql/mysql_db_import.yml @@ -0,0 +1,4 @@ +--- +- tasks: + - name: Import the DB + mysql_db: name={{db_name}} state=import target={{db_data}} login_user={{db_user}} login_password={{db_password}} \ No newline at end of file From cf10755eccf06462e3fa3f25f4cce90c4cb69f08 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Dec 2015 12:32:45 +0100 Subject: [PATCH 071/509] Create the correct Dockerfiles for the indigo im fork --- docker-tosca/Dockerfile | 41 ------------------------------------- docker-tosca/ansible.cfg | 17 ---------------- docker/Dockerfile | 44 +++++++++++++++++++++++++++++++++------- 3 files changed, 37 insertions(+), 65 deletions(-) delete mode 100644 docker-tosca/Dockerfile delete mode 100644 docker-tosca/ansible.cfg diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile deleted file mode 100644 index b6e2a81e7..000000000 --- a/docker-tosca/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# Dockerfile to create a container with the IM service and TOSCA support -FROM ubuntu:14.04 -MAINTAINER Miguel Caballer -LABEL version="1.4.0" -LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" - -# Update and install all the neccesary packages -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-pbr \ - python-dateutil \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install - -# Install im - 'tosca' branch -RUN cd tmp \ - && git clone -b tosca --recursive https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install -COPY ansible.cfg /etc/ansible/ansible.cfg - -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - -# Expose the IM port -EXPOSE 8899 - -# Launch the service at the beginning of the container -CMD im_service.py diff --git a/docker-tosca/ansible.cfg b/docker-tosca/ansible.cfg deleted file mode 100644 index 3cfba7837..000000000 --- a/docker-tosca/ansible.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[defaults] -transport = smart -host_key_checking = False -sudo_user = root -sudo_exe = sudo - -[paramiko_connection] - -record_host_keys=False - -[ssh_connection] - -# Only in systems with OpenSSH support to ControlPersist -ssh_args = -o ControlMaster=auto -o ControlPersist=900s -# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) -#ssh_args = -pipelining = True diff --git a/docker/Dockerfile b/docker/Dockerfile index 081ec0c80..ec5d092b7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,11 +1,41 @@ -# Dockerfile to create a container with the IM service +# Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.3.2" -LABEL description="Container image to run the IM service. (http://www.grycap.upv.es/im)" -EXPOSE 8899 -RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy openssh-client sshpass -RUN pip install IM -RUN pip uninstall -y SOAPpy +LABEL version="1.4.0" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install tosca-parser +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +# Install im indigo tosca fork +RUN cd tmp \ + && git clone --recursive https://github.com/indigo-dc/im.git \ + && cd im \ + && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +# Expose the IM port +EXPOSE 8899 + +# Launch the service at the beginning of the container CMD im_service.py From 8774e97bcb24babc73c8fb1d03079775e7a5e7c8 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Dec 2015 12:32:45 +0100 Subject: [PATCH 072/509] Create the correct Dockerfiles for the indigo im fork --- docker-tosca/Dockerfile | 41 ------------------------------------- docker-tosca/ansible.cfg | 17 ---------------- docker/Dockerfile | 44 +++++++++++++++++++++++++++++++++------- 3 files changed, 37 insertions(+), 65 deletions(-) delete mode 100644 docker-tosca/Dockerfile delete mode 100644 docker-tosca/ansible.cfg diff --git a/docker-tosca/Dockerfile b/docker-tosca/Dockerfile deleted file mode 100644 index b6e2a81e7..000000000 --- a/docker-tosca/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# Dockerfile to create a container with the IM service and TOSCA support -FROM ubuntu:14.04 -MAINTAINER Miguel Caballer -LABEL version="1.4.0" -LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" - -# Update and install all the neccesary packages -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-pbr \ - python-dateutil \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install - -# Install im - 'tosca' branch -RUN cd tmp \ - && git clone -b tosca --recursive https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install -COPY ansible.cfg /etc/ansible/ansible.cfg - -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - -# Expose the IM port -EXPOSE 8899 - -# Launch the service at the beginning of the container -CMD im_service.py diff --git a/docker-tosca/ansible.cfg b/docker-tosca/ansible.cfg deleted file mode 100644 index 3cfba7837..000000000 --- a/docker-tosca/ansible.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[defaults] -transport = smart -host_key_checking = False -sudo_user = root -sudo_exe = sudo - -[paramiko_connection] - -record_host_keys=False - -[ssh_connection] - -# Only in systems with OpenSSH support to ControlPersist -ssh_args = -o ControlMaster=auto -o ControlPersist=900s -# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) -#ssh_args = -pipelining = True diff --git a/docker/Dockerfile b/docker/Dockerfile index 081ec0c80..ec5d092b7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,11 +1,41 @@ -# Dockerfile to create a container with the IM service +# Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.3.2" -LABEL description="Container image to run the IM service. (http://www.grycap.upv.es/im)" -EXPOSE 8899 -RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy openssh-client sshpass -RUN pip install IM -RUN pip uninstall -y SOAPpy +LABEL version="1.4.0" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install tosca-parser +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +# Install im indigo tosca fork +RUN cd tmp \ + && git clone --recursive https://github.com/indigo-dc/im.git \ + && cd im \ + && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +# Expose the IM port +EXPOSE 8899 + +# Launch the service at the beginning of the container CMD im_service.py From 1416bbacf75d4eb1559dcf2c2066d51c3079246e Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Dec 2015 13:05:02 +0100 Subject: [PATCH 073/509] Add docker for devel branch --- docker-devel/Dockerfile | 41 ++++++++++++++++++++++++++++++++++++++++ docker-devel/ansible.cfg | 17 +++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 docker-devel/Dockerfile create mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile new file mode 100644 index 000000000..dc3256ad9 --- /dev/null +++ b/docker-devel/Dockerfile @@ -0,0 +1,41 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.4.0" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install tosca-parser +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +# Install im indigo tosca fork branch 'devel' +RUN cd tmp \ + && git clone --branch devel --recursive https://github.com/indigo-dc/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +# Expose the IM port +EXPOSE 8899 + +# Launch the service at the beginning of the container +CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg new file mode 100644 index 000000000..3cfba7837 --- /dev/null +++ b/docker-devel/ansible.cfg @@ -0,0 +1,17 @@ +[defaults] +transport = smart +host_key_checking = False +sudo_user = root +sudo_exe = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True From 31c468d8204f22c26fccd4fd3030398945dc43c0 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Dec 2015 13:05:02 +0100 Subject: [PATCH 074/509] Add docker for devel branch --- docker-devel/Dockerfile | 41 ++++++++++++++++++++++++++++++++++++++++ docker-devel/ansible.cfg | 17 +++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 docker-devel/Dockerfile create mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile new file mode 100644 index 000000000..dc3256ad9 --- /dev/null +++ b/docker-devel/Dockerfile @@ -0,0 +1,41 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.4.0" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install tosca-parser +RUN cd tmp \ + && git clone https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +# Install im indigo tosca fork branch 'devel' +RUN cd tmp \ + && git clone --branch devel --recursive https://github.com/indigo-dc/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +# Expose the IM port +EXPOSE 8899 + +# Launch the service at the beginning of the container +CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg new file mode 100644 index 000000000..3cfba7837 --- /dev/null +++ b/docker-devel/ansible.cfg @@ -0,0 +1,17 @@ +[defaults] +transport = smart +host_key_checking = False +sudo_user = root +sudo_exe = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True From 1554f6c53247442f19d3988f5ce035fde368c242 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Dec 2015 16:04:29 +0100 Subject: [PATCH 075/509] Bugfix in docker files --- docker-devel/Dockerfile | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index dc3256ad9..d1ef4aec8 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y \ # Install tosca-parser RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ + && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install diff --git a/docker/Dockerfile b/docker/Dockerfile index ec5d092b7..9a87ce09b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y \ # Install tosca-parser RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ + && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install From a117b13f17fd4994fd8d648ddcab21008c5e44e5 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Dec 2015 16:04:29 +0100 Subject: [PATCH 076/509] Bugfix in docker files --- docker-devel/Dockerfile | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index dc3256ad9..d1ef4aec8 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y \ # Install tosca-parser RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ + && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install diff --git a/docker/Dockerfile b/docker/Dockerfile index ec5d092b7..9a87ce09b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y \ # Install tosca-parser RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ + && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ && python setup.py install From 38342b28217d5202cd101e478f9ee811d957f418 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Tue, 22 Dec 2015 12:52:40 +0100 Subject: [PATCH 077/509] Update Dockerfile --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 081ec0c80..3d68d51de 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.3.2" +LABEL version="1.4.0" LABEL description="Container image to run the IM service. (http://www.grycap.upv.es/im)" EXPOSE 8899 RUN apt-get update && apt-get install -y gcc python-dev python-pip python-soappy openssh-client sshpass From abf6ad3947eb8d97ee70071489b98609d8e3176f Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 22 Dec 2015 13:36:27 +0100 Subject: [PATCH 078/509] Delete docker-devel from master --- docker-devel/Dockerfile | 39 --------------------------------------- docker-devel/ansible.cfg | 17 ----------------- 2 files changed, 56 deletions(-) delete mode 100644 docker-devel/Dockerfile delete mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile deleted file mode 100644 index f42dce53f..000000000 --- a/docker-devel/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# Dockerfile to create a container with the IM service -FROM ubuntu:14.04 -MAINTAINER Miguel Caballer -LABEL version="1.4.1" -LABEL description="Container image to run the IM service. (http://www.grycap.upv.es/im)" - -EXPOSE 8899 - -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-soappy \ - python-dateutil \ - python-pbr \ - openssh-client \ - sshpass \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install tosca-parser -RUN cd tmp \ - && git clone https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && python setup.py install - -# Install im - 'devel' branch -RUN cd tmp \ - && git clone -b devel https://github.com/grycap/im.git \ - && cd im \ - && python setup.py install - -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg - -COPY ansible.cfg /etc/ansible/ansible.cfg - -CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg deleted file mode 100644 index 3cfba7837..000000000 --- a/docker-devel/ansible.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[defaults] -transport = smart -host_key_checking = False -sudo_user = root -sudo_exe = sudo - -[paramiko_connection] - -record_host_keys=False - -[ssh_connection] - -# Only in systems with OpenSSH support to ControlPersist -ssh_args = -o ControlMaster=auto -o ControlPersist=900s -# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) -#ssh_args = -pipelining = True From 809e07a2ac331c737c68dc9682bca7f43ce9f65e Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Jan 2016 11:00:52 +0100 Subject: [PATCH 079/509] Change the way to detect TOSCA documents --- IM/tosca/Tosca.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index fda32fa81..bc072f841 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -5,14 +5,14 @@ import tempfile import toscaparser.imports -from toscaparser.tosca_template import ToscaTemplate as toscaparser_ToscaTemplate +from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from toscaparser.utils.yamlparser import load_yaml +from toscaparser.utils.yamlparser import load_yaml -class ToscaTemplate(toscaparser_ToscaTemplate): +class IndigoToscaTemplate(ToscaTemplate): CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/custom_types.yaml" @@ -22,7 +22,7 @@ def __init__(self, path, parsed_params=None, a_file=True): # and update tosca_def with the data EntityType.TOSCA_DEF.update(custom_def) - super(ToscaTemplate, self).__init__(path, parsed_params, a_file) + super(IndigoToscaTemplate, self).__init__(path, parsed_params, a_file) def _get_custom_types(self, type_definitions, imports=None): """Handle custom types defined in imported template files @@ -72,28 +72,24 @@ def __init__(self, yaml_str): with tempfile.NamedTemporaryFile(suffix=".yaml") as f: f.write(yaml_str) f.flush() - self.tosca = ToscaTemplate(f.name) + self.tosca = IndigoToscaTemplate(f.name) @staticmethod def is_tosca(yaml_string): """ Check if a string seems to be a tosca document - Check if it is a correct YAML document and has the item 'tosca_definitions_version' + Check if it has the strings 'tosca_definitions_version' and 'tosca_simple_yaml' """ - try: - yamlo = yaml.load(yaml_string) - if isinstance(yamlo, dict) and 'tosca_definitions_version' in yamlo.keys(): - return True - else: - return False - except: + if yaml_string.find("tosca_definitions_version") != -1 and yaml_string.find("tosca_simple_yaml") != -1: + return True + else: return False def to_radl(self): """ Converts the current ToscaTemplate object in a RADL object """ - + relationships = [] for node in self.tosca.nodetemplates: # Store relationships to check later @@ -126,8 +122,10 @@ def to_radl(self): Tosca._add_node_nets(node, radl, sys) radl.systems.append(sys) # Add the deploy element for this system - min_instances, _, default_instances = Tosca._get_scalable_properties(node) - if default_instances is not None: + count, min_instances, _, default_instances = Tosca._get_scalable_properties(node) + if count is not None: + num_instances = count + elif default_instances is not None: num_instances = default_instances elif min_instances is not None: num_instances = min_instances @@ -224,11 +222,13 @@ def _add_node_nets(node, radl, system): @staticmethod def _get_scalable_properties(node): - min_instances = max_instances = default_instances = None + count = min_instances = max_instances = default_instances = None scalable = node.get_capability("scalable") if scalable: for prop in scalable.get_properties_objects(): if prop.value is not None: + if prop.name == "count": + count = prop.value if prop.name == "max_instances": max_instances = prop.value elif prop.name == "min_instances": @@ -236,7 +236,7 @@ def _get_scalable_properties(node): elif prop.name == "default_instances": default_instances = prop.value - return min_instances, max_instances, default_instances + return min_instances, max_instances, default_instances, count @staticmethod def _get_relationship_template(rel, src, trgt): From a28989e3ebde12323c247e45a1dd40b727b7ed17 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Jan 2016 11:00:52 +0100 Subject: [PATCH 080/509] Change the way to detect TOSCA documents --- IM/tosca/Tosca.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index fda32fa81..bc072f841 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -5,14 +5,14 @@ import tempfile import toscaparser.imports -from toscaparser.tosca_template import ToscaTemplate as toscaparser_ToscaTemplate +from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from toscaparser.utils.yamlparser import load_yaml +from toscaparser.utils.yamlparser import load_yaml -class ToscaTemplate(toscaparser_ToscaTemplate): +class IndigoToscaTemplate(ToscaTemplate): CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/custom_types.yaml" @@ -22,7 +22,7 @@ def __init__(self, path, parsed_params=None, a_file=True): # and update tosca_def with the data EntityType.TOSCA_DEF.update(custom_def) - super(ToscaTemplate, self).__init__(path, parsed_params, a_file) + super(IndigoToscaTemplate, self).__init__(path, parsed_params, a_file) def _get_custom_types(self, type_definitions, imports=None): """Handle custom types defined in imported template files @@ -72,28 +72,24 @@ def __init__(self, yaml_str): with tempfile.NamedTemporaryFile(suffix=".yaml") as f: f.write(yaml_str) f.flush() - self.tosca = ToscaTemplate(f.name) + self.tosca = IndigoToscaTemplate(f.name) @staticmethod def is_tosca(yaml_string): """ Check if a string seems to be a tosca document - Check if it is a correct YAML document and has the item 'tosca_definitions_version' + Check if it has the strings 'tosca_definitions_version' and 'tosca_simple_yaml' """ - try: - yamlo = yaml.load(yaml_string) - if isinstance(yamlo, dict) and 'tosca_definitions_version' in yamlo.keys(): - return True - else: - return False - except: + if yaml_string.find("tosca_definitions_version") != -1 and yaml_string.find("tosca_simple_yaml") != -1: + return True + else: return False def to_radl(self): """ Converts the current ToscaTemplate object in a RADL object """ - + relationships = [] for node in self.tosca.nodetemplates: # Store relationships to check later @@ -126,8 +122,10 @@ def to_radl(self): Tosca._add_node_nets(node, radl, sys) radl.systems.append(sys) # Add the deploy element for this system - min_instances, _, default_instances = Tosca._get_scalable_properties(node) - if default_instances is not None: + count, min_instances, _, default_instances = Tosca._get_scalable_properties(node) + if count is not None: + num_instances = count + elif default_instances is not None: num_instances = default_instances elif min_instances is not None: num_instances = min_instances @@ -224,11 +222,13 @@ def _add_node_nets(node, radl, system): @staticmethod def _get_scalable_properties(node): - min_instances = max_instances = default_instances = None + count = min_instances = max_instances = default_instances = None scalable = node.get_capability("scalable") if scalable: for prop in scalable.get_properties_objects(): if prop.value is not None: + if prop.name == "count": + count = prop.value if prop.name == "max_instances": max_instances = prop.value elif prop.name == "min_instances": @@ -236,7 +236,7 @@ def _get_scalable_properties(node): elif prop.name == "default_instances": default_instances = prop.value - return min_instances, max_instances, default_instances + return min_instances, max_instances, default_instances, count @staticmethod def _get_relationship_template(rel, src, trgt): From b78c86c60a526f54ccb1780a99e252a4d275a6aa Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Jan 2016 11:01:14 +0100 Subject: [PATCH 081/509] Expose the REST port in the docker files --- docker-devel/Dockerfile | 4 ++-- docker/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index d1ef4aec8..8e1c47396 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -34,8 +34,8 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -# Expose the IM port -EXPOSE 8899 +# Expose the IM ports +EXPOSE 8899 8800 # Launch the service at the beginning of the container CMD im_service.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 9a87ce09b..2531dcdb3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -34,8 +34,8 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -# Expose the IM port -EXPOSE 8899 +# Expose the IM ports +EXPOSE 8899 8800 # Launch the service at the beginning of the container CMD im_service.py From f09ab7a393890dd540151215ed0ef8e4b8233f26 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Jan 2016 11:01:14 +0100 Subject: [PATCH 082/509] Expose the REST port in the docker files --- docker-devel/Dockerfile | 4 ++-- docker/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index d1ef4aec8..8e1c47396 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -34,8 +34,8 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -# Expose the IM port -EXPOSE 8899 +# Expose the IM ports +EXPOSE 8899 8800 # Launch the service at the beginning of the container CMD im_service.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 9a87ce09b..2531dcdb3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -34,8 +34,8 @@ COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg -# Expose the IM port -EXPOSE 8899 +# Expose the IM ports +EXPOSE 8899 8800 # Launch the service at the beginning of the container CMD im_service.py From 2daedc609d687e6e68b8c2a7fca0d756dc24c909 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 09:48:44 +0100 Subject: [PATCH 083/509] Support to download implementation scripts from remote URLs --- IM/tosca/Tosca.py | 63 +++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bc072f841..e7beab41b 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -3,7 +3,9 @@ import yaml import copy import tempfile +import urllib +from IM.uriparse import uriparse import toscaparser.imports from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef @@ -296,7 +298,6 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): raise Exception("input value for %s in interface %s of node %s not valid" % (param_name, name, node.name)) name = node.name + "_" + interface.name - script_path = os.path.join(Tosca.ARTIFACTS_PATH, interface.implementation) # if there are artifacts to download if artifacts: @@ -304,40 +305,44 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): tasks += " - name: Download artifact " + artifact + "\n" tasks += " get_url: dest=" + remote_artifacts_path + "/" + os.path.basename(artifact) + " url='" + artifact + "'\n" - if interface.implementation.endswith(".yaml") or interface.implementation.endswith(".yml"): + implementation_url = uriparse(interface.implementation) + + if implementation_url[0] in ['http', 'https', 'ftp']: + script_path = implementation_url[2] + try: + response = urllib.urlopen(interface.implementation) + script_content = response.read() + except Exception, ex: + raise Exception("Error downloading the implementation script '%s': %s" % (interface.implementation, str(ex))) + else: + script_path = os.path.join(Tosca.ARTIFACTS_PATH, interface.implementation) if os.path.isfile(script_path): f = open(script_path) script_content = f.read() f.close() - - if env: - for var_name, var_value in env.iteritems(): - variables += " %s: %s " % (var_name, var_value) + "\n" - variables += "\n" - - recipe_list.append(script_content) else: - raise Exception(script_path + " is not located in the artifacts folder.") + raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH)) + + if script_path.endswith(".yaml") or script_path.endswith(".yml"): + if env: + for var_name, var_value in env.iteritems(): + variables += " %s: %s " % (var_name, var_value) + "\n" + variables += "\n" + + recipe_list.append(script_content) else: - if os.path.isfile(script_path): - f = open(script_path) - script_content = f.read().replace("\n","\\n") - f.close() - - recipe = "- tasks:\n" - recipe += " - name: Copy contents of script of interface " + name + "\n" - recipe += " copy: dest=/tmp/" + os.path.basename(script_path) + " content='" + script_content + "' mode=0755\n" - - recipe += " - name: " + name + "\n" - recipe += " shell: /tmp/" + os.path.basename(script_path) + "\n" - if env: - recipe += " environment:\n" - for var_name, var_value in env.iteritems(): - recipe += " %s: %s\n" % (var_name, var_value) - - recipe_list.append(recipe) - else: - raise Exception(script_path + " is not located in the artifacts folder.") + recipe = "- tasks:\n" + recipe += " - name: Copy contents of script of interface " + name + "\n" + recipe += " copy: dest=/tmp/" + os.path.basename(script_path) + " content='" + script_content + "' mode=0755\n" + + recipe += " - name: " + name + "\n" + recipe += " shell: /tmp/" + os.path.basename(script_path) + "\n" + if env: + recipe += " environment:\n" + for var_name, var_value in env.iteritems(): + recipe += " %s: %s\n" % (var_name, var_value) + + recipe_list.append(recipe) if tasks or recipe_list: name = node.name + "_conf" From 91b3f5aaa21997a32e38d1d42ec35ebb871b9045 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 09:48:44 +0100 Subject: [PATCH 084/509] Support to download implementation scripts from remote URLs --- IM/tosca/Tosca.py | 63 +++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bc072f841..e7beab41b 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -3,7 +3,9 @@ import yaml import copy import tempfile +import urllib +from IM.uriparse import uriparse import toscaparser.imports from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef @@ -296,7 +298,6 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): raise Exception("input value for %s in interface %s of node %s not valid" % (param_name, name, node.name)) name = node.name + "_" + interface.name - script_path = os.path.join(Tosca.ARTIFACTS_PATH, interface.implementation) # if there are artifacts to download if artifacts: @@ -304,40 +305,44 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): tasks += " - name: Download artifact " + artifact + "\n" tasks += " get_url: dest=" + remote_artifacts_path + "/" + os.path.basename(artifact) + " url='" + artifact + "'\n" - if interface.implementation.endswith(".yaml") or interface.implementation.endswith(".yml"): + implementation_url = uriparse(interface.implementation) + + if implementation_url[0] in ['http', 'https', 'ftp']: + script_path = implementation_url[2] + try: + response = urllib.urlopen(interface.implementation) + script_content = response.read() + except Exception, ex: + raise Exception("Error downloading the implementation script '%s': %s" % (interface.implementation, str(ex))) + else: + script_path = os.path.join(Tosca.ARTIFACTS_PATH, interface.implementation) if os.path.isfile(script_path): f = open(script_path) script_content = f.read() f.close() - - if env: - for var_name, var_value in env.iteritems(): - variables += " %s: %s " % (var_name, var_value) + "\n" - variables += "\n" - - recipe_list.append(script_content) else: - raise Exception(script_path + " is not located in the artifacts folder.") + raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH)) + + if script_path.endswith(".yaml") or script_path.endswith(".yml"): + if env: + for var_name, var_value in env.iteritems(): + variables += " %s: %s " % (var_name, var_value) + "\n" + variables += "\n" + + recipe_list.append(script_content) else: - if os.path.isfile(script_path): - f = open(script_path) - script_content = f.read().replace("\n","\\n") - f.close() - - recipe = "- tasks:\n" - recipe += " - name: Copy contents of script of interface " + name + "\n" - recipe += " copy: dest=/tmp/" + os.path.basename(script_path) + " content='" + script_content + "' mode=0755\n" - - recipe += " - name: " + name + "\n" - recipe += " shell: /tmp/" + os.path.basename(script_path) + "\n" - if env: - recipe += " environment:\n" - for var_name, var_value in env.iteritems(): - recipe += " %s: %s\n" % (var_name, var_value) - - recipe_list.append(recipe) - else: - raise Exception(script_path + " is not located in the artifacts folder.") + recipe = "- tasks:\n" + recipe += " - name: Copy contents of script of interface " + name + "\n" + recipe += " copy: dest=/tmp/" + os.path.basename(script_path) + " content='" + script_content + "' mode=0755\n" + + recipe += " - name: " + name + "\n" + recipe += " shell: /tmp/" + os.path.basename(script_path) + "\n" + if env: + recipe += " environment:\n" + for var_name, var_value in env.iteritems(): + recipe += " %s: %s\n" % (var_name, var_value) + + recipe_list.append(recipe) if tasks or recipe_list: name = node.name + "_conf" From cbd20cac52044725cabf11cfbc8b5cae2e3ad74a Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 15:05:33 +0100 Subject: [PATCH 085/509] Remove yaml recipes to tosca-types repo --- IM/tosca/Tosca.py | 11 +- IM/tosca/artifacts/apache/apache_install.yml | 19 --- IM/tosca/artifacts/apache/apache_start.yml | 8 - IM/tosca/artifacts/ec3/ec3_configure.yml | 139 ------------------ IM/tosca/artifacts/ec3/ec3_install.yml | 57 ------- IM/tosca/artifacts/ec3/ec3_start.yml | 33 ----- .../artifacts/galaxy/galaxy_configure.yml | 25 ---- IM/tosca/artifacts/galaxy/galaxy_install.yml | 9 -- IM/tosca/artifacts/galaxy/galaxy_start.yml | 6 - .../galaxy/galaxy_tools_configure.yml | 34 ----- .../artifacts/lrms/torque_wn_configure.yml | 21 --- IM/tosca/artifacts/lrms/torque_wn_install.yml | 13 -- IM/tosca/artifacts/lrms/torque_wn_start.yml | 11 -- IM/tosca/artifacts/mysql/mysql_configure.yml | 4 - .../artifacts/mysql/mysql_db_configure.yml | 7 - IM/tosca/artifacts/mysql/mysql_db_import.yml | 4 - IM/tosca/artifacts/mysql/mysql_install.yml | 27 ---- 17 files changed, 9 insertions(+), 419 deletions(-) delete mode 100755 IM/tosca/artifacts/apache/apache_install.yml delete mode 100755 IM/tosca/artifacts/apache/apache_start.yml delete mode 100644 IM/tosca/artifacts/ec3/ec3_configure.yml delete mode 100644 IM/tosca/artifacts/ec3/ec3_install.yml delete mode 100644 IM/tosca/artifacts/ec3/ec3_start.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_configure.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_install.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_start.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_wn_configure.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_wn_install.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_wn_start.yml delete mode 100755 IM/tosca/artifacts/mysql/mysql_configure.yml delete mode 100644 IM/tosca/artifacts/mysql/mysql_db_configure.yml delete mode 100644 IM/tosca/artifacts/mysql/mysql_db_import.yml delete mode 100755 IM/tosca/artifacts/mysql/mysql_install.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index e7beab41b..f6efe6635 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -64,7 +64,10 @@ class Tosca: """ - ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + #ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + ARTIFACTS_PATH = "/tmp" + #ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" + ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/im/master/IM/tosca/artifacts/" logger = logging.getLogger('InfrastructureManager') @@ -321,7 +324,11 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): script_content = f.read() f.close() else: - raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH)) + try: + response = urllib.urlopen(Tosca.ARTIFACTS_REMOTE_REPO + interface.implementation) + script_content = response.read() + except Exception, ex: + raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s' or in the artifacts remote url '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH, Tosca.ARTIFACTS_REMOTE_REPO)) if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: diff --git a/IM/tosca/artifacts/apache/apache_install.yml b/IM/tosca/artifacts/apache/apache_install.yml deleted file mode 100755 index b44ff9a62..000000000 --- a/IM/tosca/artifacts/apache/apache_install.yml +++ /dev/null @@ -1,19 +0,0 @@ -- tasks: - # Disable IPv6 - - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" - with_items: - - 'net.ipv6.conf.all.disable_ipv6' - - 'net.ipv6.conf.default.disable_ipv6' - - 'net.ipv6.conf.lo.disable_ipv6' - ignore_errors: yes - - - command: sysctl -p - ignore_errors: yes - - - name: Apache | Make sure the Apache packages are installed - apt: pkg=apache2 update_cache=yes - when: ansible_os_family == "Debian" - - - name: Apache | Make sure the Apache packages are installed - apt: yum=httpd - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/apache/apache_start.yml b/IM/tosca/artifacts/apache/apache_start.yml deleted file mode 100755 index a39b28a1d..000000000 --- a/IM/tosca/artifacts/apache/apache_start.yml +++ /dev/null @@ -1,8 +0,0 @@ -- tasks: - - name: Start Apache service - service: name=apache2 state=started - when: ansible_os_family == "Debian" - - - name: Start Apache service - service: name=httpd state=started - when: ansible_os_family == "RedHat" diff --git a/IM/tosca/artifacts/ec3/ec3_configure.yml b/IM/tosca/artifacts/ec3/ec3_configure.yml deleted file mode 100644 index 919f3d4fc..000000000 --- a/IM/tosca/artifacts/ec3/ec3_configure.yml +++ /dev/null @@ -1,139 +0,0 @@ ---- -- handlers: - - name: restart cluesd - service: name=cluesd state=restarted - - vars: - TORQUE_PATH: /var/spool/torque - PBS_SERVER_CONF: | - create queue batch - set queue batch queue_type = Execution - set queue batch resources_default.nodes = 1 - set queue batch enabled = True - set queue batch started = True - set server default_queue = batch - set server scheduling = True - set server scheduler_iteration = 20 - set server node_check_rate = 40 - set server resources_default.neednodes = 1 - set server resources_default.nodect = 1 - set server resources_default.nodes = 1 - set server query_other_jobs = True - set server node_pack = False - set server job_stat_rate = 30 - set server mom_job_sync = True - set server poll_jobs = True - set tcp_timeout = 600 - - tasks: - # /etc/hosts configuration - - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{IM_NODE_NET_1_IP}} torqueserver' - when: IM_NODE_NET_1_IP is defined - - - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{ansible_default_ipv4.address}} torqueserver' - when: IM_NODE_NET_1_IP is undefined - - # PBS configuration - - file: src=/var/lib/torque dest=/var/spool/torque state=link - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - - command: hostname torqueserver - when: clues_queue_system == 'torque' - - - shell: | - for i in `seq 1 {{max_instances}}`; do - item="{{wn_name}}${i}"; - grep -q "\<${item}\>" /etc/hosts || echo "127.0.0.1 ${item}.localdomain ${item}" >> /etc/hosts; - done - when: clues_queue_system == 'torque' - - - copy: dest=/etc/torque/server_name content=torqueserver - when: clues_queue_system == 'torque' - - copy: - content: | - {% for number in range(1, max_instances|int + 1) %} - vnode{{number}} - {% endfor %} - dest: "{{TORQUE_PATH}}/server_priv/nodes" - when: clues_queue_system == 'torque' - - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - - shell: echo "{{PBS_SERVER_CONF}}" | qmgr creates={{TORQUE_PATH}}/server_priv/queues/batch - when: clues_queue_system == 'torque' - - # CLUES2 Config file - - file: path=/etc/clues2 state=directory mode=755 - - - copy: src=/etc/clues2/clues2.cfg-full-example dest=/etc/clues2/clues2.cfg force=no - notify: restart cluesd - - - ini_file: dest=/etc/clues2/clues2.cfg section={{ item.section }} option={{ item.option }} value="{{ item.value }}" - with_items: - - { section: 'general', option: 'POWERMANAGER_CLASS', value: 'cluesplugins.im' } - - { section: 'scheduler_power_off_idle', option: 'IDLE_TIME', value: '300' } - - { section: 'monitoring', option: 'MAX_WAIT_POWERON', value: '2000' } - - { section: 'monitoring', option: 'MAX_WAIT_POWEROFF', value: '600' } - - { section: 'monitoring', option: 'PERIOD_LIFECYCLE', value: '10' } - - { section: 'general', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } - - { section: 'client', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } - - { section: 'client', option: 'CLUES_REQUEST_WAIT_TIMEOUT', value: '0' } - notify: restart cluesd - - # CLUES IM configuration - - file: path=/usr/local/ec3 state=directory mode=755 - # TODO: check this - - copy: dest=/usr/local/ec3/auth.dat content="type = InfrastructureManager; username = user; password = pass" - - copy: dest=/usr/local/ec3/wn_info.yml content={{wn_host_info}}\n-\n{{wn_os_info}}\n-\n{{wn_name}}\n-\n{{wn_node_type}} - - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs - notify: restart cluesd - - # CLUES PBS configuration - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs - notify: restart cluesd - when: clues_queue_system == 'torque' - - copy: src=/etc/clues2/conf.d/plugin-pbs.cfg-example dest=/etc/clues2/conf.d/plugin-pbs.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'torque' - - ini_file: dest=/etc/clues2/conf.d/plugin-pbs.cfg section=PBS option=PBS_SERVER value=torqueserver - notify: restart cluesd - when: clues_queue_system == 'torque' - - lineinfile: dest={{TORQUE_PATH}}/torque.cfg regexp=^SUBMITFILTER line='SUBMITFILTER /usr/local/bin/clues-pbs-wrapper' create=yes mode=644 - when: clues_queue_system == 'torque' - - # CLUES SGE configuration - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.sge - notify: restart cluesd - when: clues_queue_system == 'sge' - - copy: src=/etc/clues2/conf.d/plugin-sge.cfg-example dest=/etc/clues2/conf.d/plugin-sge.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'sge' - - copy: src=/etc/clues2/conf.d/wrapper-sge.cfg-example dest=/etc/clues2/conf.d/wrapper-sge.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'sge' - - lineinfile: dest={{SGE_ROOT}}/default/common/sge_request regexp='^-jsv' line="-jsv /usr/local/bin/clues-sge-wrapper" create=yes mode=644 - when: clues_queue_system == 'sge' - - lineinfile: dest=/etc/profile.d/sge_vars.sh regexp='SGE_JSV_TIMEOUT' line="export SGE_JSV_TIMEOUT=600" create=yes mode=755 - when: clues_queue_system == 'sge' - - # CLUES SLURM configuration - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.slurm - notify: restart cluesd - when: clues_queue_system == 'slurm' - - copy: src=/etc/clues2/conf.d/plugin-slurm.cfg-example dest=/etc/clues2/conf.d/plugin-slurm.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'slurm' - - ini_file: dest=/etc/clues2/conf.d/plugin-slurm.cfg section=SLURM option=SLURM_SERVER value=slurmserverpublic - notify: restart cluesd - when: clues_queue_system == 'slurm' - - command: mv /usr/local/bin/sbatch /usr/local/bin/sbatch.o creates=/usr/local/bin/sbatch.o - when: clues_queue_system == 'slurm' - - command: mv /usr/local/bin/clues-slurm-wrapper /usr/local/bin/sbatch creates=/usr/local/bin/sbatch - when: clues_queue_system == 'slurm' - - - \ No newline at end of file diff --git a/IM/tosca/artifacts/ec3/ec3_install.yml b/IM/tosca/artifacts/ec3/ec3_install.yml deleted file mode 100644 index ad9c88024..000000000 --- a/IM/tosca/artifacts/ec3/ec3_install.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -- tasks: - # General task - - name: create epel.repo - template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo - when: ansible_os_family == "RedHat" - - # CLUES2 requirements - - name: Apt install CLUES2 requirements in Deb system - apt: pkg=python-sqlite,unzip - when: ansible_os_family == "Debian" - - - name: Yum install CLUES2 requirements in REL system - yum: pkg=python-sqlite2,unzip - when: ansible_os_family == "RedHat" - - - name: Install CLUES pip requirements - pip: name={{item}} - with_items: - - web.py - - ply - - - get_url: url=https://github.com/grycap/{{item}}/archive/master.zip dest=/tmp/{{item}}.zip - register: result - until: result|success - retries: 5 - delay: 1 - with_items: - - clues - - cpyutils - - # CLUES2 installation - - unarchive: src=/tmp/{{item}}.zip dest=/tmp copy=no - with_items: - - clues - - cpyutils - - - command: python setup.py install chdir=/tmp/clues-master creates=/usr/local/bin/cluesserver - - command: python setup.py install chdir=/tmp/cpyutils-master - - # IM installation - - apt: name=python-soappy update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - yum: name=SOAPpy - when: ansible_os_family == "RedHat" - - pip: name=IM - - file: path=/etc/init.d/im mode=0755 - - - # PBS installation - - name: Apt install Torque in Deb system - apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - - name: Yum install Torque in REL system - yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' diff --git a/IM/tosca/artifacts/ec3/ec3_start.yml b/IM/tosca/artifacts/ec3/ec3_start.yml deleted file mode 100644 index cc1334ea6..000000000 --- a/IM/tosca/artifacts/ec3/ec3_start.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -- tasks: - # Launch CLUES - - service: name=cluesd state=started - # Launch IM - - service: name=im state=started - - # Launch PBS - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=torque-server state=restarted pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=pbs_server state=restarted pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - - command: sleep 5 - when: clues_queue_system == 'torque' - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' \ No newline at end of file diff --git a/IM/tosca/artifacts/galaxy/galaxy_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_configure.yml deleted file mode 100644 index 164bbaf92..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_configure.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -- vars: - GALAXY_USER_ID: 4001 - GALAXY_USER_PASSWORD: $6$Ehg4GHQT5y$6ZCTLffp.epiNEhS1M3ZB.P6Kii1wELySe/DCwUInGt8r7zgdAHfHw66DuPwpS6pfOiZ9PS/KaTiBKjoCn23t0 - - tasks: - # General configuration - - copy: src={{galaxy_install_path}}/config/galaxy.ini.sample dest={{galaxy_install_path}}/config/galaxy.ini force=no - - ini_file: dest={{galaxy_install_path}}/config/galaxy.ini section={{ item.section }} option={{ item.option }} value="{{ item.value }}" - with_items: - - { section: 'server:main', option: 'host', value: '0.0.0.0' } - - { section: 'app:main', option: 'admin_users', value: "{{galaxy_admin}}" } - - { section: 'app:main', option: 'master_api_key', value: "{{galaxy_admin_api_key}}" } - - { section: 'app:main', option: 'tool_dependency_dir', value: "{{galaxy_install_path}}/tool_dependency_dir" } - - # Create galaxy user to launch the daemon - - user: name={{galaxy_user}} password={{GALAXY_USER_PASSWORD}} generate_ssh_key=yes shell=/bin/bash uid={{GALAXY_USER_ID}} - - local_action: command cp /home/{{galaxy_user}}/.ssh/id_rsa.pub /tmp/{{galaxy_user}}_id_rsa.pub creates=/tmp/{{galaxy_user}}_id_rsa.pub - - name: Add the authorized_key to the user {{galaxy_user}} - authorized_key: user={{galaxy_user}} key="{{ lookup('file', '/tmp/' + galaxy_user + '_id_rsa.pub') }}" - - - file: path=/home/{{galaxy_user}} state=directory owner={{galaxy_user}} group={{galaxy_user}} - - file: path={{galaxy_install_path}} state=directory recurse=yes owner={{galaxy_user}} - - - copy: dest="{{galaxy_install_path}}/config/local_env.sh" content="PYTHON_EGG_CACHE={{galaxy_install_path}}/egg\nGALAXY_RUN_ALL=1\nexport PYTHON_EGG_CACHE GALAXY_RUN_ALL" \ No newline at end of file diff --git a/IM/tosca/artifacts/galaxy/galaxy_install.yml b/IM/tosca/artifacts/galaxy/galaxy_install.yml deleted file mode 100644 index cd6689fb0..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_install.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- tasks: - # Install requisites - - apt: name=git update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - yum: name=git - when: ansible_os_family == "RedHat" - # Download Galaxy - - git: repo=https://github.com/galaxyproject/galaxy/ dest={{galaxy_install_path}} version=master diff --git a/IM/tosca/artifacts/galaxy/galaxy_start.yml b/IM/tosca/artifacts/galaxy/galaxy_start.yml deleted file mode 100644 index 0801d10d8..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_start.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- tasks: - # Launch the server - - shell: bash run.sh --daemon chdir={{galaxy_install_path}}/ creates={{galaxy_install_path}}/main.pid - sudo: true - sudo_user: "{{galaxy_user}}" diff --git a/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml deleted file mode 100644 index 047fca373..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -- vars: - tool_content: | - tools: - - name: '{{galaxy_tool_name}}' - owner: '{{galaxy_tool_owner}}' - tool_panel_section_id: '{{galaxy_tool_panel_section_id}}' - tasks: - # Install galaxy tools - - name: Uninstall old version of python-requests in Ubuntu - shell: dpkg --force-all -r python-requests - when: ansible_os_family == "Debian" - ignore_errors: yes - - - name: Install script dependencies - pip: name={{item}} state=latest - with_items: - - bioblend - - requests - - - name: Place the tool management script - get_url: url=https://raw.githubusercontent.com/galaxyproject/ansible-galaxy-tools/master/files/install_tool_shed_tools.py dest={{galaxy_install_path}}/install_tool_shed_tools.py - - - name: Copy tool list files - copy: - content: "{{tool_content}}" - dest: "{{galaxy_install_path}}/my_tool_list.yml" - - - name: Wait for Galaxy to start - wait_for: port=8080 delay=5 state=started timeout=150 - - - name: Install Tool Shed tools - shell: chdir={{galaxy_install_path}} python install_tool_shed_tools.py -t my_tool_list.yml -a {{galaxy_admin_api_key}} -g 127.0.0.1:8080 - #creates={{galaxy_install_path}}//tool_dependency_dir/bowtie2 diff --git a/IM/tosca/artifacts/lrms/torque_wn_configure.yml b/IM/tosca/artifacts/lrms/torque_wn_configure.yml deleted file mode 100644 index bbe936985..000000000 --- a/IM/tosca/artifacts/lrms/torque_wn_configure.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- - - vars: - PBS_SERVER_CONF: | - create queue batch - set queue batch queue_type = Execution - set queue batch resources_default.nodes = 1 - set queue batch enabled = True - set queue batch started = True - set server default_queue = batch - set server scheduling = True - set server scheduler_iteration = 20 - set server node_check_rate = 40 - set server resources_default.neednodes = 1 - set server resources_default.nodect = 1 - set server resources_default.nodes = 1 - set server query_other_jobs = True - set server node_pack = False - set server job_stat_rate = 30 - set server mom_job_sync = True - set server poll_jobs = True - set tcp_timeout = 600 \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_install.yml b/IM/tosca/artifacts/lrms/torque_wn_install.yml deleted file mode 100644 index 7c76883c7..000000000 --- a/IM/tosca/artifacts/lrms/torque_wn_install.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- - - tasks: - - name: create epel.repo - template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo - when: ansible_os_family == "RedHat" - - - name: Apt install Torque in Deb system - apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - - name: Yum install Torque in REL system - yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_start.yml b/IM/tosca/artifacts/lrms/torque_wn_start.yml deleted file mode 100644 index 55c9f708b..000000000 --- a/IM/tosca/artifacts/lrms/torque_wn_start.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- - - tasks: - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/mysql/mysql_configure.yml b/IM/tosca/artifacts/mysql/mysql_configure.yml deleted file mode 100755 index 446ac80c9..000000000 --- a/IM/tosca/artifacts/mysql/mysql_configure.yml +++ /dev/null @@ -1,4 +0,0 @@ -- tasks: - - name: update mysql root password for all root accounts - mysql_user: name=root password={{root_password}} - ignore_errors: yes diff --git a/IM/tosca/artifacts/mysql/mysql_db_configure.yml b/IM/tosca/artifacts/mysql/mysql_db_configure.yml deleted file mode 100644 index fb3e23f7b..000000000 --- a/IM/tosca/artifacts/mysql/mysql_db_configure.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- tasks: - - name: Create DB {{db_name}} - mysql_db: name={{db_name}} state=present login_user=root login_password={{db_root_password}} - - - name: Create user {{db_user}} for the DB {{db_name}} - mysql_user: name={{db_user}} password={{db_password}} login_user=root login_password={{db_root_password}} priv={{db_name}}.*:ALL,GRANT state=present diff --git a/IM/tosca/artifacts/mysql/mysql_db_import.yml b/IM/tosca/artifacts/mysql/mysql_db_import.yml deleted file mode 100644 index 21cec3fd5..000000000 --- a/IM/tosca/artifacts/mysql/mysql_db_import.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -- tasks: - - name: Import the DB - mysql_db: name={{db_name}} state=import target={{db_data}} login_user={{db_user}} login_password={{db_password}} \ No newline at end of file diff --git a/IM/tosca/artifacts/mysql/mysql_install.yml b/IM/tosca/artifacts/mysql/mysql_install.yml deleted file mode 100755 index d7e5897c8..000000000 --- a/IM/tosca/artifacts/mysql/mysql_install.yml +++ /dev/null @@ -1,27 +0,0 @@ -- tasks: - # Disable IPv6 - - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" - with_items: - - 'net.ipv6.conf.all.disable_ipv6' - - 'net.ipv6.conf.default.disable_ipv6' - - 'net.ipv6.conf.lo.disable_ipv6' - ignore_errors: yes - - - command: sysctl -p - ignore_errors: yes - - - name: MySQL | Make sure the MySQL packages are installed - apt: pkg=mysql-server,python-mysqldb update_cache=yes - when: ansible_os_family == "Debian" - - - name: Start MySQL service - service: name=mysql state=started - when: ansible_os_family == "Debian" - - - name: MySQL | Make sure the MySQL packages are installed - apt: yum=mysql-server,MySQL-python - when: ansible_os_family == "RedHat" - - - name: Start MySQL service - service: name=mysqld state=started - when: ansible_os_family == "RedHat" From d2715b5bd808d8ab2552c019ab0bdbd7c2a209c5 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 15:05:33 +0100 Subject: [PATCH 086/509] Remove yaml recipes to tosca-types repo --- IM/tosca/Tosca.py | 11 +- IM/tosca/artifacts/apache/apache_install.yml | 19 --- IM/tosca/artifacts/apache/apache_start.yml | 8 - IM/tosca/artifacts/ec3/ec3_configure.yml | 139 ------------------ IM/tosca/artifacts/ec3/ec3_install.yml | 57 ------- IM/tosca/artifacts/ec3/ec3_start.yml | 33 ----- .../artifacts/galaxy/galaxy_configure.yml | 25 ---- IM/tosca/artifacts/galaxy/galaxy_install.yml | 9 -- IM/tosca/artifacts/galaxy/galaxy_start.yml | 6 - .../galaxy/galaxy_tools_configure.yml | 34 ----- .../artifacts/lrms/torque_wn_configure.yml | 21 --- IM/tosca/artifacts/lrms/torque_wn_install.yml | 13 -- IM/tosca/artifacts/lrms/torque_wn_start.yml | 11 -- IM/tosca/artifacts/mysql/mysql_configure.yml | 4 - .../artifacts/mysql/mysql_db_configure.yml | 7 - IM/tosca/artifacts/mysql/mysql_db_import.yml | 4 - IM/tosca/artifacts/mysql/mysql_install.yml | 27 ---- 17 files changed, 9 insertions(+), 419 deletions(-) delete mode 100755 IM/tosca/artifacts/apache/apache_install.yml delete mode 100755 IM/tosca/artifacts/apache/apache_start.yml delete mode 100644 IM/tosca/artifacts/ec3/ec3_configure.yml delete mode 100644 IM/tosca/artifacts/ec3/ec3_install.yml delete mode 100644 IM/tosca/artifacts/ec3/ec3_start.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_configure.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_install.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_start.yml delete mode 100644 IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_wn_configure.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_wn_install.yml delete mode 100644 IM/tosca/artifacts/lrms/torque_wn_start.yml delete mode 100755 IM/tosca/artifacts/mysql/mysql_configure.yml delete mode 100644 IM/tosca/artifacts/mysql/mysql_db_configure.yml delete mode 100644 IM/tosca/artifacts/mysql/mysql_db_import.yml delete mode 100755 IM/tosca/artifacts/mysql/mysql_install.yml diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index e7beab41b..f6efe6635 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -64,7 +64,10 @@ class Tosca: """ - ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + #ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + ARTIFACTS_PATH = "/tmp" + #ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" + ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/im/master/IM/tosca/artifacts/" logger = logging.getLogger('InfrastructureManager') @@ -321,7 +324,11 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): script_content = f.read() f.close() else: - raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH)) + try: + response = urllib.urlopen(Tosca.ARTIFACTS_REMOTE_REPO + interface.implementation) + script_content = response.read() + except Exception, ex: + raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s' or in the artifacts remote url '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH, Tosca.ARTIFACTS_REMOTE_REPO)) if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: diff --git a/IM/tosca/artifacts/apache/apache_install.yml b/IM/tosca/artifacts/apache/apache_install.yml deleted file mode 100755 index b44ff9a62..000000000 --- a/IM/tosca/artifacts/apache/apache_install.yml +++ /dev/null @@ -1,19 +0,0 @@ -- tasks: - # Disable IPv6 - - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" - with_items: - - 'net.ipv6.conf.all.disable_ipv6' - - 'net.ipv6.conf.default.disable_ipv6' - - 'net.ipv6.conf.lo.disable_ipv6' - ignore_errors: yes - - - command: sysctl -p - ignore_errors: yes - - - name: Apache | Make sure the Apache packages are installed - apt: pkg=apache2 update_cache=yes - when: ansible_os_family == "Debian" - - - name: Apache | Make sure the Apache packages are installed - apt: yum=httpd - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/apache/apache_start.yml b/IM/tosca/artifacts/apache/apache_start.yml deleted file mode 100755 index a39b28a1d..000000000 --- a/IM/tosca/artifacts/apache/apache_start.yml +++ /dev/null @@ -1,8 +0,0 @@ -- tasks: - - name: Start Apache service - service: name=apache2 state=started - when: ansible_os_family == "Debian" - - - name: Start Apache service - service: name=httpd state=started - when: ansible_os_family == "RedHat" diff --git a/IM/tosca/artifacts/ec3/ec3_configure.yml b/IM/tosca/artifacts/ec3/ec3_configure.yml deleted file mode 100644 index 919f3d4fc..000000000 --- a/IM/tosca/artifacts/ec3/ec3_configure.yml +++ /dev/null @@ -1,139 +0,0 @@ ---- -- handlers: - - name: restart cluesd - service: name=cluesd state=restarted - - vars: - TORQUE_PATH: /var/spool/torque - PBS_SERVER_CONF: | - create queue batch - set queue batch queue_type = Execution - set queue batch resources_default.nodes = 1 - set queue batch enabled = True - set queue batch started = True - set server default_queue = batch - set server scheduling = True - set server scheduler_iteration = 20 - set server node_check_rate = 40 - set server resources_default.neednodes = 1 - set server resources_default.nodect = 1 - set server resources_default.nodes = 1 - set server query_other_jobs = True - set server node_pack = False - set server job_stat_rate = 30 - set server mom_job_sync = True - set server poll_jobs = True - set tcp_timeout = 600 - - tasks: - # /etc/hosts configuration - - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{IM_NODE_NET_1_IP}} torqueserver' - when: IM_NODE_NET_1_IP is defined - - - lineinfile: dest=/etc/hosts regexp='torqueserver' line='{{ansible_default_ipv4.address}} torqueserver' - when: IM_NODE_NET_1_IP is undefined - - # PBS configuration - - file: src=/var/lib/torque dest=/var/spool/torque state=link - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - - command: hostname torqueserver - when: clues_queue_system == 'torque' - - - shell: | - for i in `seq 1 {{max_instances}}`; do - item="{{wn_name}}${i}"; - grep -q "\<${item}\>" /etc/hosts || echo "127.0.0.1 ${item}.localdomain ${item}" >> /etc/hosts; - done - when: clues_queue_system == 'torque' - - - copy: dest=/etc/torque/server_name content=torqueserver - when: clues_queue_system == 'torque' - - copy: - content: | - {% for number in range(1, max_instances|int + 1) %} - vnode{{number}} - {% endfor %} - dest: "{{TORQUE_PATH}}/server_priv/nodes" - when: clues_queue_system == 'torque' - - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - - shell: echo "{{PBS_SERVER_CONF}}" | qmgr creates={{TORQUE_PATH}}/server_priv/queues/batch - when: clues_queue_system == 'torque' - - # CLUES2 Config file - - file: path=/etc/clues2 state=directory mode=755 - - - copy: src=/etc/clues2/clues2.cfg-full-example dest=/etc/clues2/clues2.cfg force=no - notify: restart cluesd - - - ini_file: dest=/etc/clues2/clues2.cfg section={{ item.section }} option={{ item.option }} value="{{ item.value }}" - with_items: - - { section: 'general', option: 'POWERMANAGER_CLASS', value: 'cluesplugins.im' } - - { section: 'scheduler_power_off_idle', option: 'IDLE_TIME', value: '300' } - - { section: 'monitoring', option: 'MAX_WAIT_POWERON', value: '2000' } - - { section: 'monitoring', option: 'MAX_WAIT_POWEROFF', value: '600' } - - { section: 'monitoring', option: 'PERIOD_LIFECYCLE', value: '10' } - - { section: 'general', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } - - { section: 'client', option: 'CLUES_SECRET_TOKEN', value: '{{clues_secret_token}}' } - - { section: 'client', option: 'CLUES_REQUEST_WAIT_TIMEOUT', value: '0' } - notify: restart cluesd - - # CLUES IM configuration - - file: path=/usr/local/ec3 state=directory mode=755 - # TODO: check this - - copy: dest=/usr/local/ec3/auth.dat content="type = InfrastructureManager; username = user; password = pass" - - copy: dest=/usr/local/ec3/wn_info.yml content={{wn_host_info}}\n-\n{{wn_os_info}}\n-\n{{wn_name}}\n-\n{{wn_node_type}} - - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs - notify: restart cluesd - - # CLUES PBS configuration - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.pbs - notify: restart cluesd - when: clues_queue_system == 'torque' - - copy: src=/etc/clues2/conf.d/plugin-pbs.cfg-example dest=/etc/clues2/conf.d/plugin-pbs.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'torque' - - ini_file: dest=/etc/clues2/conf.d/plugin-pbs.cfg section=PBS option=PBS_SERVER value=torqueserver - notify: restart cluesd - when: clues_queue_system == 'torque' - - lineinfile: dest={{TORQUE_PATH}}/torque.cfg regexp=^SUBMITFILTER line='SUBMITFILTER /usr/local/bin/clues-pbs-wrapper' create=yes mode=644 - when: clues_queue_system == 'torque' - - # CLUES SGE configuration - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.sge - notify: restart cluesd - when: clues_queue_system == 'sge' - - copy: src=/etc/clues2/conf.d/plugin-sge.cfg-example dest=/etc/clues2/conf.d/plugin-sge.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'sge' - - copy: src=/etc/clues2/conf.d/wrapper-sge.cfg-example dest=/etc/clues2/conf.d/wrapper-sge.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'sge' - - lineinfile: dest={{SGE_ROOT}}/default/common/sge_request regexp='^-jsv' line="-jsv /usr/local/bin/clues-sge-wrapper" create=yes mode=644 - when: clues_queue_system == 'sge' - - lineinfile: dest=/etc/profile.d/sge_vars.sh regexp='SGE_JSV_TIMEOUT' line="export SGE_JSV_TIMEOUT=600" create=yes mode=755 - when: clues_queue_system == 'sge' - - # CLUES SLURM configuration - - ini_file: dest=/etc/clues2/clues2.cfg section=general option=LRMS_CLASS value=cluesplugins.slurm - notify: restart cluesd - when: clues_queue_system == 'slurm' - - copy: src=/etc/clues2/conf.d/plugin-slurm.cfg-example dest=/etc/clues2/conf.d/plugin-slurm.cfg force=no - notify: restart cluesd - when: clues_queue_system == 'slurm' - - ini_file: dest=/etc/clues2/conf.d/plugin-slurm.cfg section=SLURM option=SLURM_SERVER value=slurmserverpublic - notify: restart cluesd - when: clues_queue_system == 'slurm' - - command: mv /usr/local/bin/sbatch /usr/local/bin/sbatch.o creates=/usr/local/bin/sbatch.o - when: clues_queue_system == 'slurm' - - command: mv /usr/local/bin/clues-slurm-wrapper /usr/local/bin/sbatch creates=/usr/local/bin/sbatch - when: clues_queue_system == 'slurm' - - - \ No newline at end of file diff --git a/IM/tosca/artifacts/ec3/ec3_install.yml b/IM/tosca/artifacts/ec3/ec3_install.yml deleted file mode 100644 index ad9c88024..000000000 --- a/IM/tosca/artifacts/ec3/ec3_install.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -- tasks: - # General task - - name: create epel.repo - template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo - when: ansible_os_family == "RedHat" - - # CLUES2 requirements - - name: Apt install CLUES2 requirements in Deb system - apt: pkg=python-sqlite,unzip - when: ansible_os_family == "Debian" - - - name: Yum install CLUES2 requirements in REL system - yum: pkg=python-sqlite2,unzip - when: ansible_os_family == "RedHat" - - - name: Install CLUES pip requirements - pip: name={{item}} - with_items: - - web.py - - ply - - - get_url: url=https://github.com/grycap/{{item}}/archive/master.zip dest=/tmp/{{item}}.zip - register: result - until: result|success - retries: 5 - delay: 1 - with_items: - - clues - - cpyutils - - # CLUES2 installation - - unarchive: src=/tmp/{{item}}.zip dest=/tmp copy=no - with_items: - - clues - - cpyutils - - - command: python setup.py install chdir=/tmp/clues-master creates=/usr/local/bin/cluesserver - - command: python setup.py install chdir=/tmp/cpyutils-master - - # IM installation - - apt: name=python-soappy update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - yum: name=SOAPpy - when: ansible_os_family == "RedHat" - - pip: name=IM - - file: path=/etc/init.d/im mode=0755 - - - # PBS installation - - name: Apt install Torque in Deb system - apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - - name: Yum install Torque in REL system - yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' diff --git a/IM/tosca/artifacts/ec3/ec3_start.yml b/IM/tosca/artifacts/ec3/ec3_start.yml deleted file mode 100644 index cc1334ea6..000000000 --- a/IM/tosca/artifacts/ec3/ec3_start.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -- tasks: - # Launch CLUES - - service: name=cluesd state=started - # Launch IM - - service: name=im state=started - - # Launch PBS - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=torque-server state=restarted pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=pbs_server state=restarted pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - - command: sleep 5 - when: clues_queue_system == 'torque' - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" and clues_queue_system == 'torque' - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" and clues_queue_system == 'torque' \ No newline at end of file diff --git a/IM/tosca/artifacts/galaxy/galaxy_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_configure.yml deleted file mode 100644 index 164bbaf92..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_configure.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -- vars: - GALAXY_USER_ID: 4001 - GALAXY_USER_PASSWORD: $6$Ehg4GHQT5y$6ZCTLffp.epiNEhS1M3ZB.P6Kii1wELySe/DCwUInGt8r7zgdAHfHw66DuPwpS6pfOiZ9PS/KaTiBKjoCn23t0 - - tasks: - # General configuration - - copy: src={{galaxy_install_path}}/config/galaxy.ini.sample dest={{galaxy_install_path}}/config/galaxy.ini force=no - - ini_file: dest={{galaxy_install_path}}/config/galaxy.ini section={{ item.section }} option={{ item.option }} value="{{ item.value }}" - with_items: - - { section: 'server:main', option: 'host', value: '0.0.0.0' } - - { section: 'app:main', option: 'admin_users', value: "{{galaxy_admin}}" } - - { section: 'app:main', option: 'master_api_key', value: "{{galaxy_admin_api_key}}" } - - { section: 'app:main', option: 'tool_dependency_dir', value: "{{galaxy_install_path}}/tool_dependency_dir" } - - # Create galaxy user to launch the daemon - - user: name={{galaxy_user}} password={{GALAXY_USER_PASSWORD}} generate_ssh_key=yes shell=/bin/bash uid={{GALAXY_USER_ID}} - - local_action: command cp /home/{{galaxy_user}}/.ssh/id_rsa.pub /tmp/{{galaxy_user}}_id_rsa.pub creates=/tmp/{{galaxy_user}}_id_rsa.pub - - name: Add the authorized_key to the user {{galaxy_user}} - authorized_key: user={{galaxy_user}} key="{{ lookup('file', '/tmp/' + galaxy_user + '_id_rsa.pub') }}" - - - file: path=/home/{{galaxy_user}} state=directory owner={{galaxy_user}} group={{galaxy_user}} - - file: path={{galaxy_install_path}} state=directory recurse=yes owner={{galaxy_user}} - - - copy: dest="{{galaxy_install_path}}/config/local_env.sh" content="PYTHON_EGG_CACHE={{galaxy_install_path}}/egg\nGALAXY_RUN_ALL=1\nexport PYTHON_EGG_CACHE GALAXY_RUN_ALL" \ No newline at end of file diff --git a/IM/tosca/artifacts/galaxy/galaxy_install.yml b/IM/tosca/artifacts/galaxy/galaxy_install.yml deleted file mode 100644 index cd6689fb0..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_install.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- tasks: - # Install requisites - - apt: name=git update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - yum: name=git - when: ansible_os_family == "RedHat" - # Download Galaxy - - git: repo=https://github.com/galaxyproject/galaxy/ dest={{galaxy_install_path}} version=master diff --git a/IM/tosca/artifacts/galaxy/galaxy_start.yml b/IM/tosca/artifacts/galaxy/galaxy_start.yml deleted file mode 100644 index 0801d10d8..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_start.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- tasks: - # Launch the server - - shell: bash run.sh --daemon chdir={{galaxy_install_path}}/ creates={{galaxy_install_path}}/main.pid - sudo: true - sudo_user: "{{galaxy_user}}" diff --git a/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml b/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml deleted file mode 100644 index 047fca373..000000000 --- a/IM/tosca/artifacts/galaxy/galaxy_tools_configure.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -- vars: - tool_content: | - tools: - - name: '{{galaxy_tool_name}}' - owner: '{{galaxy_tool_owner}}' - tool_panel_section_id: '{{galaxy_tool_panel_section_id}}' - tasks: - # Install galaxy tools - - name: Uninstall old version of python-requests in Ubuntu - shell: dpkg --force-all -r python-requests - when: ansible_os_family == "Debian" - ignore_errors: yes - - - name: Install script dependencies - pip: name={{item}} state=latest - with_items: - - bioblend - - requests - - - name: Place the tool management script - get_url: url=https://raw.githubusercontent.com/galaxyproject/ansible-galaxy-tools/master/files/install_tool_shed_tools.py dest={{galaxy_install_path}}/install_tool_shed_tools.py - - - name: Copy tool list files - copy: - content: "{{tool_content}}" - dest: "{{galaxy_install_path}}/my_tool_list.yml" - - - name: Wait for Galaxy to start - wait_for: port=8080 delay=5 state=started timeout=150 - - - name: Install Tool Shed tools - shell: chdir={{galaxy_install_path}} python install_tool_shed_tools.py -t my_tool_list.yml -a {{galaxy_admin_api_key}} -g 127.0.0.1:8080 - #creates={{galaxy_install_path}}//tool_dependency_dir/bowtie2 diff --git a/IM/tosca/artifacts/lrms/torque_wn_configure.yml b/IM/tosca/artifacts/lrms/torque_wn_configure.yml deleted file mode 100644 index bbe936985..000000000 --- a/IM/tosca/artifacts/lrms/torque_wn_configure.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- - - vars: - PBS_SERVER_CONF: | - create queue batch - set queue batch queue_type = Execution - set queue batch resources_default.nodes = 1 - set queue batch enabled = True - set queue batch started = True - set server default_queue = batch - set server scheduling = True - set server scheduler_iteration = 20 - set server node_check_rate = 40 - set server resources_default.neednodes = 1 - set server resources_default.nodect = 1 - set server resources_default.nodes = 1 - set server query_other_jobs = True - set server node_pack = False - set server job_stat_rate = 30 - set server mom_job_sync = True - set server poll_jobs = True - set tcp_timeout = 600 \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_install.yml b/IM/tosca/artifacts/lrms/torque_wn_install.yml deleted file mode 100644 index 7c76883c7..000000000 --- a/IM/tosca/artifacts/lrms/torque_wn_install.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- - - tasks: - - name: create epel.repo - template: src=utils/templates/epel-es.repo dest=/etc/yum.repos.d/epel.repo - when: ansible_os_family == "RedHat" - - - name: Apt install Torque in Deb system - apt: name=torque-server,torque-client,g++,libtorque2-dev,make update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - - name: Yum install Torque in REL system - yum: name=torque-server,torque-scheduler,torque-client,openssh-clients,gcc-c++,torque-devel,make - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/lrms/torque_wn_start.yml b/IM/tosca/artifacts/lrms/torque_wn_start.yml deleted file mode 100644 index 55c9f708b..000000000 --- a/IM/tosca/artifacts/lrms/torque_wn_start.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- - - tasks: - - service: name=torque-scheduler state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "Debian" - - service: name=torque-server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "Debian" - - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_sched - when: ansible_os_family == "RedHat" - - service: name=pbs_server state=started pattern=/usr/sbin/pbs_server - when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/IM/tosca/artifacts/mysql/mysql_configure.yml b/IM/tosca/artifacts/mysql/mysql_configure.yml deleted file mode 100755 index 446ac80c9..000000000 --- a/IM/tosca/artifacts/mysql/mysql_configure.yml +++ /dev/null @@ -1,4 +0,0 @@ -- tasks: - - name: update mysql root password for all root accounts - mysql_user: name=root password={{root_password}} - ignore_errors: yes diff --git a/IM/tosca/artifacts/mysql/mysql_db_configure.yml b/IM/tosca/artifacts/mysql/mysql_db_configure.yml deleted file mode 100644 index fb3e23f7b..000000000 --- a/IM/tosca/artifacts/mysql/mysql_db_configure.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- tasks: - - name: Create DB {{db_name}} - mysql_db: name={{db_name}} state=present login_user=root login_password={{db_root_password}} - - - name: Create user {{db_user}} for the DB {{db_name}} - mysql_user: name={{db_user}} password={{db_password}} login_user=root login_password={{db_root_password}} priv={{db_name}}.*:ALL,GRANT state=present diff --git a/IM/tosca/artifacts/mysql/mysql_db_import.yml b/IM/tosca/artifacts/mysql/mysql_db_import.yml deleted file mode 100644 index 21cec3fd5..000000000 --- a/IM/tosca/artifacts/mysql/mysql_db_import.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -- tasks: - - name: Import the DB - mysql_db: name={{db_name}} state=import target={{db_data}} login_user={{db_user}} login_password={{db_password}} \ No newline at end of file diff --git a/IM/tosca/artifacts/mysql/mysql_install.yml b/IM/tosca/artifacts/mysql/mysql_install.yml deleted file mode 100755 index d7e5897c8..000000000 --- a/IM/tosca/artifacts/mysql/mysql_install.yml +++ /dev/null @@ -1,27 +0,0 @@ -- tasks: - # Disable IPv6 - - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" - with_items: - - 'net.ipv6.conf.all.disable_ipv6' - - 'net.ipv6.conf.default.disable_ipv6' - - 'net.ipv6.conf.lo.disable_ipv6' - ignore_errors: yes - - - command: sysctl -p - ignore_errors: yes - - - name: MySQL | Make sure the MySQL packages are installed - apt: pkg=mysql-server,python-mysqldb update_cache=yes - when: ansible_os_family == "Debian" - - - name: Start MySQL service - service: name=mysql state=started - when: ansible_os_family == "Debian" - - - name: MySQL | Make sure the MySQL packages are installed - apt: yum=mysql-server,MySQL-python - when: ansible_os_family == "RedHat" - - - name: Start MySQL service - service: name=mysqld state=started - when: ansible_os_family == "RedHat" From f2d2415cdb27ffa17e7410b55baca4817c5cc35c Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 15:08:36 +0100 Subject: [PATCH 087/509] Bugfix Tosca --- IM/tosca/Tosca.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index f6efe6635..5324e4544 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -64,10 +64,8 @@ class Tosca: """ - #ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" - ARTIFACTS_PATH = "/tmp" - #ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" - ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/im/master/IM/tosca/artifacts/" + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" logger = logging.getLogger('InfrastructureManager') From 246f8db386d9c9b46b9780bcf1c34ab98302327e Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 15:08:36 +0100 Subject: [PATCH 088/509] Bugfix Tosca --- IM/tosca/Tosca.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index f6efe6635..5324e4544 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -64,10 +64,8 @@ class Tosca: """ - #ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" - ARTIFACTS_PATH = "/tmp" - #ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" - ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/im/master/IM/tosca/artifacts/" + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" logger = logging.getLogger('InfrastructureManager') From a3827628ba87a6ac82e92fca662489da1b9affb8 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 16:07:45 +0100 Subject: [PATCH 089/509] Bugfix in setup process --- MANIFEST.in | 5 ++--- setup.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index da5e2d07e..daa02347e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,9 @@ recursive-exclude test * recursive-include contextualization * -recursive-include IM/tosca/artifacts * -include IM/tosca/tosca-types/custom_types.yaml +include IM/tosca/tosca-types/custom_types.yaml include scripts/im include etc/im.cfg include LICENSE include INSTALL include NOTICE -include changelog \ No newline at end of file +include changelog diff --git a/setup.py b/setup.py index 07db10c57..a669cc667 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', packages=['IM','IM.tosca', 'IM.radl', 'IM.ansible','connectors'], + include_package_data = True, scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From baccbcca1bb731010b7572c2aa6a02840589ded0 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 16:07:45 +0100 Subject: [PATCH 090/509] Bugfix in setup process --- MANIFEST.in | 5 ++--- setup.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index da5e2d07e..daa02347e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,9 @@ recursive-exclude test * recursive-include contextualization * -recursive-include IM/tosca/artifacts * -include IM/tosca/tosca-types/custom_types.yaml +include IM/tosca/tosca-types/custom_types.yaml include scripts/im include etc/im.cfg include LICENSE include INSTALL include NOTICE -include changelog \ No newline at end of file +include changelog diff --git a/setup.py b/setup.py index 07db10c57..a669cc667 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', packages=['IM','IM.tosca', 'IM.radl', 'IM.ansible','connectors'], + include_package_data = True, scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From 3ad1604fe0c0947cc9c91b66297e195acb662144 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 16:46:52 +0100 Subject: [PATCH 091/509] Add TOSCA Test --- test/QuickTestIM.py | 123 +++++++++++++++++++++++++++++++++++++++----- test/TestIM.py | 111 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 12 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 6277a001e..33d311217 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -168,18 +168,6 @@ def test_16_get_vm_property(self): self.assertNotEqual(info, None, msg="ERROR in the value returned by GetVMProperty: " + info) self.assertNotEqual(info, "", msg="ERROR in the value returned by GetVMPropert: " + info) -# def test_17_get_ganglia_info(self): -# """ -# Test the Ganglia IM information integration -# """ -# (success, vm_ids) = self.server.GetInfrastructureInfo(self.inf_id, self.auth_data) -# self.assertTrue(success, msg="ERROR calling GetInfrastructureInfo: " + str(vm_ids)) -# (success, info) = self.server.GetVMInfo(self.inf_id, vm_ids[1], self.auth_data) -# self.assertTrue(success, msg="ERROR calling GetVMInfo: " + str(info)) -# info_radl = radl_parse.parse_radl(info) -# prop_usage = info_radl.systems[0].getValue("cpu.usage") -# self.assertIsNotNone(prop_usage, msg="ERROR getting ganglia VM info (cpu.usage = None) of VM " + str(vm_ids[1])) - def test_18_error_addresource(self): """ Test to get error when adding a resource with an incorrect RADL @@ -447,5 +435,116 @@ def test_75_destroy(self): (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + def test_90_create_tosca(self): + """ + Test the CreateInfrastructure IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + """ + + (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) + self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) + self.__class__.inf_id = inf_id + + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) + self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") + + def test_95_destroy(self): + """ + Test DestroyInfrastructure function + """ + (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) + self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + + if __name__ == '__main__': unittest.main() diff --git a/test/TestIM.py b/test/TestIM.py index c9fde294b..1e11d06b6 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -520,5 +520,116 @@ def test_85_destroy(self): (success, res) = self.server.DestroyInfrastructure(inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + def test_90_create_tosca(self): + """ + Test the CreateInfrastructure IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + """ + + (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) + self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) + self.__class__.inf_id = inf_id + + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 600) + self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") + + def test_95_destroy(self): + """ + Test DestroyInfrastructure function + """ + (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) + self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + if __name__ == '__main__': unittest.main() From 6c8f25a01a3b527d496b43d692ee8f1f0a5bef10 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jan 2016 16:46:52 +0100 Subject: [PATCH 092/509] Add TOSCA Test --- test/QuickTestIM.py | 123 +++++++++++++++++++++++++++++++++++++++----- test/TestIM.py | 111 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 12 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 6277a001e..33d311217 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -168,18 +168,6 @@ def test_16_get_vm_property(self): self.assertNotEqual(info, None, msg="ERROR in the value returned by GetVMProperty: " + info) self.assertNotEqual(info, "", msg="ERROR in the value returned by GetVMPropert: " + info) -# def test_17_get_ganglia_info(self): -# """ -# Test the Ganglia IM information integration -# """ -# (success, vm_ids) = self.server.GetInfrastructureInfo(self.inf_id, self.auth_data) -# self.assertTrue(success, msg="ERROR calling GetInfrastructureInfo: " + str(vm_ids)) -# (success, info) = self.server.GetVMInfo(self.inf_id, vm_ids[1], self.auth_data) -# self.assertTrue(success, msg="ERROR calling GetVMInfo: " + str(info)) -# info_radl = radl_parse.parse_radl(info) -# prop_usage = info_radl.systems[0].getValue("cpu.usage") -# self.assertIsNotNone(prop_usage, msg="ERROR getting ganglia VM info (cpu.usage = None) of VM " + str(vm_ids[1])) - def test_18_error_addresource(self): """ Test to get error when adding a resource with an incorrect RADL @@ -447,5 +435,116 @@ def test_75_destroy(self): (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + def test_90_create_tosca(self): + """ + Test the CreateInfrastructure IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + """ + + (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) + self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) + self.__class__.inf_id = inf_id + + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) + self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") + + def test_95_destroy(self): + """ + Test DestroyInfrastructure function + """ + (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) + self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + + if __name__ == '__main__': unittest.main() diff --git a/test/TestIM.py b/test/TestIM.py index c9fde294b..1e11d06b6 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -520,5 +520,116 @@ def test_85_destroy(self): (success, res) = self.server.DestroyInfrastructure(inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + def test_90_create_tosca(self): + """ + Test the CreateInfrastructure IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + """ + + (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) + self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) + self.__class__.inf_id = inf_id + + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 600) + self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") + + def test_95_destroy(self): + """ + Test DestroyInfrastructure function + """ + (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) + self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) + if __name__ == '__main__': unittest.main() From 94227466b00e78d75f2b4fe494df1ddd96a0a305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Thu, 14 Jan 2016 17:26:19 +0100 Subject: [PATCH 093/509] Update Dockerfile Update python required libraries for testing --- docker-devel/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 8e1c47396..b95f410f9 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.4.0" +LABEL version="1.4.1" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages @@ -12,6 +12,8 @@ RUN apt-get update && apt-get install -y \ python-soappy \ python-pbr \ python-dateutil \ + python-mock \ + python-nose \ openssh-client \ sshpass \ git \ From 9c3d20dd3a70b8cd5c3dc622763535da460a57e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Thu, 14 Jan 2016 17:26:19 +0100 Subject: [PATCH 094/509] Update Dockerfile Update python required libraries for testing --- docker-devel/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 8e1c47396..b95f410f9 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.4.0" +LABEL version="1.4.1" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages @@ -12,6 +12,8 @@ RUN apt-get update && apt-get install -y \ python-soappy \ python-pbr \ python-dateutil \ + python-mock \ + python-nose \ openssh-client \ sshpass \ git \ From e366c84c70f20a1ae6b6fba0880282f970393a48 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 09:51:46 +0100 Subject: [PATCH 095/509] Update README docs to TOSCA version --- README | 59 +++++++++++++++++++----------------------- README.md | 77 ++++++++++++++++++++++++++----------------------------- 2 files changed, 62 insertions(+), 74 deletions(-) diff --git a/README b/README index af797df30..3f2f82134 100644 --- a/README +++ b/README @@ -41,6 +41,14 @@ However, if you install IM from sources you should install: * The SOAPpy library for Python, typically available as the 'python-soappy' or 'SOAPpy' package. * The Netaddr library for Python, typically available as the 'python-netaddr' package. + + * The boto library version 2.29 or later must be installed (http://boto.readthedocs.org/en/latest/). + + * The apache-libcloud library version 0.18 or later must be installed (http://libcloud.apache.org/). + + * The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at + https://github.com/indigo-dc/tosca-parser but we are working to improve the mainstream version + to enable to use it with the IM. * Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. @@ -67,12 +75,6 @@ However, if you install IM from sources you should install: 1.2 OPTIONAL PACKAGES --------------------- -In case of using the Amazon EC2 plugin the boto library version 2.29 or later -must be installed (http://boto.readthedocs.org/en/latest/). - -In case of using the LibCloud plugin the apache-libcloud library version 0.17 or later -must be installed (http://libcloud.apache.org/). - In case of using the SSL secured version of the XMLRPC API the SpringPython framework (http://springpython.webfactional.com/) must be installed. @@ -85,41 +87,32 @@ framework (http://www.cherrypy.org/) must be installed. 1.3 INSTALLING -------------- -1.3.1 FROM PIP --------------- - -**WARNING**: The SOAPpy distributed with pip does not work correctly so you must install -the packages 'python-soappy' or 'SOAPp'y before installing the IM with pip. - -**WARNING**: In some GNU/Linux distributions (RHEL 6 or equivalents) you must uninstall -the packages python-paramiko and python-crypto before installing the IM with pip. - -You only have to install the IM package through the pip tool. +First install the requirements: - $ pip install IM +On Debian Systems: -Pip will install all the pre-requisites needed. So Ansible 1.4.2 or later will -be installed in the system. Yo will also need to install the sshpass command -('sshpass' package in main distributions). In some cases it will need to have installed -the GCC compiler and the python developer libraries ('python-dev' or 'python-devel' -packages in main distributions). + $ apt-get -y install git python-setuptools python-dev gcc python-soappy python-pip python-pbr python-dateutil -You must also remember to modify the ansible.cfg file setting as specified in the -REQUISITES section. +On RedHat Systems: -1.3.2 FROM SOURCE ------------------ + $ yum remove python-paramiko python-crypto + $ yum -y install git python-setuptools python-devel gcc SOAPpy python-dateutil python-six python-requests + $ easy_install pip + $ pip install pbr -Select a proper path where the IM service will be installed (i.e. /usr/local/im, -/opt/im or other). This path will be called IM_PATH +Then install the TOSCA parser: - $ tar xvzf IM-X.XX.tar.gz - $ chown -R root:root IM-X.XX - $ mv IM-X.XX /usr/local + $ cd /tmp + $ git clone --recursive https://github.com/indigo-dc/tosca-parser.git + $ cd tosca-parser + $ python setup.py install -Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory. +Finally install the IM service: - $ ln -s /usr/local/im/scripts/im /etc/init.d/im + $ cd /tmp + $ git clone --recursive https://github.com/indigo-dc/im.git + $ cd im + $ python setup.py install 1.4 CONFIGURATION ----------------- diff --git a/README.md b/README.md index b220301a4..c93355a8a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - IM - Infrastructure Manager -============================ + IM - Infrastructure Manager (With TOSCA Support) +================================================= * Version ![PyPI](https://img.shields.io/pypi/v/im.svg) * PyPI ![PypI](https://img.shields.io/pypi/dm/IM.svg) @@ -29,9 +29,6 @@ of the functionality of the platform: [YouTube IM channel](https://www.youtube.c IM is based on Python, so Python 2.6 or higher runtime and standard library must be installed in the system. -If you use pip to install the IM, all the requisites will be installed. -However, if you install IM from sources you should install: - + The Python Lex & Yacc library (http://www.dabeaz.com/ply/), typically available as the 'python-ply' package. @@ -43,6 +40,16 @@ However, if you install IM from sources you should install: + The SOAPpy library for Python, typically available as the 'python-soappy' or 'SOAPpy' package. + The Netaddr library for Python, typically available as the 'python-netaddr' package. + + + The boto library version 2.29 or later + must be installed (http://boto.readthedocs.org/en/latest/). + + + The apache-libcloud library version 0.18 or later + must be installed (http://libcloud.apache.org/). + + + The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at + https://github.com/indigo-dc/tosca-parser but we are working to improve the mainstream version + to enable to use it with the IM. + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. @@ -71,12 +78,6 @@ pipelining = True 1.2 OPTIONAL PACKAGES --------------------- -In case of using the Amazon EC2 plugin the boto library version 2.29 or later -must be installed (http://boto.readthedocs.org/en/latest/). - -In case of using the LibCloud plugin the apache-libcloud library version 0.17 or later -must be installed (http://libcloud.apache.org/). - In case of using the SSL secured version of the XMLRPC API the SpringPython framework (http://springpython.webfactional.com/) must be installed. @@ -89,46 +90,40 @@ framework (http://www.cherrypy.org/) must be installed. 1.3 INSTALLING -------------- -### 1.3.1 FROM PIP - -**WARNING: The SOAPpy distributed with pip does not work correctly so you must install -the packages 'python-soappy' or 'SOAPp'y before installing the IM with pip.** - -**WARNING: In some GNU/Linux distributions (RHEL 6 or equivalents) you must uninstall -the packages 'python-paramiko' and 'python-crypto' before installing the IM with pip.** - -You only have to install the IM package through the pip tool. +First install the requirements: +On Debian Systems: ``` -pip install IM +$ apt-get -y install git python-setuptools python-dev gcc python-soappy python-pip python-pbr python-dateutil ``` -Pip will install all the pre-requisites needed. So Ansible 1.4.2 or later will - be installed in the system. Yo will also need to install the sshpass command - ('sshpass' package in main distributions). In some cases it will need to have installed - the GCC compiler and the python developer libraries ('python-dev' or 'python-devel' - packages in main distributions). - -You must also remember to modify the ansible.cfg file setting as specified in the -REQUISITES section. - -### 1.3.2 FROM SOURCE +On RedHat Systems: +``` +$ yum remove python-paramiko python-crypto +$ yum -y install git python-setuptools python-devel gcc SOAPpy python-dateutil python-six python-requests +$ easy_install pip +$ pip install pbr +``` -Select a proper path where the IM service will be installed (i.e. /usr/local/im, -/opt/im or other). This path will be called IM_PATH +Then install the TOSCA parser: ``` -$ tar xvzf IM-X.XX.tar.gz -$ chown -R root:root IM-X.XX -$ mv IM-X.XX /usr/local +$ cd /tmp +$ git clone --recursive https://github.com/indigo-dc/tosca-parser.git +$ cd tosca-parser +$ python setup.py install ``` -Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory. +Finally install the IM service: ``` -$ ln -s /usr/local/im/scripts/im /etc/init.d/im +$ cd /tmp +$ git clone --recursive https://github.com/indigo-dc/im.git +$ cd im +$ python setup.py install ``` + 1.4 CONFIGURATION ----------------- @@ -198,11 +193,11 @@ And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates path 2. DOCKER IMAGE =============== -A Docker image named `grycap/im` has been created to make easier the deployment of an IM service using the -default configuration. Information about this image can be found here: https://registry.hub.docker.com/u/grycap/im/. +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. How to launch the IM service using docker: ```sh -sudo docker run -d -p 8899:8899 --name im grycap/im +sudo docker run -d -p 8899:8899 --name im indigodatacloud/im ``` From c9be7bd5448b9a3aaa10fdefc5e96cce09df50a8 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 09:51:46 +0100 Subject: [PATCH 096/509] Update README docs to TOSCA version --- README | 59 +++++++++++++++++++----------------------- README.md | 77 ++++++++++++++++++++++++++----------------------------- 2 files changed, 62 insertions(+), 74 deletions(-) diff --git a/README b/README index af797df30..3f2f82134 100644 --- a/README +++ b/README @@ -41,6 +41,14 @@ However, if you install IM from sources you should install: * The SOAPpy library for Python, typically available as the 'python-soappy' or 'SOAPpy' package. * The Netaddr library for Python, typically available as the 'python-netaddr' package. + + * The boto library version 2.29 or later must be installed (http://boto.readthedocs.org/en/latest/). + + * The apache-libcloud library version 0.18 or later must be installed (http://libcloud.apache.org/). + + * The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at + https://github.com/indigo-dc/tosca-parser but we are working to improve the mainstream version + to enable to use it with the IM. * Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. @@ -67,12 +75,6 @@ However, if you install IM from sources you should install: 1.2 OPTIONAL PACKAGES --------------------- -In case of using the Amazon EC2 plugin the boto library version 2.29 or later -must be installed (http://boto.readthedocs.org/en/latest/). - -In case of using the LibCloud plugin the apache-libcloud library version 0.17 or later -must be installed (http://libcloud.apache.org/). - In case of using the SSL secured version of the XMLRPC API the SpringPython framework (http://springpython.webfactional.com/) must be installed. @@ -85,41 +87,32 @@ framework (http://www.cherrypy.org/) must be installed. 1.3 INSTALLING -------------- -1.3.1 FROM PIP --------------- - -**WARNING**: The SOAPpy distributed with pip does not work correctly so you must install -the packages 'python-soappy' or 'SOAPp'y before installing the IM with pip. - -**WARNING**: In some GNU/Linux distributions (RHEL 6 or equivalents) you must uninstall -the packages python-paramiko and python-crypto before installing the IM with pip. - -You only have to install the IM package through the pip tool. +First install the requirements: - $ pip install IM +On Debian Systems: -Pip will install all the pre-requisites needed. So Ansible 1.4.2 or later will -be installed in the system. Yo will also need to install the sshpass command -('sshpass' package in main distributions). In some cases it will need to have installed -the GCC compiler and the python developer libraries ('python-dev' or 'python-devel' -packages in main distributions). + $ apt-get -y install git python-setuptools python-dev gcc python-soappy python-pip python-pbr python-dateutil -You must also remember to modify the ansible.cfg file setting as specified in the -REQUISITES section. +On RedHat Systems: -1.3.2 FROM SOURCE ------------------ + $ yum remove python-paramiko python-crypto + $ yum -y install git python-setuptools python-devel gcc SOAPpy python-dateutil python-six python-requests + $ easy_install pip + $ pip install pbr -Select a proper path where the IM service will be installed (i.e. /usr/local/im, -/opt/im or other). This path will be called IM_PATH +Then install the TOSCA parser: - $ tar xvzf IM-X.XX.tar.gz - $ chown -R root:root IM-X.XX - $ mv IM-X.XX /usr/local + $ cd /tmp + $ git clone --recursive https://github.com/indigo-dc/tosca-parser.git + $ cd tosca-parser + $ python setup.py install -Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory. +Finally install the IM service: - $ ln -s /usr/local/im/scripts/im /etc/init.d/im + $ cd /tmp + $ git clone --recursive https://github.com/indigo-dc/im.git + $ cd im + $ python setup.py install 1.4 CONFIGURATION ----------------- diff --git a/README.md b/README.md index b220301a4..c93355a8a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - IM - Infrastructure Manager -============================ + IM - Infrastructure Manager (With TOSCA Support) +================================================= * Version ![PyPI](https://img.shields.io/pypi/v/im.svg) * PyPI ![PypI](https://img.shields.io/pypi/dm/IM.svg) @@ -29,9 +29,6 @@ of the functionality of the platform: [YouTube IM channel](https://www.youtube.c IM is based on Python, so Python 2.6 or higher runtime and standard library must be installed in the system. -If you use pip to install the IM, all the requisites will be installed. -However, if you install IM from sources you should install: - + The Python Lex & Yacc library (http://www.dabeaz.com/ply/), typically available as the 'python-ply' package. @@ -43,6 +40,16 @@ However, if you install IM from sources you should install: + The SOAPpy library for Python, typically available as the 'python-soappy' or 'SOAPpy' package. + The Netaddr library for Python, typically available as the 'python-netaddr' package. + + + The boto library version 2.29 or later + must be installed (http://boto.readthedocs.org/en/latest/). + + + The apache-libcloud library version 0.18 or later + must be installed (http://libcloud.apache.org/). + + + The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at + https://github.com/indigo-dc/tosca-parser but we are working to improve the mainstream version + to enable to use it with the IM. + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. @@ -71,12 +78,6 @@ pipelining = True 1.2 OPTIONAL PACKAGES --------------------- -In case of using the Amazon EC2 plugin the boto library version 2.29 or later -must be installed (http://boto.readthedocs.org/en/latest/). - -In case of using the LibCloud plugin the apache-libcloud library version 0.17 or later -must be installed (http://libcloud.apache.org/). - In case of using the SSL secured version of the XMLRPC API the SpringPython framework (http://springpython.webfactional.com/) must be installed. @@ -89,46 +90,40 @@ framework (http://www.cherrypy.org/) must be installed. 1.3 INSTALLING -------------- -### 1.3.1 FROM PIP - -**WARNING: The SOAPpy distributed with pip does not work correctly so you must install -the packages 'python-soappy' or 'SOAPp'y before installing the IM with pip.** - -**WARNING: In some GNU/Linux distributions (RHEL 6 or equivalents) you must uninstall -the packages 'python-paramiko' and 'python-crypto' before installing the IM with pip.** - -You only have to install the IM package through the pip tool. +First install the requirements: +On Debian Systems: ``` -pip install IM +$ apt-get -y install git python-setuptools python-dev gcc python-soappy python-pip python-pbr python-dateutil ``` -Pip will install all the pre-requisites needed. So Ansible 1.4.2 or later will - be installed in the system. Yo will also need to install the sshpass command - ('sshpass' package in main distributions). In some cases it will need to have installed - the GCC compiler and the python developer libraries ('python-dev' or 'python-devel' - packages in main distributions). - -You must also remember to modify the ansible.cfg file setting as specified in the -REQUISITES section. - -### 1.3.2 FROM SOURCE +On RedHat Systems: +``` +$ yum remove python-paramiko python-crypto +$ yum -y install git python-setuptools python-devel gcc SOAPpy python-dateutil python-six python-requests +$ easy_install pip +$ pip install pbr +``` -Select a proper path where the IM service will be installed (i.e. /usr/local/im, -/opt/im or other). This path will be called IM_PATH +Then install the TOSCA parser: ``` -$ tar xvzf IM-X.XX.tar.gz -$ chown -R root:root IM-X.XX -$ mv IM-X.XX /usr/local +$ cd /tmp +$ git clone --recursive https://github.com/indigo-dc/tosca-parser.git +$ cd tosca-parser +$ python setup.py install ``` -Finally you must copy (or link) $IM_PATH/scripts/im file to /etc/init.d directory. +Finally install the IM service: ``` -$ ln -s /usr/local/im/scripts/im /etc/init.d/im +$ cd /tmp +$ git clone --recursive https://github.com/indigo-dc/im.git +$ cd im +$ python setup.py install ``` + 1.4 CONFIGURATION ----------------- @@ -198,11 +193,11 @@ And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates path 2. DOCKER IMAGE =============== -A Docker image named `grycap/im` has been created to make easier the deployment of an IM service using the -default configuration. Information about this image can be found here: https://registry.hub.docker.com/u/grycap/im/. +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. How to launch the IM service using docker: ```sh -sudo docker run -d -p 8899:8899 --name im grycap/im +sudo docker run -d -p 8899:8899 --name im indigodatacloud/im ``` From a438165009e90d8c4da2448b836c1ce6cec0631a Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 09:52:11 +0100 Subject: [PATCH 097/509] Increase timeout time in TOSCA test --- test/TestIM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TestIM.py b/test/TestIM.py index 1e11d06b6..e5c7c582f 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -621,7 +621,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 600) + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") def test_95_destroy(self): From 893a7f0f7ba694cf352d943982b335cec675daa2 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 09:52:11 +0100 Subject: [PATCH 098/509] Increase timeout time in TOSCA test --- test/TestIM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TestIM.py b/test/TestIM.py index 1e11d06b6..e5c7c582f 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -621,7 +621,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 600) + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") def test_95_destroy(self): From 47354a4a266550e058f31f5b2451671f97dc7280 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 10:09:01 +0100 Subject: [PATCH 099/509] Update README docs to set the new YouTube links --- README | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README b/README index 3f2f82134..1069323a6 100644 --- a/README +++ b/README @@ -14,8 +14,8 @@ infrastructure. Read the documentation and more at http://www.grycap.upv.es/im. -There is also an Infrastructure Manager youtube channel with a set of videos with demos -of the functionality of the platform: https://www.youtube.com/channel/UCF16QmMHlRNtsC-0Cb2d8fg. +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. 1. INSTALLATION diff --git a/README.md b/README.md index c93355a8a..99f425696 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ infrastructure. Read the documentation and more at http://www.grycap.upv.es/im. -There is also an Infrastructure Manager youtube channel with a set of videos with demos -of the functionality of the platform: [YouTube IM channel](https://www.youtube.com/channel/UCF16QmMHlRNtsC-0Cb2d8fg) +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. 1. INSTALLATION From 5040e08165e63a312885fd3e69bc96b168727a24 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 10:09:01 +0100 Subject: [PATCH 100/509] Update README docs to set the new YouTube links --- README | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README b/README index 3f2f82134..1069323a6 100644 --- a/README +++ b/README @@ -14,8 +14,8 @@ infrastructure. Read the documentation and more at http://www.grycap.upv.es/im. -There is also an Infrastructure Manager youtube channel with a set of videos with demos -of the functionality of the platform: https://www.youtube.com/channel/UCF16QmMHlRNtsC-0Cb2d8fg. +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. 1. INSTALLATION diff --git a/README.md b/README.md index c93355a8a..99f425696 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ infrastructure. Read the documentation and more at http://www.grycap.upv.es/im. -There is also an Infrastructure Manager youtube channel with a set of videos with demos -of the functionality of the platform: [YouTube IM channel](https://www.youtube.com/channel/UCF16QmMHlRNtsC-0Cb2d8fg) +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. 1. INSTALLATION From e400a89749356a13acf050cffe86ef9abd381e3e Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 10:54:18 +0100 Subject: [PATCH 101/509] Increase timeout time in TOSCA test --- test/QuickTestIM.py | 2 +- test/TestIM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 33d311217..34519e4ae 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -535,7 +535,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 1200) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") def test_95_destroy(self): diff --git a/test/TestIM.py b/test/TestIM.py index e5c7c582f..adb03367d 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -621,7 +621,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 1200) self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") def test_95_destroy(self): From 63bc96460c7a038b633e244f5e480374e16159e9 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 10:54:18 +0100 Subject: [PATCH 102/509] Increase timeout time in TOSCA test --- test/QuickTestIM.py | 2 +- test/TestIM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 33d311217..34519e4ae 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -535,7 +535,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 1200) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") def test_95_destroy(self): diff --git a/test/TestIM.py b/test/TestIM.py index e5c7c582f..adb03367d 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -621,7 +621,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 1200) self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") def test_95_destroy(self): From 97de28f77488ff7d240a395ffff190d980c03cb1 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 12:40:14 +0100 Subject: [PATCH 103/509] Derease timeout time in TOSCA test --- test/QuickTestIM.py | 2 +- test/TestIM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 34519e4ae..33d311217 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -535,7 +535,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 1200) + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") def test_95_destroy(self): diff --git a/test/TestIM.py b/test/TestIM.py index adb03367d..e5c7c582f 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -621,7 +621,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 1200) + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") def test_95_destroy(self): From 2a5c036d75b89ceac613e65e91b487154c33c5bb Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 12:40:14 +0100 Subject: [PATCH 104/509] Derease timeout time in TOSCA test --- test/QuickTestIM.py | 2 +- test/TestIM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 34519e4ae..33d311217 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -535,7 +535,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 1200) + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") def test_95_destroy(self): diff --git a/test/TestIM.py b/test/TestIM.py index adb03367d..e5c7c582f 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -621,7 +621,7 @@ def test_90_create_tosca(self): self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) self.__class__.inf_id = inf_id - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 1200) + all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") def test_95_destroy(self): From 13276e78dbdf719600ed059aa09c5a8680746ecd Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 13:29:44 +0100 Subject: [PATCH 105/509] Bugfix in tosca test --- test/QuickTestIM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 33d311217..33e68dd07 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -505,7 +505,7 @@ def test_90_create_tosca(self): inputs: db_name: { get_property: [ SELF, name ] } db_data: { get_artifact: [ SELF, db_content ] } - db_name: { get_property: [ SELF, name ] } + db_password: { get_property: [ SELF, password ] } db_user: { get_property: [ SELF, user ] } mysql: From efdf7d63a4f9c1c6b999a830c14d263360a5692d Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jan 2016 13:29:44 +0100 Subject: [PATCH 106/509] Bugfix in tosca test --- test/QuickTestIM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 33d311217..33e68dd07 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -505,7 +505,7 @@ def test_90_create_tosca(self): inputs: db_name: { get_property: [ SELF, name ] } db_data: { get_artifact: [ SELF, db_content ] } - db_name: { get_property: [ SELF, name ] } + db_password: { get_property: [ SELF, password ] } db_user: { get_property: [ SELF, user ] } mysql: From 910aa998fe88792b6b749cf2120da7d7b441fda7 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 18 Jan 2016 09:13:32 +0100 Subject: [PATCH 107/509] Minor changes --- IM/tosca/Tosca.py | 128 ++++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 5324e4544..bbcb963c6 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -122,7 +122,7 @@ def to_radl(self): # Add the system RADL element sys = Tosca._gen_system(node, self.tosca.nodetemplates) # add networks using the simple method with the public_ip property - Tosca._add_node_nets(node, radl, sys) + Tosca._add_node_nets(node, radl, sys, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system count, min_instances, _, default_instances = Tosca._get_scalable_properties(node) @@ -160,67 +160,80 @@ def to_radl(self): return self._complete_radl_networks(radl) @staticmethod - def _add_node_nets(node, radl, system): - public_ip = False - node_props = node.get_properties_objects() - if node_props: - for prop in node_props: - if prop.name == "public_ip": - public_ip = prop.value - break + def _add_node_nets(node, radl, system, nodetemplates): - # If the node needs a public IP - if public_ip: - public_nets = [] + # Find associated Networks + nets = Tosca._get_bind_networks(node, nodetemplates) + if nets: + # If there are network nodes, use it to define system network properties + for net_name, ip, dns_name, num in nets: + system.setValue('net_interface.%d.connection' % num, net_name) + # This is not a normative property + if dns_name: + system.setValue('net_interface.%d.dns_name' % num, dns_name) + if ip: + system.setValue('net_interface.%d.ip' % num, ip) + else: + public_ip = False + node_props = node.get_properties_objects() + if node_props: + for prop in node_props: + if prop.name == "public_ip": + public_ip = prop.value + break + + # If the node needs a public IP + if public_ip: + public_nets = [] + for net in radl.networks: + if net.isPublic(): + public_nets.append(net) + + if public_nets: + public_net = None + for net in public_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + public_net = net + break + + if not public_net: + # There are a public net but it has not been used in this VM + public_net = public_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + public_net = network.createNetwork("public_net", True) + radl.networks.append(public_net) + num_net = system.getNumNetworkIfaces() + + system.setValue('net_interface.' + str(num_net) + '.connection',public_net.id) + + # The private net is always added + private_nets = [] for net in radl.networks: - if net.isPublic(): - public_nets.append(net) - - if public_nets: - public_net = None - for net in public_nets: + if not net.isPublic(): + private_nets.append(net) + + if private_nets: + private_net = None + for net in private_nets: num_net = system.getNumNetworkWithConnection(net.id) if num_net is not None: - public_net = net + private_net = net break - - if not public_net: + + if not private_net: # There are a public net but it has not been used in this VM - public_net = public_nets[0] + private_net = private_nets[0] num_net = system.getNumNetworkIfaces() else: # There no public net, create one - public_net = network.createNetwork("public_net", True) - radl.networks.append(public_net) + private_net = network.createNetwork("private_net", False) + radl.networks.append(private_net) num_net = system.getNumNetworkIfaces() - - system.setValue('net_interface.' + str(num_net) + '.connection',public_net.id) - - # The private net is allways added - private_nets = [] - for net in radl.networks: - if not net.isPublic(): - private_nets.append(net) - - if private_nets: - private_net = None - for net in private_nets: - num_net = system.getNumNetworkWithConnection(net.id) - if num_net is not None: - private_net = net - break - - if not private_net: - # There are a public net but it has not been used in this VM - private_net = private_nets[0] - num_net = system.getNumNetworkIfaces() - else: - # There no public net, create one - private_net = network.createNetwork("private_net", False) - radl.networks.append(private_net) - num_net = system.getNumNetworkIfaces() - - system.setValue('net_interface.' + str(num_net) + '.connection',private_net.id) + + system.setValue('net_interface.' + str(num_net) + '.connection',private_net.id) @staticmethod @@ -815,16 +828,7 @@ def _gen_system(node, nodetemplates): if location: res.setValue('disk.%d.mount_path' % num, location) res.setValue('disk.%d.fstype' % num, "ext4") - - # Find associated Networks - nets = Tosca._get_bind_networks(node, nodetemplates) - for net_name, ip, dns_name, num in nets: - res.setValue('net_interface.%d.connection' % num, net_name) - if dns_name: - res.setValue('net_interface.%d.dns_name' % num, dns_name) - if ip: - res.setValue('net_interface.%d.ip' % num, ip) - + return res @staticmethod From bdecd6cad7b3ef6877d10eebcc13aeb5f6050e3d Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 18 Jan 2016 09:13:32 +0100 Subject: [PATCH 108/509] Minor changes --- IM/tosca/Tosca.py | 128 ++++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 5324e4544..bbcb963c6 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -122,7 +122,7 @@ def to_radl(self): # Add the system RADL element sys = Tosca._gen_system(node, self.tosca.nodetemplates) # add networks using the simple method with the public_ip property - Tosca._add_node_nets(node, radl, sys) + Tosca._add_node_nets(node, radl, sys, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system count, min_instances, _, default_instances = Tosca._get_scalable_properties(node) @@ -160,67 +160,80 @@ def to_radl(self): return self._complete_radl_networks(radl) @staticmethod - def _add_node_nets(node, radl, system): - public_ip = False - node_props = node.get_properties_objects() - if node_props: - for prop in node_props: - if prop.name == "public_ip": - public_ip = prop.value - break + def _add_node_nets(node, radl, system, nodetemplates): - # If the node needs a public IP - if public_ip: - public_nets = [] + # Find associated Networks + nets = Tosca._get_bind_networks(node, nodetemplates) + if nets: + # If there are network nodes, use it to define system network properties + for net_name, ip, dns_name, num in nets: + system.setValue('net_interface.%d.connection' % num, net_name) + # This is not a normative property + if dns_name: + system.setValue('net_interface.%d.dns_name' % num, dns_name) + if ip: + system.setValue('net_interface.%d.ip' % num, ip) + else: + public_ip = False + node_props = node.get_properties_objects() + if node_props: + for prop in node_props: + if prop.name == "public_ip": + public_ip = prop.value + break + + # If the node needs a public IP + if public_ip: + public_nets = [] + for net in radl.networks: + if net.isPublic(): + public_nets.append(net) + + if public_nets: + public_net = None + for net in public_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + public_net = net + break + + if not public_net: + # There are a public net but it has not been used in this VM + public_net = public_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + public_net = network.createNetwork("public_net", True) + radl.networks.append(public_net) + num_net = system.getNumNetworkIfaces() + + system.setValue('net_interface.' + str(num_net) + '.connection',public_net.id) + + # The private net is always added + private_nets = [] for net in radl.networks: - if net.isPublic(): - public_nets.append(net) - - if public_nets: - public_net = None - for net in public_nets: + if not net.isPublic(): + private_nets.append(net) + + if private_nets: + private_net = None + for net in private_nets: num_net = system.getNumNetworkWithConnection(net.id) if num_net is not None: - public_net = net + private_net = net break - - if not public_net: + + if not private_net: # There are a public net but it has not been used in this VM - public_net = public_nets[0] + private_net = private_nets[0] num_net = system.getNumNetworkIfaces() else: # There no public net, create one - public_net = network.createNetwork("public_net", True) - radl.networks.append(public_net) + private_net = network.createNetwork("private_net", False) + radl.networks.append(private_net) num_net = system.getNumNetworkIfaces() - - system.setValue('net_interface.' + str(num_net) + '.connection',public_net.id) - - # The private net is allways added - private_nets = [] - for net in radl.networks: - if not net.isPublic(): - private_nets.append(net) - - if private_nets: - private_net = None - for net in private_nets: - num_net = system.getNumNetworkWithConnection(net.id) - if num_net is not None: - private_net = net - break - - if not private_net: - # There are a public net but it has not been used in this VM - private_net = private_nets[0] - num_net = system.getNumNetworkIfaces() - else: - # There no public net, create one - private_net = network.createNetwork("private_net", False) - radl.networks.append(private_net) - num_net = system.getNumNetworkIfaces() - - system.setValue('net_interface.' + str(num_net) + '.connection',private_net.id) + + system.setValue('net_interface.' + str(num_net) + '.connection',private_net.id) @staticmethod @@ -815,16 +828,7 @@ def _gen_system(node, nodetemplates): if location: res.setValue('disk.%d.mount_path' % num, location) res.setValue('disk.%d.fstype' % num, "ext4") - - # Find associated Networks - nets = Tosca._get_bind_networks(node, nodetemplates) - for net_name, ip, dns_name, num in nets: - res.setValue('net_interface.%d.connection' % num, net_name) - if dns_name: - res.setValue('net_interface.%d.dns_name' % num, dns_name) - if ip: - res.setValue('net_interface.%d.ip' % num, ip) - + return res @staticmethod From 0b008f0ceeac3af1265994514f2e8510db9fcbb2 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 18 Jan 2016 11:10:42 +0100 Subject: [PATCH 109/509] Update tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index d1ad9a7ef..bb5f15069 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit d1ad9a7efe30ece97a6e821938d0335e6b88736a +Subproject commit bb5f150693e9297f47a3e6aab8839dd0ad33db0c From bf9ce074f1fb38eb004b6b874ad7703efe2a9e1f Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 18 Jan 2016 11:10:42 +0100 Subject: [PATCH 110/509] Update tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index d1ad9a7ef..bb5f15069 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit d1ad9a7efe30ece97a6e821938d0335e6b88736a +Subproject commit bb5f150693e9297f47a3e6aab8839dd0ad33db0c From 49eb33165fe017c16f7113bf214f1e612cafc0f1 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 19 Jan 2016 16:40:56 +0100 Subject: [PATCH 111/509] Bugfixes and doc update --- IM/InfrastructureManager.py | 18 +----- IM/REST.py | 5 ++ IM/radl/__init__.py | 2 +- IM/tosca/Tosca.py | 2 +- IM/tosca/tosca-types | 2 +- doc/source/REST.rst | 4 +- etc/im.cfg | 2 +- test/QuickTestIM.py | 112 ----------------------------------- test/TestIM.py | 111 ----------------------------------- test/TestREST.py | 113 ++++++++++++++++++++++++++++++++++++ 10 files changed, 126 insertions(+), 245 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 840f41ced..320f5c13e 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -34,7 +34,6 @@ from IM.radl.radl import Feature, RADL from IM.recipe import Recipe from IM.db import DataBase -from IM.tosca.Tosca import Tosca from config import Config from IM.VirtualMachine import VirtualMachine @@ -359,24 +358,11 @@ def AddResource(inf_id, radl_data, auth, context = True, failed_clouds = []): """ InfrastructureManager.logger.info("Adding resources to inf: " + str(inf_id)) - + if isinstance(radl_data, RADL): radl = radl_data else: - # TODO: Think about CSAR files using xmlrpclib.Binary o enconding a file using b64 - # see: http://stackoverflow.com/questions/9099174/send-file-from-client-to-server-using-xmlrpc - # We must save the file, unzip it and get the file pointed by: Entry-Definitions: some.yaml - # http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/csd03/TOSCA-Simple-Profile-YAML-v1.0-csd03.html#_Toc419746172 - if Tosca.is_tosca(radl_data): - try: - tosca = Tosca(radl_data) - radl = tosca.to_radl() - except Exception, ex: - InfrastructureManager.logger.exception("Error parsing TOSCA input data.") - raise Exception("Error parsing TOSCA input data: " + str(ex)) - else: - radl = radl_parse.parse_radl(radl_data) - + radl = radl_parse.parse_radl(radl_data) InfrastructureManager.logger.debug(radl) radl.check() diff --git a/IM/REST.py b/IM/REST.py index 7ab3f86a8..2e8ef19a5 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -23,6 +23,7 @@ from config import Config from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json from radl.radl_parse import parse_radl +from IM.tosca.Tosca import Tosca AUTH_LINE_SEPARATOR = '\\n' @@ -218,6 +219,8 @@ def RESTCreateInfrastructure(): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) + elif content_type == "text/yaml": + radl_data = Tosca(radl_data).to_radl() elif content_type != "text/plain": bottle.abort(415, "Unsupported Media Type %s" % content_type) return False @@ -333,6 +336,8 @@ def RESTAddResource(id=None): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) + elif content_type == "text/yaml": + radl_data = Tosca(radl_data).to_radl() elif content_type != "text/plain": bottle.abort(415, "Unsupported Media Type %s" % content_type) return False diff --git a/IM/radl/__init__.py b/IM/radl/__init__.py index dc055b705..ba678fe48 100644 --- a/IM/radl/__init__.py +++ b/IM/radl/__init__.py @@ -15,4 +15,4 @@ # along with this program. If not, see . -__all__ = ['radl_lex','radl_parse','radl'] +__all__ = ['radl_lex','radl_parse', 'radl_json','radl'] diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bbcb963c6..cc942cc6d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -64,7 +64,7 @@ class Tosca: """ - ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/artifacts" ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" logger = logging.getLogger('InfrastructureManager') diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index bb5f15069..9c239878f 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit bb5f150693e9297f47a3e6aab8839dd0ad33db0c +Subproject commit 9c239878fca9561cdfa11f16cd1033948fd20444 diff --git a/doc/source/REST.rst b/doc/source/REST.rst index 9443a675a..771c8c1bc 100644 --- a/doc/source/REST.rst +++ b/doc/source/REST.rst @@ -62,7 +62,7 @@ GET ``http://imserver.com/infrastructures`` POST ``http://imserver.com/infrastructures`` :body: ``RADL document`` - :body Content-type: text/plain or application/json + :body Content-type: text/plain or application/json or text/yaml :Response Content-type: text/uri-list :ok response: 200 OK :fail response: 401, 400, 415 @@ -93,7 +93,7 @@ GET ``http://imserver.com/infrastructures//`` POST ``http://imserver.com/infrastructures/`` :body: ``RADL document`` - :body Content-type: text/plain or application/json + :body Content-type: text/plain or application/json or text/yaml :input fields: ``context`` (optional) :Response Content-type: text/uri-list :ok response: 200 OK diff --git a/etc/im.cfg b/etc/im.cfg index f7fe593c0..798f2f698 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -60,7 +60,7 @@ DEFAULT_VM_NAME = vnode-#N# DEFAULT_DOMAIN = localdomain # REST API Info -ACTIVATE_REST = False +ACTIVATE_REST = True REST_PORT = 8800 REST_ADDRESS = 0.0.0.0 diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 33e68dd07..97cdc108b 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -420,7 +420,6 @@ def test_70_create_cloud_init(self): ) """ - a = radl_parse.parse_radl(radl) (success, inf_id) = self.server.CreateInfrastructure(radl, self.auth_data) self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) self.__class__.inf_id = inf_id @@ -435,116 +434,5 @@ def test_75_destroy(self): (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - def test_90_create_tosca(self): - """ - Test the CreateInfrastructure IM function with a TOSCA document - """ - tosca = """ -tosca_definitions_version: tosca_simple_yaml_1_0 - -description: TOSCA test for the IM - - -topology_template: - inputs: - db_name: - type: string - default: world - db_user: - type: string - default: dbuser - db_password: - type: string - default: pass - mysql_root_password: - type: string - default: mypass - - node_templates: - - apache: - type: tosca.nodes.WebServer.Apache - requirements: - - host: web_server - - web_server: - type: tosca.nodes.indigo.Compute - properties: - public_ip: yes - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - # Guest Operating System properties - os: - properties: - # host Operating System image properties - type: linux - distribution: ubuntu - - test_db: - type: tosca.nodes.Database.MySQL - properties: - name: { get_input: db_name } - user: { get_input: db_user } - password: { get_input: db_password } - root_password: { get_input: mysql_root_password } - artifacts: - db_content: - file: http://downloads.mysql.com/docs/world.sql.gz - type: tosca.artifacts.File - requirements: - - host: - node: mysql - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_import.yml - inputs: - db_name: { get_property: [ SELF, name ] } - db_data: { get_artifact: [ SELF, db_content ] } - db_password: { get_property: [ SELF, password ] } - db_user: { get_property: [ SELF, user ] } - - mysql: - type: tosca.nodes.DBMS.MySQL - properties: - root_password: { get_input: mysql_root_password } - requirements: - - host: - node: db_server - - db_server: - type: tosca.nodes.Compute - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - os: - properties: - architecture: x86_64 - type: linux - distribution: ubuntu - """ - - (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) - self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) - self.__class__.inf_id = inf_id - - all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) - self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - - def test_95_destroy(self): - """ - Test DestroyInfrastructure function - """ - (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) - self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - - if __name__ == '__main__': unittest.main() diff --git a/test/TestIM.py b/test/TestIM.py index e5c7c582f..c9fde294b 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -520,116 +520,5 @@ def test_85_destroy(self): (success, res) = self.server.DestroyInfrastructure(inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - def test_90_create_tosca(self): - """ - Test the CreateInfrastructure IM function with a TOSCA document - """ - tosca = """ -tosca_definitions_version: tosca_simple_yaml_1_0 - -description: TOSCA test for the IM - - -topology_template: - inputs: - db_name: - type: string - default: world - db_user: - type: string - default: dbuser - db_password: - type: string - default: pass - mysql_root_password: - type: string - default: mypass - - node_templates: - - apache: - type: tosca.nodes.WebServer.Apache - requirements: - - host: web_server - - web_server: - type: tosca.nodes.indigo.Compute - properties: - public_ip: yes - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - # Guest Operating System properties - os: - properties: - # host Operating System image properties - type: linux - distribution: ubuntu - - test_db: - type: tosca.nodes.Database.MySQL - properties: - name: { get_input: db_name } - user: { get_input: db_user } - password: { get_input: db_password } - root_password: { get_input: mysql_root_password } - artifacts: - db_content: - file: http://downloads.mysql.com/docs/world.sql.gz - type: tosca.artifacts.File - requirements: - - host: - node: mysql - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_import.yml - inputs: - db_name: { get_property: [ SELF, name ] } - db_data: { get_artifact: [ SELF, db_content ] } - db_name: { get_property: [ SELF, name ] } - db_user: { get_property: [ SELF, user ] } - - mysql: - type: tosca.nodes.DBMS.MySQL - properties: - root_password: { get_input: mysql_root_password } - requirements: - - host: - node: db_server - - db_server: - type: tosca.nodes.Compute - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - disk_size: 10 GB - mem_size: 4 GB - os: - properties: - architecture: x86_64 - type: linux - distribution: ubuntu - """ - - (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) - self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) - self.__class__.inf_id = inf_id - - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) - self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") - - def test_95_destroy(self): - """ - Test DestroyInfrastructure function - """ - (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) - self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - if __name__ == '__main__': unittest.main() diff --git a/test/TestREST.py b/test/TestREST.py index ea06f8259..5a9909667 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -337,6 +337,119 @@ def test_95_destroy(self): resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR destroying the infrastructure:" + output) + + def test_96_create_tosca(self): + """ + Test the CreateInfrastructure IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + """ + + self.server.request('POST', "/infrastructures", body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR creating the infrastructure:" + output) + + self.__class__.inf_id = str(os.path.basename(output)) + + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) + self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") + + def test_97_destroy(self): + self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR destroying the infrastructure:" + output) if __name__ == '__main__': unittest.main() From 7493810f2fb86796f45bf65dd3a8add61d564633 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 19 Jan 2016 16:40:56 +0100 Subject: [PATCH 112/509] Bugfixes and doc update --- IM/InfrastructureManager.py | 18 +----- IM/REST.py | 5 ++ IM/radl/__init__.py | 2 +- IM/tosca/Tosca.py | 2 +- IM/tosca/tosca-types | 2 +- doc/source/REST.rst | 4 +- etc/im.cfg | 2 +- test/QuickTestIM.py | 112 ----------------------------------- test/TestIM.py | 111 ----------------------------------- test/TestREST.py | 113 ++++++++++++++++++++++++++++++++++++ 10 files changed, 126 insertions(+), 245 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 840f41ced..320f5c13e 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -34,7 +34,6 @@ from IM.radl.radl import Feature, RADL from IM.recipe import Recipe from IM.db import DataBase -from IM.tosca.Tosca import Tosca from config import Config from IM.VirtualMachine import VirtualMachine @@ -359,24 +358,11 @@ def AddResource(inf_id, radl_data, auth, context = True, failed_clouds = []): """ InfrastructureManager.logger.info("Adding resources to inf: " + str(inf_id)) - + if isinstance(radl_data, RADL): radl = radl_data else: - # TODO: Think about CSAR files using xmlrpclib.Binary o enconding a file using b64 - # see: http://stackoverflow.com/questions/9099174/send-file-from-client-to-server-using-xmlrpc - # We must save the file, unzip it and get the file pointed by: Entry-Definitions: some.yaml - # http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/csd03/TOSCA-Simple-Profile-YAML-v1.0-csd03.html#_Toc419746172 - if Tosca.is_tosca(radl_data): - try: - tosca = Tosca(radl_data) - radl = tosca.to_radl() - except Exception, ex: - InfrastructureManager.logger.exception("Error parsing TOSCA input data.") - raise Exception("Error parsing TOSCA input data: " + str(ex)) - else: - radl = radl_parse.parse_radl(radl_data) - + radl = radl_parse.parse_radl(radl_data) InfrastructureManager.logger.debug(radl) radl.check() diff --git a/IM/REST.py b/IM/REST.py index 7ab3f86a8..2e8ef19a5 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -23,6 +23,7 @@ from config import Config from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json from radl.radl_parse import parse_radl +from IM.tosca.Tosca import Tosca AUTH_LINE_SEPARATOR = '\\n' @@ -218,6 +219,8 @@ def RESTCreateInfrastructure(): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) + elif content_type == "text/yaml": + radl_data = Tosca(radl_data).to_radl() elif content_type != "text/plain": bottle.abort(415, "Unsupported Media Type %s" % content_type) return False @@ -333,6 +336,8 @@ def RESTAddResource(id=None): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) + elif content_type == "text/yaml": + radl_data = Tosca(radl_data).to_radl() elif content_type != "text/plain": bottle.abort(415, "Unsupported Media Type %s" % content_type) return False diff --git a/IM/radl/__init__.py b/IM/radl/__init__.py index dc055b705..ba678fe48 100644 --- a/IM/radl/__init__.py +++ b/IM/radl/__init__.py @@ -15,4 +15,4 @@ # along with this program. If not, see . -__all__ = ['radl_lex','radl_parse','radl'] +__all__ = ['radl_lex','radl_parse', 'radl_json','radl'] diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bbcb963c6..cc942cc6d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -64,7 +64,7 @@ class Tosca: """ - ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/artifacts" + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/artifacts" ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" logger = logging.getLogger('InfrastructureManager') diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index bb5f15069..9c239878f 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit bb5f150693e9297f47a3e6aab8839dd0ad33db0c +Subproject commit 9c239878fca9561cdfa11f16cd1033948fd20444 diff --git a/doc/source/REST.rst b/doc/source/REST.rst index 9443a675a..771c8c1bc 100644 --- a/doc/source/REST.rst +++ b/doc/source/REST.rst @@ -62,7 +62,7 @@ GET ``http://imserver.com/infrastructures`` POST ``http://imserver.com/infrastructures`` :body: ``RADL document`` - :body Content-type: text/plain or application/json + :body Content-type: text/plain or application/json or text/yaml :Response Content-type: text/uri-list :ok response: 200 OK :fail response: 401, 400, 415 @@ -93,7 +93,7 @@ GET ``http://imserver.com/infrastructures//`` POST ``http://imserver.com/infrastructures/`` :body: ``RADL document`` - :body Content-type: text/plain or application/json + :body Content-type: text/plain or application/json or text/yaml :input fields: ``context`` (optional) :Response Content-type: text/uri-list :ok response: 200 OK diff --git a/etc/im.cfg b/etc/im.cfg index f7fe593c0..798f2f698 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -60,7 +60,7 @@ DEFAULT_VM_NAME = vnode-#N# DEFAULT_DOMAIN = localdomain # REST API Info -ACTIVATE_REST = False +ACTIVATE_REST = True REST_PORT = 8800 REST_ADDRESS = 0.0.0.0 diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 33e68dd07..97cdc108b 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -420,7 +420,6 @@ def test_70_create_cloud_init(self): ) """ - a = radl_parse.parse_radl(radl) (success, inf_id) = self.server.CreateInfrastructure(radl, self.auth_data) self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) self.__class__.inf_id = inf_id @@ -435,116 +434,5 @@ def test_75_destroy(self): (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - def test_90_create_tosca(self): - """ - Test the CreateInfrastructure IM function with a TOSCA document - """ - tosca = """ -tosca_definitions_version: tosca_simple_yaml_1_0 - -description: TOSCA test for the IM - - -topology_template: - inputs: - db_name: - type: string - default: world - db_user: - type: string - default: dbuser - db_password: - type: string - default: pass - mysql_root_password: - type: string - default: mypass - - node_templates: - - apache: - type: tosca.nodes.WebServer.Apache - requirements: - - host: web_server - - web_server: - type: tosca.nodes.indigo.Compute - properties: - public_ip: yes - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - # Guest Operating System properties - os: - properties: - # host Operating System image properties - type: linux - distribution: ubuntu - - test_db: - type: tosca.nodes.Database.MySQL - properties: - name: { get_input: db_name } - user: { get_input: db_user } - password: { get_input: db_password } - root_password: { get_input: mysql_root_password } - artifacts: - db_content: - file: http://downloads.mysql.com/docs/world.sql.gz - type: tosca.artifacts.File - requirements: - - host: - node: mysql - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_import.yml - inputs: - db_name: { get_property: [ SELF, name ] } - db_data: { get_artifact: [ SELF, db_content ] } - db_password: { get_property: [ SELF, password ] } - db_user: { get_property: [ SELF, user ] } - - mysql: - type: tosca.nodes.DBMS.MySQL - properties: - root_password: { get_input: mysql_root_password } - requirements: - - host: - node: db_server - - db_server: - type: tosca.nodes.Compute - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - os: - properties: - architecture: x86_64 - type: linux - distribution: ubuntu - """ - - (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) - self.assertTrue(success, msg="ERROR calling CreateInfrastructure: " + str(inf_id)) - self.__class__.inf_id = inf_id - - all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 900) - self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - - def test_95_destroy(self): - """ - Test DestroyInfrastructure function - """ - (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) - self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - - if __name__ == '__main__': unittest.main() diff --git a/test/TestIM.py b/test/TestIM.py index e5c7c582f..c9fde294b 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -520,116 +520,5 @@ def test_85_destroy(self): (success, res) = self.server.DestroyInfrastructure(inf_id, self.auth_data) self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - def test_90_create_tosca(self): - """ - Test the CreateInfrastructure IM function with a TOSCA document - """ - tosca = """ -tosca_definitions_version: tosca_simple_yaml_1_0 - -description: TOSCA test for the IM - - -topology_template: - inputs: - db_name: - type: string - default: world - db_user: - type: string - default: dbuser - db_password: - type: string - default: pass - mysql_root_password: - type: string - default: mypass - - node_templates: - - apache: - type: tosca.nodes.WebServer.Apache - requirements: - - host: web_server - - web_server: - type: tosca.nodes.indigo.Compute - properties: - public_ip: yes - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - # Guest Operating System properties - os: - properties: - # host Operating System image properties - type: linux - distribution: ubuntu - - test_db: - type: tosca.nodes.Database.MySQL - properties: - name: { get_input: db_name } - user: { get_input: db_user } - password: { get_input: db_password } - root_password: { get_input: mysql_root_password } - artifacts: - db_content: - file: http://downloads.mysql.com/docs/world.sql.gz - type: tosca.artifacts.File - requirements: - - host: - node: mysql - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_import.yml - inputs: - db_name: { get_property: [ SELF, name ] } - db_data: { get_artifact: [ SELF, db_content ] } - db_name: { get_property: [ SELF, name ] } - db_user: { get_property: [ SELF, user ] } - - mysql: - type: tosca.nodes.DBMS.MySQL - properties: - root_password: { get_input: mysql_root_password } - requirements: - - host: - node: db_server - - db_server: - type: tosca.nodes.Compute - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - disk_size: 10 GB - mem_size: 4 GB - os: - properties: - architecture: x86_64 - type: linux - distribution: ubuntu - """ - - (success, inf_id) = self.server.CreateInfrastructure(tosca, self.auth_data) - self.assertTrue(success, msg="ERROR calling CreateInfrastructure to create the VM: " + str(inf_id)) - self.__class__.inf_id = inf_id - - all_configured = self.wait_inf_state(inf_id, VirtualMachine.CONFIGURED, 900) - self.assertTrue(all_configured, msg="ERROR waiting the VM to be configured (timeout).") - - def test_95_destroy(self): - """ - Test DestroyInfrastructure function - """ - (success, res) = self.server.DestroyInfrastructure(self.inf_id, self.auth_data) - self.assertTrue(success, msg="ERROR calling DestroyInfrastructure: " + str(res)) - if __name__ == '__main__': unittest.main() diff --git a/test/TestREST.py b/test/TestREST.py index ea06f8259..5a9909667 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -337,6 +337,119 @@ def test_95_destroy(self): resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR destroying the infrastructure:" + output) + + def test_96_create_tosca(self): + """ + Test the CreateInfrastructure IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + """ + + self.server.request('POST', "/infrastructures", body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR creating the infrastructure:" + output) + + self.__class__.inf_id = str(os.path.basename(output)) + + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) + self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") + + def test_97_destroy(self): + self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR destroying the infrastructure:" + output) if __name__ == '__main__': unittest.main() From 3e487829ff27e3156e045741b26485c017fd12b0 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 25 Jan 2016 09:27:19 +0100 Subject: [PATCH 113/509] Update tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index 9c239878f..f44fc9237 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit 9c239878fca9561cdfa11f16cd1033948fd20444 +Subproject commit f44fc9237de1409a683f7ee0d089ea96fa5358fc From 5dc41f8fe0cfd6d08e85d974c27869ecf61cb768 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 25 Jan 2016 09:27:19 +0100 Subject: [PATCH 114/509] Update tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index 9c239878f..f44fc9237 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit 9c239878fca9561cdfa11f16cd1033948fd20444 +Subproject commit f44fc9237de1409a683f7ee0d089ea96fa5358fc From 8345c47c92249f0b5730e4794fd7564abcaf6012 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 27 Jan 2016 16:34:31 +0100 Subject: [PATCH 115/509] Bugfixes detecting incorrect implementation file --- IM/tosca/Tosca.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index cc942cc6d..144c40f17 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -326,6 +326,8 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): try: response = urllib.urlopen(interface.implementation) script_content = response.read() + if response.code != 200: + raise Exception("") except Exception, ex: raise Exception("Error downloading the implementation script '%s': %s" % (interface.implementation, str(ex))) else: @@ -338,6 +340,8 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): try: response = urllib.urlopen(Tosca.ARTIFACTS_REMOTE_REPO + interface.implementation) script_content = response.read() + if response.code != 200: + raise Exception("") except Exception, ex: raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s' or in the artifacts remote url '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH, Tosca.ARTIFACTS_REMOTE_REPO)) From e1c011aaf6c36e0beaf690b3e8674ea6bff27396 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 27 Jan 2016 16:34:31 +0100 Subject: [PATCH 116/509] Bugfixes detecting incorrect implementation file --- IM/tosca/Tosca.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index cc942cc6d..144c40f17 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -326,6 +326,8 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): try: response = urllib.urlopen(interface.implementation) script_content = response.read() + if response.code != 200: + raise Exception("") except Exception, ex: raise Exception("Error downloading the implementation script '%s': %s" % (interface.implementation, str(ex))) else: @@ -338,6 +340,8 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): try: response = urllib.urlopen(Tosca.ARTIFACTS_REMOTE_REPO + interface.implementation) script_content = response.read() + if response.code != 200: + raise Exception("") except Exception, ex: raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s' or in the artifacts remote url '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH, Tosca.ARTIFACTS_REMOTE_REPO)) From 5ab11484d45ceb04cfc2d6ee4bf3363475345da5 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 27 Jan 2016 16:34:39 +0100 Subject: [PATCH 117/509] Bugfixes in REST API --- IM/REST.py | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index a494b21b8..1ed80c94c 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -22,8 +22,8 @@ import json from config import Config from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json -from radl.radl_parse import parse_radl from IM.tosca.Tosca import Tosca +from bottle import HTTPError AUTH_LINE_SEPARATOR = '\\n' @@ -98,6 +98,16 @@ def run(host, port): def stop(): bottle_server.shutdown() +def get_media_type(header): + """ + Function to get only the header media type + """ + accept = bottle.request.headers.get(header) + pos = accept.find(";") + if pos != 1: + accept = accept[:pos] + return accept.strip() + @app.route('/infrastructures/:id', method='DELETE') def RESTDestroyInfrastructure(id=None): try: @@ -170,6 +180,8 @@ def RESTGetInfrastructureProperty(id=None, prop=None): else: bottle.abort(403, "Incorrect infrastructure property") return str(res) + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting Inf. info: " + str(ex)) return False @@ -213,7 +225,7 @@ def RESTCreateInfrastructure(): bottle.abort(401, "No authentication data provided") try: - content_type = bottle.request.headers.get('Content-Type') + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() if content_type: @@ -229,6 +241,8 @@ def RESTCreateInfrastructure(): bottle.response.content_type = "text/uri-list" return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + except HTTPError, ex: + raise ex except UnauthorizedUserException, ex: bottle.abort(401, "Error Getting Inf. info: " + str(ex)) return False @@ -245,7 +259,7 @@ def RESTGetVMInfo(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: - accept = bottle.request.headers.get('Accept') + accept = get_media_type('Accept') radl = InfrastructureManager.GetVMInfo(infid, vmid, auth) @@ -264,6 +278,8 @@ def RESTGetVMInfo(infid=None, vmid=None): bottle.response.content_type = "text/plain" return info + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) return False @@ -294,7 +310,8 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): else: info = InfrastructureManager.GetVMProperty(infid, vmid, prop, auth) - accept = bottle.request.headers.get('Accept') + accept = get_media_type('Accept') + if accept: if accept == "application/json": bottle.response.content_type = accept @@ -309,6 +326,8 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): bottle.response.content_type = "text/plain" return str(info) + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) return False @@ -344,7 +363,7 @@ def RESTAddResource(id=None): else: bottle.abort(400, "Incorrect value in context parameter") - content_type = bottle.request.headers.get('Content-Type') + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() if content_type: @@ -366,6 +385,8 @@ def RESTAddResource(id=None): bottle.response.content_type = "text/uri-list" return res + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Adding resources: " + str(ex)) return False @@ -398,6 +419,8 @@ def RESTRemoveResource(infid=None, vmid=None): InfrastructureManager.RemoveResource(infid, vmid, auth, context) bottle.response.content_type = "text/plain" return "" + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) return False @@ -423,8 +446,8 @@ def RESTAlterVM(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: - content_type = bottle.request.headers.get('Content-Type') - accept = bottle.request.headers.get('Accept') + content_type = get_media_type('Content-Type') + accept = get_media_type('Accept') radl_data = bottle.request.body.read() if content_type: @@ -450,6 +473,8 @@ def RESTAlterVM(infid=None, vmid=None): bottle.response.content_type = "text/plain" return res + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) return False @@ -483,7 +508,7 @@ def RESTReconfigureInfrastructure(id=None): except: bottle.abort(400, "Incorrect vm_list format.") - content_type = bottle.request.headers.get('Content-Type') + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() if radl_data: @@ -496,6 +521,8 @@ def RESTReconfigureInfrastructure(id=None): else: radl_data = "" return InfrastructureManager.Reconfigure(id, radl_data, auth, vm_list) + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) return False From 29a79fc2b9765b24bb6da09c664eb5edc6d276ee Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 27 Jan 2016 16:34:39 +0100 Subject: [PATCH 118/509] Bugfixes in REST API --- IM/REST.py | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index a494b21b8..1ed80c94c 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -22,8 +22,8 @@ import json from config import Config from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json -from radl.radl_parse import parse_radl from IM.tosca.Tosca import Tosca +from bottle import HTTPError AUTH_LINE_SEPARATOR = '\\n' @@ -98,6 +98,16 @@ def run(host, port): def stop(): bottle_server.shutdown() +def get_media_type(header): + """ + Function to get only the header media type + """ + accept = bottle.request.headers.get(header) + pos = accept.find(";") + if pos != 1: + accept = accept[:pos] + return accept.strip() + @app.route('/infrastructures/:id', method='DELETE') def RESTDestroyInfrastructure(id=None): try: @@ -170,6 +180,8 @@ def RESTGetInfrastructureProperty(id=None, prop=None): else: bottle.abort(403, "Incorrect infrastructure property") return str(res) + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting Inf. info: " + str(ex)) return False @@ -213,7 +225,7 @@ def RESTCreateInfrastructure(): bottle.abort(401, "No authentication data provided") try: - content_type = bottle.request.headers.get('Content-Type') + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() if content_type: @@ -229,6 +241,8 @@ def RESTCreateInfrastructure(): bottle.response.content_type = "text/uri-list" return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + except HTTPError, ex: + raise ex except UnauthorizedUserException, ex: bottle.abort(401, "Error Getting Inf. info: " + str(ex)) return False @@ -245,7 +259,7 @@ def RESTGetVMInfo(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: - accept = bottle.request.headers.get('Accept') + accept = get_media_type('Accept') radl = InfrastructureManager.GetVMInfo(infid, vmid, auth) @@ -264,6 +278,8 @@ def RESTGetVMInfo(infid=None, vmid=None): bottle.response.content_type = "text/plain" return info + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) return False @@ -294,7 +310,8 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): else: info = InfrastructureManager.GetVMProperty(infid, vmid, prop, auth) - accept = bottle.request.headers.get('Accept') + accept = get_media_type('Accept') + if accept: if accept == "application/json": bottle.response.content_type = accept @@ -309,6 +326,8 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): bottle.response.content_type = "text/plain" return str(info) + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) return False @@ -344,7 +363,7 @@ def RESTAddResource(id=None): else: bottle.abort(400, "Incorrect value in context parameter") - content_type = bottle.request.headers.get('Content-Type') + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() if content_type: @@ -366,6 +385,8 @@ def RESTAddResource(id=None): bottle.response.content_type = "text/uri-list" return res + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Adding resources: " + str(ex)) return False @@ -398,6 +419,8 @@ def RESTRemoveResource(infid=None, vmid=None): InfrastructureManager.RemoveResource(infid, vmid, auth, context) bottle.response.content_type = "text/plain" return "" + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) return False @@ -423,8 +446,8 @@ def RESTAlterVM(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: - content_type = bottle.request.headers.get('Content-Type') - accept = bottle.request.headers.get('Accept') + content_type = get_media_type('Content-Type') + accept = get_media_type('Accept') radl_data = bottle.request.body.read() if content_type: @@ -450,6 +473,8 @@ def RESTAlterVM(infid=None, vmid=None): bottle.response.content_type = "text/plain" return res + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) return False @@ -483,7 +508,7 @@ def RESTReconfigureInfrastructure(id=None): except: bottle.abort(400, "Incorrect vm_list format.") - content_type = bottle.request.headers.get('Content-Type') + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() if radl_data: @@ -496,6 +521,8 @@ def RESTReconfigureInfrastructure(id=None): else: radl_data = "" return InfrastructureManager.Reconfigure(id, radl_data, auth, vm_list) + except HTTPError, ex: + raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) return False From a05876fc30313e61775ff30e0fc65157d3d5cdc6 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Jan 2016 08:54:52 +0100 Subject: [PATCH 119/509] Bugfixes in REST API --- IM/REST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index 1ed80c94c..d2e24f33b 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -104,7 +104,7 @@ def get_media_type(header): """ accept = bottle.request.headers.get(header) pos = accept.find(";") - if pos != 1: + if pos != -1: accept = accept[:pos] return accept.strip() From a634acaf5ddc9a7243a827a31918a32478a03bac Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Jan 2016 08:54:52 +0100 Subject: [PATCH 120/509] Bugfixes in REST API --- IM/REST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index 1ed80c94c..d2e24f33b 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -104,7 +104,7 @@ def get_media_type(header): """ accept = bottle.request.headers.get(header) pos = accept.find(";") - if pos != 1: + if pos != -1: accept = accept[:pos] return accept.strip() From 65842c045d98fb70ef9aaf83caaae1e6aec6fc68 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Jan 2016 12:06:57 +0100 Subject: [PATCH 121/509] Update README --- README | 11 ++++++++--- README.md | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README b/README index 1069323a6..9087df806 100644 --- a/README +++ b/README @@ -57,9 +57,14 @@ However, if you install IM from sources you should install: [defaults] transport = smart host_key_checking = False + # For old versions 1.X sudo_user = root sudo_exe = sudo + # For new versions 2.X + become_user = root + become_method = sudo + [paramiko_connection] record_host_keys=False @@ -172,9 +177,9 @@ And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates path 2. DOCKER IMAGE =============== -A Docker image named `grycap/im` has been created to make easier the deployment of an IM service using the -default configuration. Information about this image can be found here: https://registry.hub.docker.com/u/grycap/im/. +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. How to launch the IM service using docker: - $ sudo docker run -d -p 8899:8899 --name im grycap/im \ No newline at end of file + $ sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im \ No newline at end of file diff --git a/README.md b/README.md index 99f425696..ed4d19db3 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,14 @@ be installed in the system. [defaults] transport = smart host_key_checking = False +# For old versions 1.X sudo_user = root sudo_exe = sudo +# For new versions 2.X +become_user = root +become_method = sudo + [paramiko_connection] record_host_keys=False @@ -199,5 +204,5 @@ default configuration. Information about this image can be found here: https://h How to launch the IM service using docker: ```sh -sudo docker run -d -p 8899:8899 --name im indigodatacloud/im +sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im ``` From e7184d0e2b42c496599c1dc19d462eff5cfd0a2b Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Jan 2016 12:06:57 +0100 Subject: [PATCH 122/509] Update README --- README | 11 ++++++++--- README.md | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README b/README index 1069323a6..9087df806 100644 --- a/README +++ b/README @@ -57,9 +57,14 @@ However, if you install IM from sources you should install: [defaults] transport = smart host_key_checking = False + # For old versions 1.X sudo_user = root sudo_exe = sudo + # For new versions 2.X + become_user = root + become_method = sudo + [paramiko_connection] record_host_keys=False @@ -172,9 +177,9 @@ And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates path 2. DOCKER IMAGE =============== -A Docker image named `grycap/im` has been created to make easier the deployment of an IM service using the -default configuration. Information about this image can be found here: https://registry.hub.docker.com/u/grycap/im/. +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. How to launch the IM service using docker: - $ sudo docker run -d -p 8899:8899 --name im grycap/im \ No newline at end of file + $ sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im \ No newline at end of file diff --git a/README.md b/README.md index 99f425696..ed4d19db3 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,14 @@ be installed in the system. [defaults] transport = smart host_key_checking = False +# For old versions 1.X sudo_user = root sudo_exe = sudo +# For new versions 2.X +become_user = root +become_method = sudo + [paramiko_connection] record_host_keys=False @@ -199,5 +204,5 @@ default configuration. Information about this image can be found here: https://h How to launch the IM service using docker: ```sh -sudo docker run -d -p 8899:8899 --name im indigodatacloud/im +sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im ``` From 642f67455435934435561cc83e53d9296c1e2626 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 09:22:16 +0100 Subject: [PATCH 123/509] Bugfix in REST API with wildcards in content-types --- IM/REST.py | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index f5f86c6cf..b341533d0 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -180,6 +180,14 @@ def RESTGetInfrastructureProperty(id=None, prop=None): bottle.response.content_type = "application/json" res = InfrastructureManager.GetInfrastructureState(id, auth) res = json.dumps(res) + elif prop == "otputs": + bottle.response.content_type = "application/json" + sel_inf = InfrastructureManager.get_infrastructure(id, auth) + if "TOSCA" in sel_inf.extra_info: + res = Tosca(sel_inf.extra_info["TOSCA"]).get_outputs(sel_inf) + else: + bottle.abort(403, "'otputs' infrastructure property is not valid in this infrastructure") + res = json.dumps(res) else: bottle.abort(403, "Incorrect infrastructure property") return str(res) @@ -230,18 +238,27 @@ def RESTCreateInfrastructure(): try: content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() + tosca_data = None if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": + tosca_data = radl_data radl_data = Tosca(radl_data).to_radl() - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False inf_id = InfrastructureManager.CreateInfrastructure(radl_data, auth) + # Store the TOSCA document + if tosca_data: + sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) + sel_inf.extra_info['TOSCA'] = radl_data + bottle.response.content_type = "text/uri-list" return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) except HTTPError, ex: @@ -270,9 +287,9 @@ def RESTGetVMInfo(infid=None, vmid=None): if accept == "application/json": bottle.response.content_type = accept info = dump_radl_json(radl, enter="", indent="") - elif accept == "text/plain": + elif accept in ["text/plain","*/*","text/*"]: info = str(radl) - bottle.response.content_type = accept + bottle.response.content_type = "text/plain" else: bottle.abort(404, "Unsupported Accept Media Type: %s" % accept) return False @@ -320,8 +337,8 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): bottle.response.content_type = accept if isinstance(info, str) or isinstance(info, unicode): info = '"' + info + '"' - elif accept == "text/plain": - bottle.response.content_type = accept + elif accept in ["text/plain","*/*","text/*"]: + bottle.response.content_type = "text/plain" else: bottle.abort(404, "Unsupported Accept Media Type: %s" % accept) return False @@ -368,17 +385,26 @@ def RESTAddResource(id=None): content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() + tosca_data = None if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": + tosca_data = radl_data radl_data = Tosca(radl_data).to_radl() - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False vm_ids = InfrastructureManager.AddResource(id, radl_data, auth, context) + + # Replace the TOSCA document + if tosca_data: + sel_inf = InfrastructureManager.get_infrastructure(id, auth) + sel_inf.extra_info['TOSCA'] = radl_data res = "" for vm_id in vm_ids: @@ -456,7 +482,9 @@ def RESTAlterVM(infid=None, vmid=None): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False @@ -518,7 +546,9 @@ def RESTReconfigureInfrastructure(id=None): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False else: From 870feff8ca5596809470feb264ca2d8964050844 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 09:22:16 +0100 Subject: [PATCH 124/509] Bugfix in REST API with wildcards in content-types --- IM/REST.py | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index f5f86c6cf..b341533d0 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -180,6 +180,14 @@ def RESTGetInfrastructureProperty(id=None, prop=None): bottle.response.content_type = "application/json" res = InfrastructureManager.GetInfrastructureState(id, auth) res = json.dumps(res) + elif prop == "otputs": + bottle.response.content_type = "application/json" + sel_inf = InfrastructureManager.get_infrastructure(id, auth) + if "TOSCA" in sel_inf.extra_info: + res = Tosca(sel_inf.extra_info["TOSCA"]).get_outputs(sel_inf) + else: + bottle.abort(403, "'otputs' infrastructure property is not valid in this infrastructure") + res = json.dumps(res) else: bottle.abort(403, "Incorrect infrastructure property") return str(res) @@ -230,18 +238,27 @@ def RESTCreateInfrastructure(): try: content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() + tosca_data = None if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": + tosca_data = radl_data radl_data = Tosca(radl_data).to_radl() - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False inf_id = InfrastructureManager.CreateInfrastructure(radl_data, auth) + # Store the TOSCA document + if tosca_data: + sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) + sel_inf.extra_info['TOSCA'] = radl_data + bottle.response.content_type = "text/uri-list" return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) except HTTPError, ex: @@ -270,9 +287,9 @@ def RESTGetVMInfo(infid=None, vmid=None): if accept == "application/json": bottle.response.content_type = accept info = dump_radl_json(radl, enter="", indent="") - elif accept == "text/plain": + elif accept in ["text/plain","*/*","text/*"]: info = str(radl) - bottle.response.content_type = accept + bottle.response.content_type = "text/plain" else: bottle.abort(404, "Unsupported Accept Media Type: %s" % accept) return False @@ -320,8 +337,8 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): bottle.response.content_type = accept if isinstance(info, str) or isinstance(info, unicode): info = '"' + info + '"' - elif accept == "text/plain": - bottle.response.content_type = accept + elif accept in ["text/plain","*/*","text/*"]: + bottle.response.content_type = "text/plain" else: bottle.abort(404, "Unsupported Accept Media Type: %s" % accept) return False @@ -368,17 +385,26 @@ def RESTAddResource(id=None): content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() + tosca_data = None if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": + tosca_data = radl_data radl_data = Tosca(radl_data).to_radl() - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False vm_ids = InfrastructureManager.AddResource(id, radl_data, auth, context) + + # Replace the TOSCA document + if tosca_data: + sel_inf = InfrastructureManager.get_infrastructure(id, auth) + sel_inf.extra_info['TOSCA'] = radl_data res = "" for vm_id in vm_ids: @@ -456,7 +482,9 @@ def RESTAlterVM(infid=None, vmid=None): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False @@ -518,7 +546,9 @@ def RESTReconfigureInfrastructure(id=None): if content_type: if content_type == "application/json": radl_data = parse_radl_json(radl_data) - elif content_type != "text/plain": + elif content_type in ["text/plain","*/*","text/*"]: + content_type = "text/plain" + else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False else: From be9655b9f1fd11b15b1a6bbd975bdae300c5b06f Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 13:13:12 +0100 Subject: [PATCH 125/509] Add IM_NODE_PUBLIC_IP and IM_NODE_PRIVATE_IP ansible variables --- IM/ConfManager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 597d79206..2585c761a 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -378,6 +378,10 @@ def generate_inventory(self, tmp_dir): if self.inf.vm_master and vm.id == self.inf.vm_master.id: node_line += ' ansible_connection=local' + if vm.getPublicIP(): + node_line += ' IM_NODE_PUBLIC_IP=' + vm.getPublicIP() + if vm.getPrivateIP(): + node_line += ' IM_NODE_PRIVATE_IP=' + vm.getPrivateIP() node_line += ' IM_NODE_HOSTNAME=' + nodename node_line += ' IM_NODE_FQDN=' + nodename + "." + nodedom node_line += ' IM_NODE_DOMAIN=' + nodedom From 041a06a5ab7ac472dc3e685d44a3e76ec4902fc1 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 13:13:12 +0100 Subject: [PATCH 126/509] Add IM_NODE_PUBLIC_IP and IM_NODE_PRIVATE_IP ansible variables --- IM/ConfManager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 597d79206..2585c761a 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -378,6 +378,10 @@ def generate_inventory(self, tmp_dir): if self.inf.vm_master and vm.id == self.inf.vm_master.id: node_line += ' ansible_connection=local' + if vm.getPublicIP(): + node_line += ' IM_NODE_PUBLIC_IP=' + vm.getPublicIP() + if vm.getPrivateIP(): + node_line += ' IM_NODE_PRIVATE_IP=' + vm.getPrivateIP() node_line += ' IM_NODE_HOSTNAME=' + nodename node_line += ' IM_NODE_FQDN=' + nodename + "." + nodedom node_line += ' IM_NODE_DOMAIN=' + nodedom From 00047178a50e4564bb99b0868ab000d881e98663 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 13:35:36 +0100 Subject: [PATCH 127/509] Add the get outputs function to TOSCA docs --- IM/InfrastructureInfo.py | 2 + IM/REST.py | 8 +-- IM/tosca/Tosca.py | 133 +++++++++++++++++++++++++-------------- README.md | 3 + 4 files changed, 94 insertions(+), 52 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index ecaa8ef0d..af0076821 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -84,6 +84,8 @@ def __init__(self): """Flag to specify that the configuration threads of this inf has finished successfully or with errors.""" self.conf_threads = [] """ List of configuration threads.""" + self.extra_info = {} + """ Extra information about the Infrastructure.""" def __getstate__(self): """ diff --git a/IM/REST.py b/IM/REST.py index b341533d0..d4192175d 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -180,13 +180,13 @@ def RESTGetInfrastructureProperty(id=None, prop=None): bottle.response.content_type = "application/json" res = InfrastructureManager.GetInfrastructureState(id, auth) res = json.dumps(res) - elif prop == "otputs": + elif prop == "outputs": bottle.response.content_type = "application/json" sel_inf = InfrastructureManager.get_infrastructure(id, auth) if "TOSCA" in sel_inf.extra_info: res = Tosca(sel_inf.extra_info["TOSCA"]).get_outputs(sel_inf) else: - bottle.abort(403, "'otputs' infrastructure property is not valid in this infrastructure") + bottle.abort(403, "'outputs' infrastructure property is not valid in this infrastructure") res = json.dumps(res) else: bottle.abort(403, "Incorrect infrastructure property") @@ -257,7 +257,7 @@ def RESTCreateInfrastructure(): # Store the TOSCA document if tosca_data: sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) - sel_inf.extra_info['TOSCA'] = radl_data + sel_inf.extra_info['TOSCA'] = tosca_data bottle.response.content_type = "text/uri-list" return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) @@ -404,7 +404,7 @@ def RESTAddResource(id=None): # Replace the TOSCA document if tosca_data: sel_inf = InfrastructureManager.get_infrastructure(id, auth) - sel_inf.extra_info['TOSCA'] = radl_data + sel_inf.extra_info['TOSCA'] = tosca_data res = "" for vm_id in vm_ids: diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 144c40f17..16ce3212d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -77,17 +77,6 @@ def __init__(self, yaml_str): f.flush() self.tosca = IndigoToscaTemplate(f.name) - @staticmethod - def is_tosca(yaml_string): - """ - Check if a string seems to be a tosca document - Check if it has the strings 'tosca_definitions_version' and 'tosca_simple_yaml' - """ - if yaml_string.find("tosca_definitions_version") != -1 and yaml_string.find("tosca_simple_yaml") != -1: - return True - else: - return False - def to_radl(self): """ Converts the current ToscaTemplate object in a RADL object @@ -348,7 +337,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: for var_name, var_value in env.iteritems(): - variables += " %s: %s " % (var_name, var_value) + "\n" + variables += ' %s: "%s" ' % (var_name, var_value) + "\n" variables += "\n" recipe_list.append(script_content) @@ -452,7 +441,7 @@ def _is_intrinsic(function): return func_name in ["concat", "token"] return False - def _get_intrinsic_value(self, func, node): + def _get_intrinsic_value(self, func, node, inf_info): if isinstance(func, dict) and len(func) == 1: func_name = list(func.keys())[0] if func_name == "concat": @@ -460,7 +449,7 @@ def _get_intrinsic_value(self, func, node): res = "" for item in items: if is_function(item): - res += str(self._final_function_result(item, node)) + res += str(self._final_function_result(item, node, inf_info)) else: res += str(item) return res @@ -483,7 +472,7 @@ def _get_intrinsic_value(self, func, node): Tosca.logger.warn("Intrinsic function %s not supported." % func_name) return None - def _get_attribute_result(self, func, node): + def _get_attribute_result(self, func, node, inf_info): """Get an attribute value of an entity defined in the service template Node template attributes values are set in runtime and therefore its the @@ -516,43 +505,78 @@ def _get_attribute_result(self, func, node): node = n break - if attribute_name == "tosca_id": - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_VMID }}" - else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_VMID'] }}" % node.name - elif attribute_name == "tosca_name": - return node.name - elif attribute_name == "private_address": - # TODO: we suppose that iface 1 is the private one - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_NET_1_IP }}" + if inf_info: + vm_list = inf_info.get_vm_list_by_system_name() + + if node.name not in vm_list: + Tosca.logger.warn("There are no VM associated with the name %s." % node.name) + return None else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_1_IP'] }}" % node.name - elif attribute_name == "public_address": - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_ANSIBLE_IP }}" + # Always assume that there will be only one VM per group + vm = vm_list[node.name][0] + + if attribute_name == "tosca_id": + return vm.id + elif attribute_name == "tosca_name": + return node.name + elif attribute_name == "private_address": + return vm.getPrivateIP() + elif attribute_name == "public_address": + return vm.getPublicIP() + elif attribute_name == "ip_address": + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.network.Port": + order = node.get_property_value('order') + return vm.getNumNetworkWithConnection(order) + elif root_type == "tosca.capabilities.Endpoint": + if vm.getPublicIP(): + return vm.getPublicIP() + else: + return vm.getPrivateIP() + else: + Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + return None else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name - elif attribute_name == "ip_address": - root_type = Tosca._get_root_parent_type(node).type - if root_type == "tosca.nodes.network.Port": - order = node.get_property_value('order') - return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) - elif root_type == "tosca.capabilities.Endpoint": - # TODO: check this + Tosca.logger.warn("Attribute %s not supported." % attribute_name) + return None + else: + if attribute_name == "tosca_id": if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_ANSIBLE_IP }}" + return "{{ IM_NODE_VMID }}" else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name + return "{{ hostvars[groups['%s'][0]]['IM_NODE_VMID'] }}" % node.name + elif attribute_name == "tosca_name": + return node.name + elif attribute_name == "private_address": + # TODO: we suppose that iface 1 is the private one + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PRIVATE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PRIVATE_IP'] }}" % node.name + elif attribute_name == "public_address": + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PUBLIC_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name + elif attribute_name == "ip_address": + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.network.Port": + order = node.get_property_value('order') + return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) + elif root_type == "tosca.capabilities.Endpoint": + # TODO: check this + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PUBLIC_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name + else: + Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + return None else: - Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + Tosca.logger.warn("Attribute %s not supported." % attribute_name) return None - else: - Tosca.logger.warn("Attribute %s not supported." % attribute_name) - return None - def _final_function_result(self, func, node): + def _final_function_result(self, func, node, inf_info=None): """ Take a translator.toscalib.functions.Function and return the final result (in some cases the result of a function is another function) @@ -563,13 +587,13 @@ def _final_function_result(self, func, node): if isinstance(func, Function): if isinstance(func, GetAttribute): - func = self._get_attribute_result(func, node) + func = self._get_attribute_result(func, node, inf_info) while isinstance(func, Function): func = func.result() if isinstance(func, dict): if self._is_intrinsic(func): - func = self._get_intrinsic_value(func, node) + func = self._get_intrinsic_value(func, node, inf_info) if func is None: # TODO: resolve function values related with run-time values as IM or ansible variables @@ -1008,4 +1032,17 @@ def _merge_yaml(yaml1, yaml2): yamlo1[key] = yamlo2[key] result.append(yamlo1) - return yaml.dump(result, default_flow_style=False, explicit_start=True, width=256) \ No newline at end of file + return yaml.dump(result, default_flow_style=False, explicit_start=True, width=256) + + def get_outputs(self, inf_info): + """ + Get the outputs of the TOSCA document using the InfrastructureInfo + object 'inf_info' to get the data of the VMs + """ + res = {} + + for output in self.tosca.outputs: + val = self._final_function_result(output.attrs.get(output.VALUE), None, inf_info) + res[output.name] = val + + return res \ No newline at end of file diff --git a/README.md b/README.md index ed4d19db3..bbd0b65f0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/) has +added support to TOSCA documents as input for the infrastructure creation. + Read the documentation and more at http://www.grycap.upv.es/im. There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos From 4f15b3efbd2fe2b2380cbe827b9bff22424f1ae3 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 13:35:36 +0100 Subject: [PATCH 128/509] Add the get outputs function to TOSCA docs --- IM/InfrastructureInfo.py | 2 + IM/REST.py | 8 +-- IM/tosca/Tosca.py | 133 +++++++++++++++++++++++++-------------- README.md | 3 + 4 files changed, 94 insertions(+), 52 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index ecaa8ef0d..af0076821 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -84,6 +84,8 @@ def __init__(self): """Flag to specify that the configuration threads of this inf has finished successfully or with errors.""" self.conf_threads = [] """ List of configuration threads.""" + self.extra_info = {} + """ Extra information about the Infrastructure.""" def __getstate__(self): """ diff --git a/IM/REST.py b/IM/REST.py index b341533d0..d4192175d 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -180,13 +180,13 @@ def RESTGetInfrastructureProperty(id=None, prop=None): bottle.response.content_type = "application/json" res = InfrastructureManager.GetInfrastructureState(id, auth) res = json.dumps(res) - elif prop == "otputs": + elif prop == "outputs": bottle.response.content_type = "application/json" sel_inf = InfrastructureManager.get_infrastructure(id, auth) if "TOSCA" in sel_inf.extra_info: res = Tosca(sel_inf.extra_info["TOSCA"]).get_outputs(sel_inf) else: - bottle.abort(403, "'otputs' infrastructure property is not valid in this infrastructure") + bottle.abort(403, "'outputs' infrastructure property is not valid in this infrastructure") res = json.dumps(res) else: bottle.abort(403, "Incorrect infrastructure property") @@ -257,7 +257,7 @@ def RESTCreateInfrastructure(): # Store the TOSCA document if tosca_data: sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) - sel_inf.extra_info['TOSCA'] = radl_data + sel_inf.extra_info['TOSCA'] = tosca_data bottle.response.content_type = "text/uri-list" return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) @@ -404,7 +404,7 @@ def RESTAddResource(id=None): # Replace the TOSCA document if tosca_data: sel_inf = InfrastructureManager.get_infrastructure(id, auth) - sel_inf.extra_info['TOSCA'] = radl_data + sel_inf.extra_info['TOSCA'] = tosca_data res = "" for vm_id in vm_ids: diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 144c40f17..16ce3212d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -77,17 +77,6 @@ def __init__(self, yaml_str): f.flush() self.tosca = IndigoToscaTemplate(f.name) - @staticmethod - def is_tosca(yaml_string): - """ - Check if a string seems to be a tosca document - Check if it has the strings 'tosca_definitions_version' and 'tosca_simple_yaml' - """ - if yaml_string.find("tosca_definitions_version") != -1 and yaml_string.find("tosca_simple_yaml") != -1: - return True - else: - return False - def to_radl(self): """ Converts the current ToscaTemplate object in a RADL object @@ -348,7 +337,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: for var_name, var_value in env.iteritems(): - variables += " %s: %s " % (var_name, var_value) + "\n" + variables += ' %s: "%s" ' % (var_name, var_value) + "\n" variables += "\n" recipe_list.append(script_content) @@ -452,7 +441,7 @@ def _is_intrinsic(function): return func_name in ["concat", "token"] return False - def _get_intrinsic_value(self, func, node): + def _get_intrinsic_value(self, func, node, inf_info): if isinstance(func, dict) and len(func) == 1: func_name = list(func.keys())[0] if func_name == "concat": @@ -460,7 +449,7 @@ def _get_intrinsic_value(self, func, node): res = "" for item in items: if is_function(item): - res += str(self._final_function_result(item, node)) + res += str(self._final_function_result(item, node, inf_info)) else: res += str(item) return res @@ -483,7 +472,7 @@ def _get_intrinsic_value(self, func, node): Tosca.logger.warn("Intrinsic function %s not supported." % func_name) return None - def _get_attribute_result(self, func, node): + def _get_attribute_result(self, func, node, inf_info): """Get an attribute value of an entity defined in the service template Node template attributes values are set in runtime and therefore its the @@ -516,43 +505,78 @@ def _get_attribute_result(self, func, node): node = n break - if attribute_name == "tosca_id": - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_VMID }}" - else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_VMID'] }}" % node.name - elif attribute_name == "tosca_name": - return node.name - elif attribute_name == "private_address": - # TODO: we suppose that iface 1 is the private one - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_NET_1_IP }}" + if inf_info: + vm_list = inf_info.get_vm_list_by_system_name() + + if node.name not in vm_list: + Tosca.logger.warn("There are no VM associated with the name %s." % node.name) + return None else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_1_IP'] }}" % node.name - elif attribute_name == "public_address": - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_ANSIBLE_IP }}" + # Always assume that there will be only one VM per group + vm = vm_list[node.name][0] + + if attribute_name == "tosca_id": + return vm.id + elif attribute_name == "tosca_name": + return node.name + elif attribute_name == "private_address": + return vm.getPrivateIP() + elif attribute_name == "public_address": + return vm.getPublicIP() + elif attribute_name == "ip_address": + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.network.Port": + order = node.get_property_value('order') + return vm.getNumNetworkWithConnection(order) + elif root_type == "tosca.capabilities.Endpoint": + if vm.getPublicIP(): + return vm.getPublicIP() + else: + return vm.getPrivateIP() + else: + Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + return None else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name - elif attribute_name == "ip_address": - root_type = Tosca._get_root_parent_type(node).type - if root_type == "tosca.nodes.network.Port": - order = node.get_property_value('order') - return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) - elif root_type == "tosca.capabilities.Endpoint": - # TODO: check this + Tosca.logger.warn("Attribute %s not supported." % attribute_name) + return None + else: + if attribute_name == "tosca_id": if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_ANSIBLE_IP }}" + return "{{ IM_NODE_VMID }}" else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_ANSIBLE_IP'] }}" % node.name + return "{{ hostvars[groups['%s'][0]]['IM_NODE_VMID'] }}" % node.name + elif attribute_name == "tosca_name": + return node.name + elif attribute_name == "private_address": + # TODO: we suppose that iface 1 is the private one + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PRIVATE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PRIVATE_IP'] }}" % node.name + elif attribute_name == "public_address": + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PUBLIC_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name + elif attribute_name == "ip_address": + root_type = Tosca._get_root_parent_type(node).type + if root_type == "tosca.nodes.network.Port": + order = node.get_property_value('order') + return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) + elif root_type == "tosca.capabilities.Endpoint": + # TODO: check this + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PUBLIC_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name + else: + Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + return None else: - Tosca.logger.warn("Attribute ip_address only supported in tosca.nodes.network.Port and tosca.capabilities.Endpoint nodes.") + Tosca.logger.warn("Attribute %s not supported." % attribute_name) return None - else: - Tosca.logger.warn("Attribute %s not supported." % attribute_name) - return None - def _final_function_result(self, func, node): + def _final_function_result(self, func, node, inf_info=None): """ Take a translator.toscalib.functions.Function and return the final result (in some cases the result of a function is another function) @@ -563,13 +587,13 @@ def _final_function_result(self, func, node): if isinstance(func, Function): if isinstance(func, GetAttribute): - func = self._get_attribute_result(func, node) + func = self._get_attribute_result(func, node, inf_info) while isinstance(func, Function): func = func.result() if isinstance(func, dict): if self._is_intrinsic(func): - func = self._get_intrinsic_value(func, node) + func = self._get_intrinsic_value(func, node, inf_info) if func is None: # TODO: resolve function values related with run-time values as IM or ansible variables @@ -1008,4 +1032,17 @@ def _merge_yaml(yaml1, yaml2): yamlo1[key] = yamlo2[key] result.append(yamlo1) - return yaml.dump(result, default_flow_style=False, explicit_start=True, width=256) \ No newline at end of file + return yaml.dump(result, default_flow_style=False, explicit_start=True, width=256) + + def get_outputs(self, inf_info): + """ + Get the outputs of the TOSCA document using the InfrastructureInfo + object 'inf_info' to get the data of the VMs + """ + res = {} + + for output in self.tosca.outputs: + val = self._final_function_result(output.attrs.get(output.VALUE), None, inf_info) + res[output.name] = val + + return res \ No newline at end of file diff --git a/README.md b/README.md index ed4d19db3..bbd0b65f0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/) has +added support to TOSCA documents as input for the infrastructure creation. + Read the documentation and more at http://www.grycap.upv.es/im. There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos From 5dc8381987bb4dbe845b61fe454214ca14728eb8 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 14:20:22 +0100 Subject: [PATCH 129/509] Update REST documentation --- doc/source/REST.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/REST.rst b/doc/source/REST.rst index d9b78dd77..c95dd0b7a 100644 --- a/doc/source/REST.rst +++ b/doc/source/REST.rst @@ -39,7 +39,8 @@ Next tables summaries the resources and the HTTP methods available. +=============+=====================================================+====================================================+ | **GET** | | **Get** the specified property ``property_name`` | | **Get** the specified property ``property_name`` | | | | associated to the machine ``vmId`` in ``infId``. | | associated to the infrastructure ``infId``. | -| | | It has one special property: ``contmsg``. | | It has two properties: ``contmsg`` and ``radl``. | +| | | It has one special property: ``contmsg``. | | It has four properties: ``contmsg``, ``radl``, | +| | | | ``state`` and ``outputs``. | +-------------+-----------------------------------------------------+----------------------------------------------------+ +-------------+-----------------------------------------------+------------------------------------------------+ @@ -80,6 +81,7 @@ GET ``http://imserver.com/infrastructures//`` :fail response: 401, 404, 400, 403 Return property ``property_name`` associated to the infrastructure with ID ``infId``. It has three properties: + :``outputs``: in case of TOSCA documents it will return a JSON object with the outputs of the TOSCA document. :``contmsg``: a string with the contextualization message. :``radl``: a string with the original specified RADL of the infrastructure. :``state``: a JSON object with two elements: From 0f91717316fb79c34b15177c049f23d324948f86 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 1 Feb 2016 14:20:22 +0100 Subject: [PATCH 130/509] Update REST documentation --- doc/source/REST.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/REST.rst b/doc/source/REST.rst index d9b78dd77..c95dd0b7a 100644 --- a/doc/source/REST.rst +++ b/doc/source/REST.rst @@ -39,7 +39,8 @@ Next tables summaries the resources and the HTTP methods available. +=============+=====================================================+====================================================+ | **GET** | | **Get** the specified property ``property_name`` | | **Get** the specified property ``property_name`` | | | | associated to the machine ``vmId`` in ``infId``. | | associated to the infrastructure ``infId``. | -| | | It has one special property: ``contmsg``. | | It has two properties: ``contmsg`` and ``radl``. | +| | | It has one special property: ``contmsg``. | | It has four properties: ``contmsg``, ``radl``, | +| | | | ``state`` and ``outputs``. | +-------------+-----------------------------------------------------+----------------------------------------------------+ +-------------+-----------------------------------------------+------------------------------------------------+ @@ -80,6 +81,7 @@ GET ``http://imserver.com/infrastructures//`` :fail response: 401, 404, 400, 403 Return property ``property_name`` associated to the infrastructure with ID ``infId``. It has three properties: + :``outputs``: in case of TOSCA documents it will return a JSON object with the outputs of the TOSCA document. :``contmsg``: a string with the contextualization message. :``radl``: a string with the original specified RADL of the infrastructure. :``state``: a JSON object with two elements: From 95a0462aaad1a6fbdf2b965a3b41b9df857fab2f Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 2 Feb 2016 15:25:09 +0100 Subject: [PATCH 131/509] update tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index f44fc9237..9504bff21 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit f44fc9237de1409a683f7ee0d089ea96fa5358fc +Subproject commit 9504bff2132c0bc8a4ab3e19102078e6a8493ea0 From 4c0d12c9da00c19d6988421d8d8c9f32d3b0b71c Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 2 Feb 2016 15:25:09 +0100 Subject: [PATCH 132/509] update tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index f44fc9237..9504bff21 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit f44fc9237de1409a683f7ee0d089ea96fa5358fc +Subproject commit 9504bff2132c0bc8a4ab3e19102078e6a8493ea0 From 52833592ec9d82a835502713c927c0fd38533005 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 2 Feb 2016 15:55:57 +0100 Subject: [PATCH 133/509] Add test for TOSCA get outputs function --- test/TestREST.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index 3e4044af3..902e15576 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -394,7 +394,7 @@ def test_96_create_tosca(self): distribution: ubuntu test_db: - type: tosca.nodes.Database.MySQL + type: tosca.nodes.indigo.Database.MySQL properties: name: { get_input: db_name } user: { get_input: db_user } @@ -439,6 +439,11 @@ def test_96_create_tosca(self): architecture: x86_64 type: linux distribution: ubuntu + + + outputs: + server_url: + value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } """ self.server.request('POST', "/infrastructures", body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) @@ -451,7 +456,16 @@ def test_96_create_tosca(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_97_destroy(self): + def test_96_get_outputs(self): + self.server.request('GET', "/infrastructures/" + self.inf_id + "/outputs", headers = {'Authorization' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) + res = json.loads(output) + server_url = res['server_url'] + self.assertRegexpMatches(server_url, 'http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', msg="Unexpected outputs: " + output) + + def test_98_destroy(self): self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) From fd7cb486397737d222a154bb6a16971830540a52 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 2 Feb 2016 15:55:57 +0100 Subject: [PATCH 134/509] Add test for TOSCA get outputs function --- test/TestREST.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index 3e4044af3..902e15576 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -394,7 +394,7 @@ def test_96_create_tosca(self): distribution: ubuntu test_db: - type: tosca.nodes.Database.MySQL + type: tosca.nodes.indigo.Database.MySQL properties: name: { get_input: db_name } user: { get_input: db_user } @@ -439,6 +439,11 @@ def test_96_create_tosca(self): architecture: x86_64 type: linux distribution: ubuntu + + + outputs: + server_url: + value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } """ self.server.request('POST', "/infrastructures", body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) @@ -451,7 +456,16 @@ def test_96_create_tosca(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_97_destroy(self): + def test_96_get_outputs(self): + self.server.request('GET', "/infrastructures/" + self.inf_id + "/outputs", headers = {'Authorization' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) + res = json.loads(output) + server_url = res['server_url'] + self.assertRegexpMatches(server_url, 'http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', msg="Unexpected outputs: " + output) + + def test_98_destroy(self): self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) From 2c8a5cc3403f623207a5c0d49857f23ee429ab8c Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 3 Feb 2016 14:52:58 +0100 Subject: [PATCH 135/509] Modify AddResource to support correct behaviour with count --- IM/REST.py | 3 +- IM/tosca/Tosca.py | 26 +++++++-- IM/tosca/tosca-types | 2 +- test/TestREST.py | 125 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 148 insertions(+), 8 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index d4192175d..63f8af8ac 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -392,7 +392,8 @@ def RESTAddResource(id=None): radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": tosca_data = radl_data - radl_data = Tosca(radl_data).to_radl() + sel_inf = InfrastructureManager.get_infrastructure(id, auth) + radl_data = Tosca(radl_data).to_radl(sel_inf) elif content_type in ["text/plain","*/*","text/*"]: content_type = "text/plain" else: diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 16ce3212d..a95b17506 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -77,9 +77,12 @@ def __init__(self, yaml_str): f.flush() self.tosca = IndigoToscaTemplate(f.name) - def to_radl(self): + def to_radl(self, inf_info = None): """ - Converts the current ToscaTemplate object in a RADL object + Converts the current ToscaTemplate object in a RADL object + If the inf_info parameter is not None, it is an AddResource and + we must check the number of resources to correctly compute the + number of nodes to deploy """ relationships = [] @@ -114,8 +117,9 @@ def to_radl(self): Tosca._add_node_nets(node, radl, sys, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system - count, min_instances, _, default_instances = Tosca._get_scalable_properties(node) + min_instances, _, default_instances, count = Tosca._get_scalable_properties(node) if count is not None: + # we must check the correct number of instances to deploy num_instances = count elif default_instances is not None: num_instances = default_instances @@ -123,6 +127,9 @@ def to_radl(self): num_instances = min_instances else: num_instances = 1 + + num_instances = num_instances - self._get_num_instances(sys.name, inf_info) + if num_instances > 0: dep = deploy(sys.name, num_instances) radl.deploys.append(dep) @@ -148,6 +155,19 @@ def to_radl(self): return self._complete_radl_networks(radl) + def _get_num_instances(self, sys_name, inf_info): + """ + Get the current number of instances of system type name sys_name + """ + current_num = 0 + + if inf_info: + vm_list = inf_info.get_vm_list_by_system_name() + if sys_name in vm_list: + current_num = len(vm_list[sys_name]) + + return current_num + @staticmethod def _add_node_nets(node, radl, system, nodetemplates): diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index 9504bff21..b39eaf456 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit 9504bff2132c0bc8a4ab3e19102078e6a8493ea0 +Subproject commit b39eaf4560c3b986bda2a6ccc99a4659143953a4 diff --git a/test/TestREST.py b/test/TestREST.py index 902e15576..91a4c8fc7 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -338,13 +338,13 @@ def test_90_start_vm(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 120, [VirtualMachine.RUNNING], ["/infrastructures/" + self.inf_id + "/vms/0"]) self.assertTrue(all_configured, msg="ERROR waiting the vm to be started (timeout).") - def test_95_destroy(self): + def test_92_destroy(self): self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR destroying the infrastructure:" + output) - def test_96_create_tosca(self): + def test_93_create_tosca(self): """ Test the CreateInfrastructure IM function with a TOSCA document """ @@ -456,7 +456,7 @@ def test_96_create_tosca(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_96_get_outputs(self): + def test_94_get_outputs(self): self.server.request('GET', "/infrastructures/" + self.inf_id + "/outputs", headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) @@ -465,6 +465,125 @@ def test_96_get_outputs(self): server_url = res['server_url'] self.assertRegexpMatches(server_url, 'http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', msg="Unexpected outputs: " + output) + def test_95_add_tosca(self): + """ + Test the AddResource IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + scalable: + properties: + count: 2 + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.indigo.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + + + outputs: + server_url: + value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } + """ + + self.server.request('POST', "/infrastructures/" + self.inf_id, body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) + + self.server.request('GET', "/infrastructures/" + self.inf_id, headers = {'AUTHORIZATION' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure info:" + output) + vm_ids = output.split("\n") + self.assertEqual(len(vm_ids), 3, msg="ERROR getting infrastructure info: Incorrect number of VMs(" + str(len(vm_ids)) + "). It must be 2") + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) + self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") + def test_98_destroy(self): self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() From f2473cf2dcc95710402c6cfa751d02535c8a7221 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 3 Feb 2016 14:52:58 +0100 Subject: [PATCH 136/509] Modify AddResource to support correct behaviour with count --- IM/REST.py | 3 +- IM/tosca/Tosca.py | 26 +++++++-- IM/tosca/tosca-types | 2 +- test/TestREST.py | 125 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 148 insertions(+), 8 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index d4192175d..63f8af8ac 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -392,7 +392,8 @@ def RESTAddResource(id=None): radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": tosca_data = radl_data - radl_data = Tosca(radl_data).to_radl() + sel_inf = InfrastructureManager.get_infrastructure(id, auth) + radl_data = Tosca(radl_data).to_radl(sel_inf) elif content_type in ["text/plain","*/*","text/*"]: content_type = "text/plain" else: diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 16ce3212d..a95b17506 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -77,9 +77,12 @@ def __init__(self, yaml_str): f.flush() self.tosca = IndigoToscaTemplate(f.name) - def to_radl(self): + def to_radl(self, inf_info = None): """ - Converts the current ToscaTemplate object in a RADL object + Converts the current ToscaTemplate object in a RADL object + If the inf_info parameter is not None, it is an AddResource and + we must check the number of resources to correctly compute the + number of nodes to deploy """ relationships = [] @@ -114,8 +117,9 @@ def to_radl(self): Tosca._add_node_nets(node, radl, sys, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system - count, min_instances, _, default_instances = Tosca._get_scalable_properties(node) + min_instances, _, default_instances, count = Tosca._get_scalable_properties(node) if count is not None: + # we must check the correct number of instances to deploy num_instances = count elif default_instances is not None: num_instances = default_instances @@ -123,6 +127,9 @@ def to_radl(self): num_instances = min_instances else: num_instances = 1 + + num_instances = num_instances - self._get_num_instances(sys.name, inf_info) + if num_instances > 0: dep = deploy(sys.name, num_instances) radl.deploys.append(dep) @@ -148,6 +155,19 @@ def to_radl(self): return self._complete_radl_networks(radl) + def _get_num_instances(self, sys_name, inf_info): + """ + Get the current number of instances of system type name sys_name + """ + current_num = 0 + + if inf_info: + vm_list = inf_info.get_vm_list_by_system_name() + if sys_name in vm_list: + current_num = len(vm_list[sys_name]) + + return current_num + @staticmethod def _add_node_nets(node, radl, system, nodetemplates): diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index 9504bff21..b39eaf456 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit 9504bff2132c0bc8a4ab3e19102078e6a8493ea0 +Subproject commit b39eaf4560c3b986bda2a6ccc99a4659143953a4 diff --git a/test/TestREST.py b/test/TestREST.py index 902e15576..91a4c8fc7 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -338,13 +338,13 @@ def test_90_start_vm(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 120, [VirtualMachine.RUNNING], ["/infrastructures/" + self.inf_id + "/vms/0"]) self.assertTrue(all_configured, msg="ERROR waiting the vm to be started (timeout).") - def test_95_destroy(self): + def test_92_destroy(self): self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR destroying the infrastructure:" + output) - def test_96_create_tosca(self): + def test_93_create_tosca(self): """ Test the CreateInfrastructure IM function with a TOSCA document """ @@ -456,7 +456,7 @@ def test_96_create_tosca(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - def test_96_get_outputs(self): + def test_94_get_outputs(self): self.server.request('GET', "/infrastructures/" + self.inf_id + "/outputs", headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) @@ -465,6 +465,125 @@ def test_96_get_outputs(self): server_url = res['server_url'] self.assertRegexpMatches(server_url, 'http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', msg="Unexpected outputs: " + output) + def test_95_add_tosca(self): + """ + Test the AddResource IM function with a TOSCA document + """ + tosca = """ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + scalable: + properties: + count: 2 + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.indigo.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + + + outputs: + server_url: + value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } + """ + + self.server.request('POST', "/infrastructures/" + self.inf_id, body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) + + self.server.request('GET', "/infrastructures/" + self.inf_id, headers = {'AUTHORIZATION' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure info:" + output) + vm_ids = output.split("\n") + self.assertEqual(len(vm_ids), 3, msg="ERROR getting infrastructure info: Incorrect number of VMs(" + str(len(vm_ids)) + "). It must be 2") + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) + self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") + def test_98_destroy(self): self.server.request('DELETE', "/infrastructures/" + self.inf_id, headers = {'Authorization' : self.auth_data}) resp = self.server.getresponse() From cc6cf41e8fae06ffdaafcbfa03cd38a9a58a0a6c Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 10 Feb 2016 15:25:59 +0100 Subject: [PATCH 137/509] Add removed docker-devel dir --- docker-devel/Dockerfile | 43 ++++++++++++++++++++++++++++++++++++++++ docker-devel/ansible.cfg | 17 ++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 docker-devel/Dockerfile create mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile new file mode 100644 index 000000000..2c14fcd0a --- /dev/null +++ b/docker-devel/Dockerfile @@ -0,0 +1,43 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.4.2" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + python-mock \ + python-nose \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install tosca-parser +RUN cd tmp \ + && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +# Install im indigo tosca fork branch 'devel' +RUN cd tmp \ + && git clone --branch devel --recursive https://github.com/indigo-dc/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +# Expose the IM ports +EXPOSE 8899 8800 + +# Launch the service at the beginning of the container +CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg new file mode 100644 index 000000000..2ae9bcc53 --- /dev/null +++ b/docker-devel/ansible.cfg @@ -0,0 +1,17 @@ +[defaults] +transport = smart +host_key_checking = False +become_user = root +become_method = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True From c1dcbc53d841a8d27ffc276a00ba84f5f12091e8 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 10 Feb 2016 15:25:59 +0100 Subject: [PATCH 138/509] Add removed docker-devel dir --- docker-devel/Dockerfile | 43 ++++++++++++++++++++++++++++++++++++++++ docker-devel/ansible.cfg | 17 ++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 docker-devel/Dockerfile create mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile new file mode 100644 index 000000000..2c14fcd0a --- /dev/null +++ b/docker-devel/Dockerfile @@ -0,0 +1,43 @@ +# Dockerfile to create a container with the IM service and TOSCA support +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.4.2" +LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" + +# Update and install all the neccesary packages +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-pbr \ + python-dateutil \ + python-mock \ + python-nose \ + openssh-client \ + sshpass \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install tosca-parser +RUN cd tmp \ + && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ + && cd tosca-parser \ + && python setup.py install + +# Install im indigo tosca fork branch 'devel' +RUN cd tmp \ + && git clone --branch devel --recursive https://github.com/indigo-dc/im.git \ + && cd im \ + && python setup.py install +COPY ansible.cfg /etc/ansible/ansible.cfg + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +# Expose the IM ports +EXPOSE 8899 8800 + +# Launch the service at the beginning of the container +CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg new file mode 100644 index 000000000..2ae9bcc53 --- /dev/null +++ b/docker-devel/ansible.cfg @@ -0,0 +1,17 @@ +[defaults] +transport = smart +host_key_checking = False +become_user = root +become_method = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True From 9dd5ceab53ddddc735ddc2cbb750caa11d269753 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 12 Feb 2016 13:24:13 +0100 Subject: [PATCH 139/509] Minor change to avoid recontextualize if no VMs are added --- IM/InfrastructureManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index d0cdbcd74..8e808c735 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -535,7 +535,7 @@ def AddResource(inf_id, radl_data, auth, context = True, failed_clouds = []): InfrastructureManager.logger.info("VMs %s successfully added to Inf id %s" % (new_vms, sel_inf.id)) # Let's contextualize! - if context: + if context and new_vms: sel_inf.Contextualize(auth) return [vm.im_id for vm in new_vms] From 7a488ce8b77085fd29328dea72320345a930b875 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 12 Feb 2016 13:24:13 +0100 Subject: [PATCH 140/509] Minor change to avoid recontextualize if no VMs are added --- IM/InfrastructureManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index d0cdbcd74..8e808c735 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -535,7 +535,7 @@ def AddResource(inf_id, radl_data, auth, context = True, failed_clouds = []): InfrastructureManager.logger.info("VMs %s successfully added to Inf id %s" % (new_vms, sel_inf.id)) # Let's contextualize! - if context: + if context and new_vms: sel_inf.Contextualize(auth) return [vm.im_id for vm in new_vms] From 9e70258edd4926e452552e9b509e6031ad37e7c5 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 12 Feb 2016 13:24:40 +0100 Subject: [PATCH 141/509] Update Dockerfile version --- docker-devel/Dockerfile | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 2c14fcd0a..1e7ae4ca3 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.4.2" +LABEL version="1.4.3" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages diff --git a/docker/Dockerfile b/docker/Dockerfile index eb8c8e812..ab0cfa75b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.4.2" +LABEL version="1.4.3" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages From d2c8a024f74ab163391c5ba7175f17bd933e83cc Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 12 Feb 2016 13:24:40 +0100 Subject: [PATCH 142/509] Update Dockerfile version --- docker-devel/Dockerfile | 2 +- docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 2c14fcd0a..1e7ae4ca3 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.4.2" +LABEL version="1.4.3" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages diff --git a/docker/Dockerfile b/docker/Dockerfile index eb8c8e812..ab0cfa75b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:14.04 MAINTAINER Miguel Caballer -LABEL version="1.4.2" +LABEL version="1.4.3" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Update and install all the neccesary packages From 049539a8505a295bcf7bff353de487f6edd28eaf Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 12 Feb 2016 13:27:03 +0100 Subject: [PATCH 143/509] Update Tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index b39eaf456..e501b4cbe 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit b39eaf4560c3b986bda2a6ccc99a4659143953a4 +Subproject commit e501b4cbee78a8ad9a340c4a357507073e53f0d3 From e5f8878c2597afb4a22e1579ccc14e0bd13afd4e Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 12 Feb 2016 13:27:03 +0100 Subject: [PATCH 144/509] Update Tosca-types --- IM/tosca/tosca-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index b39eaf456..e501b4cbe 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit b39eaf4560c3b986bda2a6ccc99a4659143953a4 +Subproject commit e501b4cbee78a8ad9a340c4a357507073e53f0d3 From 86e1e9708cc98c08cbcfdbb632dfa254601011b9 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 15 Feb 2016 15:39:19 +0100 Subject: [PATCH 145/509] Update Tosca to use imports in all the examples --- IM/tosca/Tosca.py | 47 +------------------------------------------- IM/tosca/tosca-types | 2 +- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a95b17506..2c1862856 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -6,55 +6,10 @@ import urllib from IM.uriparse import uriparse -import toscaparser.imports from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef -from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from toscaparser.utils.yamlparser import load_yaml - -class IndigoToscaTemplate(ToscaTemplate): - - CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/custom_types.yaml" - - def __init__(self, path, parsed_params=None, a_file=True): - # Load custom data - custom_def = load_yaml(self.CUSTOM_TYPES_FILE) - # and update tosca_def with the data - EntityType.TOSCA_DEF.update(custom_def) - - super(IndigoToscaTemplate, self).__init__(path, parsed_params, a_file) - - def _get_custom_types(self, type_definitions, imports=None): - """Handle custom types defined in imported template files - - This method loads the custom type definitions referenced in "imports" - section of the TOSCA YAML template. - """ - - custom_defs = {} - type_defs = [] - if not isinstance(type_definitions, list): - type_defs.append(type_definitions) - else: - type_defs = type_definitions - - if not imports: - imports = self._tpl_imports() - - if imports: - custom_defs = toscaparser.imports.\ - ImportsLoader(imports, self.path, - type_defs).get_custom_defs() - - # Handle custom types defined in current template file - for type_def in type_defs: - if type_def != "imports": - inner_custom_types = self.tpl.get(type_def) or {} - if inner_custom_types: - custom_defs.update(inner_custom_types) - return custom_defs class Tosca: """ @@ -75,7 +30,7 @@ def __init__(self, yaml_str): with tempfile.NamedTemporaryFile(suffix=".yaml") as f: f.write(yaml_str) f.flush() - self.tosca = IndigoToscaTemplate(f.name) + self.tosca = ToscaTemplate(f.name) def to_radl(self, inf_info = None): """ diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index e501b4cbe..59101568c 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit e501b4cbee78a8ad9a340c4a357507073e53f0d3 +Subproject commit 59101568c61fc11eebd0c437bd2922d2403349b5 From 40a967fa43fc1049a2fa6b1372759a8bbb8b49a5 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 15 Feb 2016 15:39:19 +0100 Subject: [PATCH 146/509] Update Tosca to use imports in all the examples --- IM/tosca/Tosca.py | 47 +------------------------------------------- IM/tosca/tosca-types | 2 +- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a95b17506..2c1862856 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -6,55 +6,10 @@ import urllib from IM.uriparse import uriparse -import toscaparser.imports from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef -from toscaparser.elements.entity_type import EntityType from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from toscaparser.utils.yamlparser import load_yaml - -class IndigoToscaTemplate(ToscaTemplate): - - CUSTOM_TYPES_FILE = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/custom_types.yaml" - - def __init__(self, path, parsed_params=None, a_file=True): - # Load custom data - custom_def = load_yaml(self.CUSTOM_TYPES_FILE) - # and update tosca_def with the data - EntityType.TOSCA_DEF.update(custom_def) - - super(IndigoToscaTemplate, self).__init__(path, parsed_params, a_file) - - def _get_custom_types(self, type_definitions, imports=None): - """Handle custom types defined in imported template files - - This method loads the custom type definitions referenced in "imports" - section of the TOSCA YAML template. - """ - - custom_defs = {} - type_defs = [] - if not isinstance(type_definitions, list): - type_defs.append(type_definitions) - else: - type_defs = type_definitions - - if not imports: - imports = self._tpl_imports() - - if imports: - custom_defs = toscaparser.imports.\ - ImportsLoader(imports, self.path, - type_defs).get_custom_defs() - - # Handle custom types defined in current template file - for type_def in type_defs: - if type_def != "imports": - inner_custom_types = self.tpl.get(type_def) or {} - if inner_custom_types: - custom_defs.update(inner_custom_types) - return custom_defs class Tosca: """ @@ -75,7 +30,7 @@ def __init__(self, yaml_str): with tempfile.NamedTemporaryFile(suffix=".yaml") as f: f.write(yaml_str) f.flush() - self.tosca = IndigoToscaTemplate(f.name) + self.tosca = ToscaTemplate(f.name) def to_radl(self, inf_info = None): """ diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types index e501b4cbe..59101568c 160000 --- a/IM/tosca/tosca-types +++ b/IM/tosca/tosca-types @@ -1 +1 @@ -Subproject commit e501b4cbee78a8ad9a340c4a357507073e53f0d3 +Subproject commit 59101568c61fc11eebd0c437bd2922d2403349b5 From 6bfe0bd296071f5cab0aa236e9f6c124cc3843a0 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 15 Feb 2016 15:52:32 +0100 Subject: [PATCH 147/509] Update Tosca tests --- test/TestREST.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/TestREST.py b/test/TestREST.py index 91a4c8fc7..7d7036f9b 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -353,6 +353,15 @@ def test_93_create_tosca(self): description: TOSCA test for the IM +repositories: + indigo_repository: + description: INDIGO Custom types repository + url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ + +imports: + - indigo_custom_types: + file: custom_types.yaml + repository: indigo_repository topology_template: inputs: @@ -474,6 +483,15 @@ def test_95_add_tosca(self): description: TOSCA test for the IM +repositories: + indigo_repository: + description: INDIGO Custom types repository + url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ + +imports: + - indigo_custom_types: + file: custom_types.yaml + repository: indigo_repository topology_template: inputs: From 90903350c6221a04241d2211a348a14bb3df8784 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 15 Feb 2016 15:52:32 +0100 Subject: [PATCH 148/509] Update Tosca tests --- test/TestREST.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/TestREST.py b/test/TestREST.py index 91a4c8fc7..7d7036f9b 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -353,6 +353,15 @@ def test_93_create_tosca(self): description: TOSCA test for the IM +repositories: + indigo_repository: + description: INDIGO Custom types repository + url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ + +imports: + - indigo_custom_types: + file: custom_types.yaml + repository: indigo_repository topology_template: inputs: @@ -474,6 +483,15 @@ def test_95_add_tosca(self): description: TOSCA test for the IM +repositories: + indigo_repository: + description: INDIGO Custom types repository + url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ + +imports: + - indigo_custom_types: + file: custom_types.yaml + repository: indigo_repository topology_template: inputs: From bc49e35a2c8daaa309debd0462457ec1dd0ffa95 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 16 Feb 2016 11:42:42 +0100 Subject: [PATCH 149/509] Remove custom types --- .gitmodules | 3 --- IM/tosca/tosca-types | 1 - MANIFEST.in | 1 - 3 files changed, 5 deletions(-) delete mode 160000 IM/tosca/tosca-types diff --git a/.gitmodules b/.gitmodules index e21c15d9e..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "IM/tosca/tosca-types"] - path = IM/tosca/tosca-types - url = https://github.com/indigo-dc/tosca-types diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types deleted file mode 160000 index 59101568c..000000000 --- a/IM/tosca/tosca-types +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 59101568c61fc11eebd0c437bd2922d2403349b5 diff --git a/MANIFEST.in b/MANIFEST.in index 7abecfd62..290085366 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ recursive-exclude test * recursive-include contextualization * -include IM/tosca/tosca-types/custom_types.yaml include scripts/im include etc/im.cfg include etc/logging.conf From 47c7482326b64520e4c4d7e08969696e689c4ba5 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 16 Feb 2016 11:42:42 +0100 Subject: [PATCH 150/509] Remove custom types --- .gitmodules | 3 --- IM/tosca/tosca-types | 1 - MANIFEST.in | 1 - 3 files changed, 5 deletions(-) delete mode 160000 IM/tosca/tosca-types diff --git a/.gitmodules b/.gitmodules index e21c15d9e..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "IM/tosca/tosca-types"] - path = IM/tosca/tosca-types - url = https://github.com/indigo-dc/tosca-types diff --git a/IM/tosca/tosca-types b/IM/tosca/tosca-types deleted file mode 160000 index 59101568c..000000000 --- a/IM/tosca/tosca-types +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 59101568c61fc11eebd0c437bd2922d2403349b5 diff --git a/MANIFEST.in b/MANIFEST.in index 7abecfd62..290085366 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ recursive-exclude test * recursive-include contextualization * -include IM/tosca/tosca-types/custom_types.yaml include scripts/im include etc/im.cfg include etc/logging.conf From aa04983a0b870e096bcfbb99407346799f95a552 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 16 Feb 2016 11:43:22 +0100 Subject: [PATCH 151/509] Consider repositories in the artifact location --- IM/tosca/Tosca.py | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 2c1862856..cddcbdd77 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -10,6 +10,7 @@ from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from __builtin__ import isinstance class Tosca: """ @@ -243,6 +244,37 @@ def _get_relationships_interfaces(relationships, node): res[name] = iface return res + def _get_artifact_full_uri(self, node, artifact_name): + res = None + artifacts = node.type_definition.get_value('artifacts',node.entity_tpl,True) + if artifacts: + for name, artifact in artifacts.items(): + if name == artifact_name: + if isinstance(artifact, dict): + res = artifact['file'] + if 'repository' in artifact: + repo = artifact['repository'] + repositories = self.tosca.tpl.get('repositories') + + if repositories: + for repo_name, repo_def in repositories.items(): + if repo_name == repo: + repo_url = ((repo_def['url']).strip()).rstrip("//") + res = repo_url + "/" + artifact['file'] + else: + res = artifact + + return res + + def _get_implementation_url(self, node, implementation): + res = implementation + if implementation: + artifact_url = self._get_artifact_full_uri(node, implementation) + if artifact_url: + res = artifact_url + + return res + def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if not interfaces: return None @@ -283,7 +315,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): tasks += " - name: Download artifact " + artifact + "\n" tasks += " get_url: dest=" + remote_artifacts_path + "/" + os.path.basename(artifact) + " url='" + artifact + "'\n" - implementation_url = uriparse(interface.implementation) + implementation_url = uriparse(self._get_implementation_url(node, interface.implementation)) if implementation_url[0] in ['http', 'https', 'ftp']: script_path = implementation_url[2] @@ -365,16 +397,10 @@ def _is_artifact(function): return func_name == "get_artifact" return False - @staticmethod - def _get_artifact_uri(function, node): + def _get_artifact_uri(self, function, node): if isinstance(function, dict) and len(function) == 1: name = function["get_artifact"][1] - artifacts = node.entity_tpl.get("artifacts") - if isinstance(artifacts, dict): - for artifact_name, value in artifacts.iteritems(): - if artifact_name == name: - #return value['implementation'] - return value['file'] + return self._get_artifact_full_uri(node, name) return None From f07e3565f0186fd0e844643bd27c347717afa9f0 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 16 Feb 2016 11:43:22 +0100 Subject: [PATCH 152/509] Consider repositories in the artifact location --- IM/tosca/Tosca.py | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 2c1862856..cddcbdd77 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -10,6 +10,7 @@ from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from __builtin__ import isinstance class Tosca: """ @@ -243,6 +244,37 @@ def _get_relationships_interfaces(relationships, node): res[name] = iface return res + def _get_artifact_full_uri(self, node, artifact_name): + res = None + artifacts = node.type_definition.get_value('artifacts',node.entity_tpl,True) + if artifacts: + for name, artifact in artifacts.items(): + if name == artifact_name: + if isinstance(artifact, dict): + res = artifact['file'] + if 'repository' in artifact: + repo = artifact['repository'] + repositories = self.tosca.tpl.get('repositories') + + if repositories: + for repo_name, repo_def in repositories.items(): + if repo_name == repo: + repo_url = ((repo_def['url']).strip()).rstrip("//") + res = repo_url + "/" + artifact['file'] + else: + res = artifact + + return res + + def _get_implementation_url(self, node, implementation): + res = implementation + if implementation: + artifact_url = self._get_artifact_full_uri(node, implementation) + if artifact_url: + res = artifact_url + + return res + def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if not interfaces: return None @@ -283,7 +315,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): tasks += " - name: Download artifact " + artifact + "\n" tasks += " get_url: dest=" + remote_artifacts_path + "/" + os.path.basename(artifact) + " url='" + artifact + "'\n" - implementation_url = uriparse(interface.implementation) + implementation_url = uriparse(self._get_implementation_url(node, interface.implementation)) if implementation_url[0] in ['http', 'https', 'ftp']: script_path = implementation_url[2] @@ -365,16 +397,10 @@ def _is_artifact(function): return func_name == "get_artifact" return False - @staticmethod - def _get_artifact_uri(function, node): + def _get_artifact_uri(self, function, node): if isinstance(function, dict) and len(function) == 1: name = function["get_artifact"][1] - artifacts = node.entity_tpl.get("artifacts") - if isinstance(artifacts, dict): - for artifact_name, value in artifacts.iteritems(): - if artifact_name == name: - #return value['implementation'] - return value['file'] + return self._get_artifact_full_uri(node, name) return None From b43714e6166ae32bbbf8431bd31e4bc48cfc382e Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 16 Feb 2016 11:45:05 +0100 Subject: [PATCH 153/509] Remove unnecessary import --- IM/tosca/Tosca.py | 1 - 1 file changed, 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index cddcbdd77..108e2ae9e 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -10,7 +10,6 @@ from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from __builtin__ import isinstance class Tosca: """ From 7a105244c9e9453e6b351d700c182fd3fe45cbca Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 16 Feb 2016 11:45:05 +0100 Subject: [PATCH 154/509] Remove unnecessary import --- IM/tosca/Tosca.py | 1 - 1 file changed, 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index cddcbdd77..108e2ae9e 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -10,7 +10,6 @@ from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize -from __builtin__ import isinstance class Tosca: """ From 1362c80701cc913d7907c30c2e1f6fea95ef9198 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 17 Feb 2016 14:58:41 +0100 Subject: [PATCH 155/509] Bugfixes --- IM/tosca/Tosca.py | 53 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 108e2ae9e..fa6f9ad01 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -343,7 +343,10 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: for var_name, var_value in env.iteritems(): - variables += ' %s: "%s" ' % (var_name, var_value) + "\n" + if var_value.startswith("|"): + variables += ' %s: %s ' % (var_name, var_value) + "\n" + else: + variables += ' %s: "%s" ' % (var_name, var_value) + "\n" variables += "\n" recipe_list.append(script_content) @@ -499,11 +502,17 @@ def _get_attribute_result(self, func, node, inf_info): if node_name == "HOST": node = self._find_host_compute(node, self.tosca.nodetemplates) - else: + elif node_name != "SELF": + node = None for n in self.tosca.nodetemplates: if n.name == node_name: node = n break + if not node: + Tosca.logger.error("Calling get_attribute function for non existing node: %s" % node_name) + return None + + root_type = Tosca._get_root_parent_type(node).type if inf_info: vm_list = inf_info.get_vm_list_by_system_name() @@ -513,6 +522,7 @@ def _get_attribute_result(self, func, node, inf_info): return None else: # Always assume that there will be only one VM per group + # TODO: this is not true!! vm = vm_list[node.name][0] if attribute_name == "tosca_id": @@ -520,11 +530,16 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "tosca_name": return node.name elif attribute_name == "private_address": - return vm.getPrivateIP() + if node.type == "tosca.nodes.indigo.Compute": + return [vm.getPrivateIP() for vm in vm_list[node.name]] + else: + return vm.getPrivateIP() elif attribute_name == "public_address": - return vm.getPublicIP() + if node.type == "tosca.nodes.indigo.Compute": + return [vm.getPublicIP() for vm in vm_list[node.name]] + else: + return vm.getPublicIP() elif attribute_name == "ip_address": - root_type = Tosca._get_root_parent_type(node).type if root_type == "tosca.nodes.network.Port": order = node.get_property_value('order') return vm.getNumNetworkWithConnection(order) @@ -548,18 +563,26 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "tosca_name": return node.name elif attribute_name == "private_address": - # TODO: we suppose that iface 1 is the private one - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_PRIVATE_IP }}" + if node.type == "tosca.nodes.indigo.Compute": + # This only works with Ansible 2.1, wait for it to be released + #return "{{ groups['%s']|map('extract', hostvars, 'IM_NODE_PRIVATE_IP')|list }}" % node.name + return """|\n {%% set comma = joiner(",") %%}\n [ {%% for host in groups['%s'] %%}\n {{ comma() }}"{{ hostvars[host]['IM_NODE_PRIVATE_IP'] }}"\n {%% endfor %%} ]""" % node.name else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_PRIVATE_IP'] }}" % node.name + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PRIVATE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PRIVATE_IP'] }}" % node.name elif attribute_name == "public_address": - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_PUBLIC_IP }}" + if node.type == "tosca.nodes.indigo.Compute": + # This only works with Ansible 2.1, wait for it to be released + #return "{{ groups['%s']|map('extract', hostvars, 'IM_NODE_PUBLIC_IP')|list }}" % node.name + return """|\n {%% set comma = joiner(",") %%}\n [ {%% for host in groups['%s'] %%}\n {{ comma() }}"{{ hostvars[host]['IM_NODE_PUBLIC_IP'] }}"\n {%% endfor %%} ]""" % node.name else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PUBLIC_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name elif attribute_name == "ip_address": - root_type = Tosca._get_root_parent_type(node).type if root_type == "tosca.nodes.network.Port": order = node.get_property_value('order') return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) @@ -585,10 +608,10 @@ def _final_function_result(self, func, node, inf_info=None): if is_function(func): func = get_function(self.tosca, node, func) - if isinstance(func, Function): + while isinstance(func, Function): if isinstance(func, GetAttribute): func = self._get_attribute_result(func, node, inf_info) - while isinstance(func, Function): + else: func = func.result() if isinstance(func, dict): From dbd12224b06897bdfd9ee84cac4ed6ea6dfb11c7 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 17 Feb 2016 14:58:41 +0100 Subject: [PATCH 156/509] Bugfixes --- IM/tosca/Tosca.py | 53 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 108e2ae9e..fa6f9ad01 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -343,7 +343,10 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: for var_name, var_value in env.iteritems(): - variables += ' %s: "%s" ' % (var_name, var_value) + "\n" + if var_value.startswith("|"): + variables += ' %s: %s ' % (var_name, var_value) + "\n" + else: + variables += ' %s: "%s" ' % (var_name, var_value) + "\n" variables += "\n" recipe_list.append(script_content) @@ -499,11 +502,17 @@ def _get_attribute_result(self, func, node, inf_info): if node_name == "HOST": node = self._find_host_compute(node, self.tosca.nodetemplates) - else: + elif node_name != "SELF": + node = None for n in self.tosca.nodetemplates: if n.name == node_name: node = n break + if not node: + Tosca.logger.error("Calling get_attribute function for non existing node: %s" % node_name) + return None + + root_type = Tosca._get_root_parent_type(node).type if inf_info: vm_list = inf_info.get_vm_list_by_system_name() @@ -513,6 +522,7 @@ def _get_attribute_result(self, func, node, inf_info): return None else: # Always assume that there will be only one VM per group + # TODO: this is not true!! vm = vm_list[node.name][0] if attribute_name == "tosca_id": @@ -520,11 +530,16 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "tosca_name": return node.name elif attribute_name == "private_address": - return vm.getPrivateIP() + if node.type == "tosca.nodes.indigo.Compute": + return [vm.getPrivateIP() for vm in vm_list[node.name]] + else: + return vm.getPrivateIP() elif attribute_name == "public_address": - return vm.getPublicIP() + if node.type == "tosca.nodes.indigo.Compute": + return [vm.getPublicIP() for vm in vm_list[node.name]] + else: + return vm.getPublicIP() elif attribute_name == "ip_address": - root_type = Tosca._get_root_parent_type(node).type if root_type == "tosca.nodes.network.Port": order = node.get_property_value('order') return vm.getNumNetworkWithConnection(order) @@ -548,18 +563,26 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "tosca_name": return node.name elif attribute_name == "private_address": - # TODO: we suppose that iface 1 is the private one - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_PRIVATE_IP }}" + if node.type == "tosca.nodes.indigo.Compute": + # This only works with Ansible 2.1, wait for it to be released + #return "{{ groups['%s']|map('extract', hostvars, 'IM_NODE_PRIVATE_IP')|list }}" % node.name + return """|\n {%% set comma = joiner(",") %%}\n [ {%% for host in groups['%s'] %%}\n {{ comma() }}"{{ hostvars[host]['IM_NODE_PRIVATE_IP'] }}"\n {%% endfor %%} ]""" % node.name else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_PRIVATE_IP'] }}" % node.name + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PRIVATE_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PRIVATE_IP'] }}" % node.name elif attribute_name == "public_address": - if node_name in ["HOST", "SELF"]: - return "{{ IM_NODE_PUBLIC_IP }}" + if node.type == "tosca.nodes.indigo.Compute": + # This only works with Ansible 2.1, wait for it to be released + #return "{{ groups['%s']|map('extract', hostvars, 'IM_NODE_PUBLIC_IP')|list }}" % node.name + return """|\n {%% set comma = joiner(",") %%}\n [ {%% for host in groups['%s'] %%}\n {{ comma() }}"{{ hostvars[host]['IM_NODE_PUBLIC_IP'] }}"\n {%% endfor %%} ]""" % node.name else: - return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name + if node_name in ["HOST", "SELF"]: + return "{{ IM_NODE_PUBLIC_IP }}" + else: + return "{{ hostvars[groups['%s'][0]]['IM_NODE_PUBLIC_IP'] }}" % node.name elif attribute_name == "ip_address": - root_type = Tosca._get_root_parent_type(node).type if root_type == "tosca.nodes.network.Port": order = node.get_property_value('order') return "{{ hostvars[groups['%s'][0]]['IM_NODE_NET_%s_IP'] }}" % (node.name, order) @@ -585,10 +608,10 @@ def _final_function_result(self, func, node, inf_info=None): if is_function(func): func = get_function(self.tosca, node, func) - if isinstance(func, Function): + while isinstance(func, Function): if isinstance(func, GetAttribute): func = self._get_attribute_result(func, node, inf_info) - while isinstance(func, Function): + else: func = func.result() if isinstance(func, dict): From e313f9a4b6a3cebf2b2f69d42093b0e8d0bfdd62 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 17 Feb 2016 16:26:00 +0100 Subject: [PATCH 157/509] Bugfix --- IM/tosca/Tosca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index fa6f9ad01..78dd0d714 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -302,7 +302,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): val = self._final_function_result(param_value, node) if val: - env[param_name] = val + env[param_name] = str(val) else: raise Exception("input value for %s in interface %s of node %s not valid" % (param_name, name, node.name)) From 5dbad3a445748084b5435fee449a539482cb2660 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 17 Feb 2016 16:26:00 +0100 Subject: [PATCH 158/509] Bugfix --- IM/tosca/Tosca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index fa6f9ad01..78dd0d714 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -302,7 +302,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): val = self._final_function_result(param_value, node) if val: - env[param_name] = val + env[param_name] = str(val) else: raise Exception("input value for %s in interface %s of node %s not valid" % (param_name, name, node.name)) From f0d40d8f2ebe882df0b0833420ac34227f89e770 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 09:21:21 +0100 Subject: [PATCH 159/509] Update test to new custom_types definition --- test/TestREST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index 7d7036f9b..f8d768e9f 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -452,7 +452,7 @@ def test_93_create_tosca(self): outputs: server_url: - value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } + value: { get_attribute: [ web_server, public_address ] } """ self.server.request('POST', "/infrastructures", body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) @@ -472,7 +472,7 @@ def test_94_get_outputs(self): self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) res = json.loads(output) server_url = res['server_url'] - self.assertRegexpMatches(server_url, 'http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', msg="Unexpected outputs: " + output) + self.assertRegexpMatches(server_url, '\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]', msg="Unexpected outputs: " + output) def test_95_add_tosca(self): """ @@ -585,7 +585,7 @@ def test_95_add_tosca(self): outputs: server_url: - value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } + value: { get_attribute: [ web_server, public_address ] } """ self.server.request('POST', "/infrastructures/" + self.inf_id, body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) From 5ef880bd54cadeebb11d94dc5d1b47ae866566bf Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 09:21:21 +0100 Subject: [PATCH 160/509] Update test to new custom_types definition --- test/TestREST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index 7d7036f9b..f8d768e9f 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -452,7 +452,7 @@ def test_93_create_tosca(self): outputs: server_url: - value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } + value: { get_attribute: [ web_server, public_address ] } """ self.server.request('POST', "/infrastructures", body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) @@ -472,7 +472,7 @@ def test_94_get_outputs(self): self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) res = json.loads(output) server_url = res['server_url'] - self.assertRegexpMatches(server_url, 'http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', msg="Unexpected outputs: " + output) + self.assertRegexpMatches(server_url, '\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]', msg="Unexpected outputs: " + output) def test_95_add_tosca(self): """ @@ -585,7 +585,7 @@ def test_95_add_tosca(self): outputs: server_url: - value: { concat: [ 'http://', get_attribute: [ web_server, public_address ], '/' ] } + value: { get_attribute: [ web_server, public_address ] } """ self.server.request('POST', "/infrastructures/" + self.inf_id, body = tosca, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'text/yaml'}) From e01e0aea08f35ca1db57a42cb3d6ea24791f7f1f Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 09:57:35 +0100 Subject: [PATCH 161/509] Update test to new custom_types definition --- test/TestREST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TestREST.py b/test/TestREST.py index f8d768e9f..4b99c9727 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -471,7 +471,7 @@ def test_94_get_outputs(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) res = json.loads(output) - server_url = res['server_url'] + server_url = str(res['server_url']) self.assertRegexpMatches(server_url, '\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]', msg="Unexpected outputs: " + output) def test_95_add_tosca(self): From e8422442bd434708da8812bc9b9c4a527984c5af Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 09:57:35 +0100 Subject: [PATCH 162/509] Update test to new custom_types definition --- test/TestREST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TestREST.py b/test/TestREST.py index f8d768e9f..4b99c9727 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -471,7 +471,7 @@ def test_94_get_outputs(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) res = json.loads(output) - server_url = res['server_url'] + server_url = str(res['server_url']) self.assertRegexpMatches(server_url, '\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]', msg="Unexpected outputs: " + output) def test_95_add_tosca(self): From 5c0002e4b34633df35396fc5188eb2e00bc18a22 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 10:31:36 +0100 Subject: [PATCH 163/509] Update test to new custom_types definition --- test/TestREST.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index 4b99c9727..b139c2640 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -471,8 +471,8 @@ def test_94_get_outputs(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) res = json.loads(output) - server_url = str(res['server_url']) - self.assertRegexpMatches(server_url, '\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]', msg="Unexpected outputs: " + output) + server_url = str(res['server_url'][0]) + self.assertRegexpMatches(server_url, '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', msg="Unexpected outputs: " + output) def test_95_add_tosca(self): """ From 55ee66041af440321fd7daaf0780b7054686e59e Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 10:31:36 +0100 Subject: [PATCH 164/509] Update test to new custom_types definition --- test/TestREST.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index 4b99c9727..b139c2640 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -471,8 +471,8 @@ def test_94_get_outputs(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) res = json.loads(output) - server_url = str(res['server_url']) - self.assertRegexpMatches(server_url, '\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]', msg="Unexpected outputs: " + output) + server_url = str(res['server_url'][0]) + self.assertRegexpMatches(server_url, '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', msg="Unexpected outputs: " + output) def test_95_add_tosca(self): """ From a2a5365380292e24adeca993905e3d3f44d3b91b Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 16:04:32 +0100 Subject: [PATCH 165/509] Add support for the removal_list and add the test --- IM/REST.py | 8 ++- IM/tosca/Tosca.py | 39 +++++++++++--- test/TestREST.py | 129 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 10 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 63f8af8ac..a413da953 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -245,7 +245,7 @@ def RESTCreateInfrastructure(): radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": tosca_data = radl_data - radl_data = Tosca(radl_data).to_radl() + _, radl_data = Tosca(radl_data).to_radl() elif content_type in ["text/plain","*/*","text/*"]: content_type = "text/plain" else: @@ -386,6 +386,7 @@ def RESTAddResource(id=None): content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() tosca_data = None + remove_list = [] if content_type: if content_type == "application/json": @@ -393,13 +394,16 @@ def RESTAddResource(id=None): elif content_type == "text/yaml": tosca_data = radl_data sel_inf = InfrastructureManager.get_infrastructure(id, auth) - radl_data = Tosca(radl_data).to_radl(sel_inf) + remove_list, radl_data = Tosca(radl_data).to_radl(sel_inf) elif content_type in ["text/plain","*/*","text/*"]: content_type = "text/plain" else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False + if remove_list: + InfrastructureManager.RemoveResource(id, remove_list, auth, context) + vm_ids = InfrastructureManager.AddResource(id, radl_data, auth, context) # Replace the TOSCA document diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 78dd0d714..3c7b8ada9 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -18,7 +18,7 @@ class Tosca: TODO: What about CSAR files? """ - + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/artifacts" ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" @@ -40,6 +40,7 @@ def to_radl(self, inf_info = None): number of nodes to deploy """ + all_removal_list = [] relationships = [] for node in self.tosca.nodetemplates: # Store relationships to check later @@ -72,7 +73,7 @@ def to_radl(self, inf_info = None): Tosca._add_node_nets(node, radl, sys, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system - min_instances, _, default_instances, count = Tosca._get_scalable_properties(node) + min_instances, _, default_instances, count, removal_list = Tosca._get_scalable_properties(node) if count is not None: # we must check the correct number of instances to deploy num_instances = count @@ -85,6 +86,10 @@ def to_radl(self, inf_info = None): num_instances = num_instances - self._get_num_instances(sys.name, inf_info) + # TODO: Think about to check the IDs of the VMs + if num_instances < 0: + all_removal_list.extend(removal_list[0:-num_instances]) + if num_instances > 0: dep = deploy(sys.name, num_instances) radl.deploys.append(dep) @@ -108,7 +113,7 @@ def to_radl(self, inf_info = None): if cont_intems: radl.contextualize = contextualize(cont_intems) - return self._complete_radl_networks(radl) + return all_removal_list, self._complete_radl_networks(radl) def _get_num_instances(self, sys_name, inf_info): """ @@ -203,20 +208,23 @@ def _add_node_nets(node, radl, system, nodetemplates): @staticmethod def _get_scalable_properties(node): count = min_instances = max_instances = default_instances = None + removal_list = [] scalable = node.get_capability("scalable") if scalable: for prop in scalable.get_properties_objects(): if prop.value is not None: if prop.name == "count": count = prop.value - if prop.name == "max_instances": + elif prop.name == "max_instances": max_instances = prop.value elif prop.name == "min_instances": min_instances = prop.value elif prop.name == "default_instances": default_instances = prop.value + elif prop.name == "removal_list": + removal_list = prop.value - return min_instances, max_instances, default_instances, count + return min_instances, max_instances, default_instances, count, removal_list @staticmethod def _get_relationship_template(rel, src, trgt): @@ -499,6 +507,14 @@ def _get_attribute_result(self, func, node, inf_info): """ node_name = func.args[0] attribute_name = func.args[1] + # TODO: Currently only supports indexes + index = None + if len(func.args) > 2: + try: + index = int(func.args[2]) + except: + Tosca.logger.exception("Error getting get_attribute index.") + pass if node_name == "HOST": node = self._find_host_compute(node, self.tosca.nodetemplates) @@ -522,8 +538,9 @@ def _get_attribute_result(self, func, node, inf_info): return None else: # Always assume that there will be only one VM per group - # TODO: this is not true!! vm = vm_list[node.name][0] + if len(vm_list[node.name]) Date: Thu, 18 Feb 2016 16:04:32 +0100 Subject: [PATCH 166/509] Add support for the removal_list and add the test --- IM/REST.py | 8 ++- IM/tosca/Tosca.py | 39 +++++++++++--- test/TestREST.py | 129 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 10 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 63f8af8ac..a413da953 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -245,7 +245,7 @@ def RESTCreateInfrastructure(): radl_data = parse_radl_json(radl_data) elif content_type == "text/yaml": tosca_data = radl_data - radl_data = Tosca(radl_data).to_radl() + _, radl_data = Tosca(radl_data).to_radl() elif content_type in ["text/plain","*/*","text/*"]: content_type = "text/plain" else: @@ -386,6 +386,7 @@ def RESTAddResource(id=None): content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() tosca_data = None + remove_list = [] if content_type: if content_type == "application/json": @@ -393,13 +394,16 @@ def RESTAddResource(id=None): elif content_type == "text/yaml": tosca_data = radl_data sel_inf = InfrastructureManager.get_infrastructure(id, auth) - radl_data = Tosca(radl_data).to_radl(sel_inf) + remove_list, radl_data = Tosca(radl_data).to_radl(sel_inf) elif content_type in ["text/plain","*/*","text/*"]: content_type = "text/plain" else: bottle.abort(415, "Unsupported Media Type %s" % content_type) return False + if remove_list: + InfrastructureManager.RemoveResource(id, remove_list, auth, context) + vm_ids = InfrastructureManager.AddResource(id, radl_data, auth, context) # Replace the TOSCA document diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 78dd0d714..3c7b8ada9 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -18,7 +18,7 @@ class Tosca: TODO: What about CSAR files? """ - + ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/artifacts" ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" @@ -40,6 +40,7 @@ def to_radl(self, inf_info = None): number of nodes to deploy """ + all_removal_list = [] relationships = [] for node in self.tosca.nodetemplates: # Store relationships to check later @@ -72,7 +73,7 @@ def to_radl(self, inf_info = None): Tosca._add_node_nets(node, radl, sys, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system - min_instances, _, default_instances, count = Tosca._get_scalable_properties(node) + min_instances, _, default_instances, count, removal_list = Tosca._get_scalable_properties(node) if count is not None: # we must check the correct number of instances to deploy num_instances = count @@ -85,6 +86,10 @@ def to_radl(self, inf_info = None): num_instances = num_instances - self._get_num_instances(sys.name, inf_info) + # TODO: Think about to check the IDs of the VMs + if num_instances < 0: + all_removal_list.extend(removal_list[0:-num_instances]) + if num_instances > 0: dep = deploy(sys.name, num_instances) radl.deploys.append(dep) @@ -108,7 +113,7 @@ def to_radl(self, inf_info = None): if cont_intems: radl.contextualize = contextualize(cont_intems) - return self._complete_radl_networks(radl) + return all_removal_list, self._complete_radl_networks(radl) def _get_num_instances(self, sys_name, inf_info): """ @@ -203,20 +208,23 @@ def _add_node_nets(node, radl, system, nodetemplates): @staticmethod def _get_scalable_properties(node): count = min_instances = max_instances = default_instances = None + removal_list = [] scalable = node.get_capability("scalable") if scalable: for prop in scalable.get_properties_objects(): if prop.value is not None: if prop.name == "count": count = prop.value - if prop.name == "max_instances": + elif prop.name == "max_instances": max_instances = prop.value elif prop.name == "min_instances": min_instances = prop.value elif prop.name == "default_instances": default_instances = prop.value + elif prop.name == "removal_list": + removal_list = prop.value - return min_instances, max_instances, default_instances, count + return min_instances, max_instances, default_instances, count, removal_list @staticmethod def _get_relationship_template(rel, src, trgt): @@ -499,6 +507,14 @@ def _get_attribute_result(self, func, node, inf_info): """ node_name = func.args[0] attribute_name = func.args[1] + # TODO: Currently only supports indexes + index = None + if len(func.args) > 2: + try: + index = int(func.args[2]) + except: + Tosca.logger.exception("Error getting get_attribute index.") + pass if node_name == "HOST": node = self._find_host_compute(node, self.tosca.nodetemplates) @@ -522,8 +538,9 @@ def _get_attribute_result(self, func, node, inf_info): return None else: # Always assume that there will be only one VM per group - # TODO: this is not true!! vm = vm_list[node.name][0] + if len(vm_list[node.name]) Date: Thu, 18 Feb 2016 16:59:26 +0100 Subject: [PATCH 167/509] Add REST get version function and test --- IM/REST.py | 10 ++++++++++ test/TestREST.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/IM/REST.py b/IM/REST.py index a413da953..d4385a977 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -666,3 +666,13 @@ def RESTStopVM(infid=None, vmid=None, prop=None): except Exception, ex: bottle.abort(400, "Error stopping VM: " + str(ex)) return False + +@app.route('/version', method='GET') +def RESTGeVersion(): + try: + from IM import __version__ as version + bottle.response.content_type = "text/plain" + return version + except Exception, ex: + bottle.abort(400, "Error getting IM state: " + str(ex)) + return False diff --git a/test/TestREST.py b/test/TestREST.py index aec1f4bf5..bd8dc4aa4 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -29,6 +29,7 @@ from IM.VirtualMachine import VirtualMachine from IM.uriparse import uriparse from IM.radl import radl_parse +from IM import __version__ as version PID = None RADL_ADD = "network publica\nsystem front\ndeploy front 1" @@ -106,6 +107,13 @@ def wait_inf_state(self, state, timeout, incorrect_states = [], vm_ids = None): return all_ok + def test_05_version(self): + self.server.request('GET', "/version") + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting IM version:" + output) + self.assertEqual(output, version, msg="Incorrect version. Expected %s, obtained: %s" % (version, output)) + def test_10_list(self): self.server.request('GET', "/infrastructures", headers = {'AUTHORIZATION' : self.auth_data}) resp = self.server.getresponse() From dc3922fc8ec5e46e4cf8d9b09add297bdb5f5a32 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 18 Feb 2016 16:59:26 +0100 Subject: [PATCH 168/509] Add REST get version function and test --- IM/REST.py | 10 ++++++++++ test/TestREST.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/IM/REST.py b/IM/REST.py index a413da953..d4385a977 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -666,3 +666,13 @@ def RESTStopVM(infid=None, vmid=None, prop=None): except Exception, ex: bottle.abort(400, "Error stopping VM: " + str(ex)) return False + +@app.route('/version', method='GET') +def RESTGeVersion(): + try: + from IM import __version__ as version + bottle.response.content_type = "text/plain" + return version + except Exception, ex: + bottle.abort(400, "Error getting IM state: " + str(ex)) + return False diff --git a/test/TestREST.py b/test/TestREST.py index aec1f4bf5..bd8dc4aa4 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -29,6 +29,7 @@ from IM.VirtualMachine import VirtualMachine from IM.uriparse import uriparse from IM.radl import radl_parse +from IM import __version__ as version PID = None RADL_ADD = "network publica\nsystem front\ndeploy front 1" @@ -106,6 +107,13 @@ def wait_inf_state(self, state, timeout, incorrect_states = [], vm_ids = None): return all_ok + def test_05_version(self): + self.server.request('GET', "/version") + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting IM version:" + output) + self.assertEqual(output, version, msg="Incorrect version. Expected %s, obtained: %s" % (version, output)) + def test_10_list(self): self.server.request('GET', "/infrastructures", headers = {'AUTHORIZATION' : self.auth_data}) resp = self.server.getresponse() From f57fa38a404ecfac0e9018e01e920038b8df7533 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 10:38:23 +0100 Subject: [PATCH 169/509] Bugfix REST SSL --- IM/REST.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index d4385a977..074dc6f46 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -34,14 +34,14 @@ # It's almost equal to the supported cherrypy class CherryPyServer class MySSLCherryPy(bottle.ServerAdapter): def run(self, handler): - from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter + from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter from cherrypy import wsgiserver server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) self.srv = server # If cert variable is has a valid path, SSL will be used # You can set it to None to disable SSL - server.ssl_adapter = BuiltinSSLAdapter(Config.REST_SSL_CERTFILE, Config.REST_SSL_KEYFILE, Config.REST_SSL_CA_CERTS) + server.ssl_adapter = pyOpenSSLAdapter(Config.REST_SSL_CERTFILE, Config.REST_SSL_KEYFILE, Config.REST_SSL_CA_CERTS) try: server.start() finally: From d1bebbe14c2c183f7d174233308684050ee9feec Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 10:38:23 +0100 Subject: [PATCH 170/509] Bugfix REST SSL --- IM/REST.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index d4385a977..074dc6f46 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -34,14 +34,14 @@ # It's almost equal to the supported cherrypy class CherryPyServer class MySSLCherryPy(bottle.ServerAdapter): def run(self, handler): - from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter + from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter from cherrypy import wsgiserver server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) self.srv = server # If cert variable is has a valid path, SSL will be used # You can set it to None to disable SSL - server.ssl_adapter = BuiltinSSLAdapter(Config.REST_SSL_CERTFILE, Config.REST_SSL_KEYFILE, Config.REST_SSL_CA_CERTS) + server.ssl_adapter = pyOpenSSLAdapter(Config.REST_SSL_CERTFILE, Config.REST_SSL_KEYFILE, Config.REST_SSL_CA_CERTS) try: server.start() finally: From fb2ccf8867456e6deebcecd6c571a6f44ebed729 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 10:56:24 +0100 Subject: [PATCH 171/509] Add CherryPy to dockerfiles --- docker-devel/Dockerfile | 3 +++ docker/Dockerfile | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 1e7ae4ca3..b98399a92 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -20,6 +20,9 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install CherryPy to enable HTTPS in REST API +RUN pip install CherryPy + # Install tosca-parser RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ diff --git a/docker/Dockerfile b/docker/Dockerfile index ab0cfa75b..c2b8b1004 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -18,6 +18,9 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install CherryPy to enable HTTPS in REST API +RUN pip install CherryPy + # Install tosca-parser RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ From 49b8b1e27bba6f4eb0b4edccc0d38a256cd89430 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 10:56:24 +0100 Subject: [PATCH 172/509] Add CherryPy to dockerfiles --- docker-devel/Dockerfile | 3 +++ docker/Dockerfile | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 1e7ae4ca3..b98399a92 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -20,6 +20,9 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install CherryPy to enable HTTPS in REST API +RUN pip install CherryPy + # Install tosca-parser RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ diff --git a/docker/Dockerfile b/docker/Dockerfile index ab0cfa75b..c2b8b1004 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -18,6 +18,9 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install CherryPy to enable HTTPS in REST API +RUN pip install CherryPy + # Install tosca-parser RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ From 343d750381e0965cfcf97edec1c9011dfcbfb8bc Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 12:01:54 +0100 Subject: [PATCH 173/509] Add pyOpenSSL to dockerfiles --- docker-devel/Dockerfile | 4 +++- docker/Dockerfile | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index b98399a92..9ec91eb10 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -17,11 +17,13 @@ RUN apt-get update && apt-get install -y \ openssh-client \ sshpass \ git \ + libssl-dev \ + libffi-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Install CherryPy to enable HTTPS in REST API -RUN pip install CherryPy +RUN pip install CherryPy pyOpenSSL # Install tosca-parser RUN cd tmp \ diff --git a/docker/Dockerfile b/docker/Dockerfile index c2b8b1004..2ced65ef3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,11 +15,13 @@ RUN apt-get update && apt-get install -y \ openssh-client \ sshpass \ git \ + libssl-dev \ + libffi-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Install CherryPy to enable HTTPS in REST API -RUN pip install CherryPy +RUN pip install CherryPy pyOpenSSL # Install tosca-parser RUN cd tmp \ From 2d3c2797ee138670f7403afe06d32871fda4b410 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 12:01:54 +0100 Subject: [PATCH 174/509] Add pyOpenSSL to dockerfiles --- docker-devel/Dockerfile | 4 +++- docker/Dockerfile | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index b98399a92..9ec91eb10 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -17,11 +17,13 @@ RUN apt-get update && apt-get install -y \ openssh-client \ sshpass \ git \ + libssl-dev \ + libffi-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Install CherryPy to enable HTTPS in REST API -RUN pip install CherryPy +RUN pip install CherryPy pyOpenSSL # Install tosca-parser RUN cd tmp \ diff --git a/docker/Dockerfile b/docker/Dockerfile index c2b8b1004..2ced65ef3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,11 +15,13 @@ RUN apt-get update && apt-get install -y \ openssh-client \ sshpass \ git \ + libssl-dev \ + libffi-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Install CherryPy to enable HTTPS in REST API -RUN pip install CherryPy +RUN pip install CherryPy pyOpenSSL # Install tosca-parser RUN cd tmp \ From 338819c7d7395535154e7d934d6cc1eaa058a778 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 12:30:55 +0100 Subject: [PATCH 175/509] Bugfix returning protocol https in REST calls --- IM/REST.py | 20 ++++++++++++++++---- README | 2 +- README.md | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 074dc6f46..4b29e3fb5 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -145,10 +145,13 @@ def RESTGetInfrastructureInfo(id=None): vm_ids = InfrastructureManager.GetInfrastructureInfo(id, auth) res = "" + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" for vm_id in vm_ids: if res: res += "\n" - res += 'http://' + bottle.request.environ['HTTP_HOST'] + '/infrastructures/' + str(id) + '/vms/' + str(vm_id) + res += protocol + bottle.request.environ['HTTP_HOST'] + '/infrastructures/' + str(id) + '/vms/' + str(vm_id) bottle.response.content_type = "text/uri-list" return res @@ -213,9 +216,12 @@ def RESTGetInfrastructureList(): try: inf_ids = InfrastructureManager.GetInfrastructureList(auth) + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" res = "" for inf_id in inf_ids: - res += "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + "\n" + res += protocol + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + "\n" bottle.response.content_type = "text/uri-list" return res @@ -260,7 +266,10 @@ def RESTCreateInfrastructure(): sel_inf.extra_info['TOSCA'] = tosca_data bottle.response.content_type = "text/uri-list" - return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" + return protocol + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) except HTTPError, ex: raise ex except UnauthorizedUserException, ex: @@ -411,11 +420,14 @@ def RESTAddResource(id=None): sel_inf = InfrastructureManager.get_infrastructure(id, auth) sel_inf.extra_info['TOSCA'] = tosca_data + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" res = "" for vm_id in vm_ids: if res: res += "\n" - res += "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(id) + "/vms/" + str(vm_id) + res += protocol + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(id) + "/vms/" + str(vm_id) bottle.response.content_type = "text/uri-list" return res diff --git a/README b/README index 9087df806..b362a77c1 100644 --- a/README +++ b/README @@ -87,7 +87,7 @@ In case of using the REST API the Bottle framework (http://bottlepy.org/) must be installed. In case of using the SSL secured version of the REST API the CherryPy Web -framework (http://www.cherrypy.org/) must be installed. +framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- diff --git a/README.md b/README.md index bbd0b65f0..248b1cdfd 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ In case of using the REST API the Bottle framework (http://bottlepy.org/) must be installed. In case of using the SSL secured version of the REST API the CherryPy Web -framework (http://www.cherrypy.org/) must be installed. +framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- From 15216b358073b4188698ff23363658890c07b40c Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 22 Feb 2016 12:30:55 +0100 Subject: [PATCH 176/509] Bugfix returning protocol https in REST calls --- IM/REST.py | 20 ++++++++++++++++---- README | 2 +- README.md | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 074dc6f46..4b29e3fb5 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -145,10 +145,13 @@ def RESTGetInfrastructureInfo(id=None): vm_ids = InfrastructureManager.GetInfrastructureInfo(id, auth) res = "" + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" for vm_id in vm_ids: if res: res += "\n" - res += 'http://' + bottle.request.environ['HTTP_HOST'] + '/infrastructures/' + str(id) + '/vms/' + str(vm_id) + res += protocol + bottle.request.environ['HTTP_HOST'] + '/infrastructures/' + str(id) + '/vms/' + str(vm_id) bottle.response.content_type = "text/uri-list" return res @@ -213,9 +216,12 @@ def RESTGetInfrastructureList(): try: inf_ids = InfrastructureManager.GetInfrastructureList(auth) + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" res = "" for inf_id in inf_ids: - res += "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + "\n" + res += protocol + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + "\n" bottle.response.content_type = "text/uri-list" return res @@ -260,7 +266,10 @@ def RESTCreateInfrastructure(): sel_inf.extra_info['TOSCA'] = tosca_data bottle.response.content_type = "text/uri-list" - return "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" + return protocol + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id) except HTTPError, ex: raise ex except UnauthorizedUserException, ex: @@ -411,11 +420,14 @@ def RESTAddResource(id=None): sel_inf = InfrastructureManager.get_infrastructure(id, auth) sel_inf.extra_info['TOSCA'] = tosca_data + protocol = "http://" + if Config.REST_SSL: + protocol = "https://" res = "" for vm_id in vm_ids: if res: res += "\n" - res += "http://" + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(id) + "/vms/" + str(vm_id) + res += protocol + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(id) + "/vms/" + str(vm_id) bottle.response.content_type = "text/uri-list" return res diff --git a/README b/README index 9087df806..b362a77c1 100644 --- a/README +++ b/README @@ -87,7 +87,7 @@ In case of using the REST API the Bottle framework (http://bottlepy.org/) must be installed. In case of using the SSL secured version of the REST API the CherryPy Web -framework (http://www.cherrypy.org/) must be installed. +framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- diff --git a/README.md b/README.md index bbd0b65f0..248b1cdfd 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ In case of using the REST API the Bottle framework (http://bottlepy.org/) must be installed. In case of using the SSL secured version of the REST API the CherryPy Web -framework (http://www.cherrypy.org/) must be installed. +framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- From dbff5f035aa06b58d3bc4283c35b656b78ce8b87 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 23 Feb 2016 10:17:53 +0100 Subject: [PATCH 177/509] minor change --- test/QuickTestIM.py | 2 +- test/TestIM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 85bd4e3d4..d97d4db7b 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -94,7 +94,7 @@ def wait_inf_state(self, state, timeout, incorrect_states = [], vm_ids = None): return all_ok - def test_05_list(self): + def test_05_getversion(self): """ Test the GetVersion IM function """ diff --git a/test/TestIM.py b/test/TestIM.py index b9847ae19..2ac335133 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -100,7 +100,7 @@ def wait_inf_state(self, inf_id, state, timeout, incorrect_states = [], vm_ids = return all_ok - def test_05_list(self): + def test_05_getversion(self): """ Test the GetVersion IM function """ From 84484a03eb4fadb90a70a21158d798a0ae1f69bc Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 23 Feb 2016 10:17:53 +0100 Subject: [PATCH 178/509] minor change --- test/QuickTestIM.py | 2 +- test/TestIM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/QuickTestIM.py b/test/QuickTestIM.py index 85bd4e3d4..d97d4db7b 100755 --- a/test/QuickTestIM.py +++ b/test/QuickTestIM.py @@ -94,7 +94,7 @@ def wait_inf_state(self, state, timeout, incorrect_states = [], vm_ids = None): return all_ok - def test_05_list(self): + def test_05_getversion(self): """ Test the GetVersion IM function """ diff --git a/test/TestIM.py b/test/TestIM.py index b9847ae19..2ac335133 100755 --- a/test/TestIM.py +++ b/test/TestIM.py @@ -100,7 +100,7 @@ def wait_inf_state(self, inf_id, state, timeout, incorrect_states = [], vm_ids = return all_ok - def test_05_list(self): + def test_05_getversion(self): """ Test the GetVersion IM function """ From 2896b8cc3142e58b9e012595708d35551ff4a747 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 24 Feb 2016 10:48:39 +0100 Subject: [PATCH 179/509] Set ansible version 1.9.4 as recommended --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 248b1cdfd..5957b9f95 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ be installed in the system. to enable to use it with the IM. + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. - In particular, Ansible 1.4.2+ must be installed. + In particular, Ansible 1.4.2+ must be installed. The current recommended version is 1.9.4 untill the 2.X versions become stable. To ensure the functionality the following values must be set in the ansible.cfg file (usually found in /etc/ansible/): ``` diff --git a/setup.py b/setup.py index 3fcd0df24..c881d0e41 100644 --- a/setup.py +++ b/setup.py @@ -44,5 +44,5 @@ long_description="IM is a tool that ease the access and the usability of IaaS clouds by automating the VMI selection, deployment, configuration, software installation, monitoring and update of Virtual Appliances. It supports APIs from a large number of virtual platforms, making user applications cloud-agnostic. In addition it integrates a contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure.", description="IM is a tool to manage virtual infrastructures on Cloud deployments", platforms=["any"], - install_requires=["ansible >= 1.9","paramiko >= 1.14","PyYAML","SOAPpy","boto >= 2.29","apache-libcloud >= 0.17","ply", "bottle", "netaddr", "scp"] + install_requires=["ansible == 1.9.4","paramiko >= 1.14","PyYAML","SOAPpy","boto >= 2.29","apache-libcloud >= 0.17","ply", "bottle", "netaddr", "scp"] ) From ac6ffb9ac9f85356fe35a54cda253ec680d2fc0f Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 24 Feb 2016 10:48:39 +0100 Subject: [PATCH 180/509] Set ansible version 1.9.4 as recommended --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 248b1cdfd..5957b9f95 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ be installed in the system. to enable to use it with the IM. + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. - In particular, Ansible 1.4.2+ must be installed. + In particular, Ansible 1.4.2+ must be installed. The current recommended version is 1.9.4 untill the 2.X versions become stable. To ensure the functionality the following values must be set in the ansible.cfg file (usually found in /etc/ansible/): ``` diff --git a/setup.py b/setup.py index 3fcd0df24..c881d0e41 100644 --- a/setup.py +++ b/setup.py @@ -44,5 +44,5 @@ long_description="IM is a tool that ease the access and the usability of IaaS clouds by automating the VMI selection, deployment, configuration, software installation, monitoring and update of Virtual Appliances. It supports APIs from a large number of virtual platforms, making user applications cloud-agnostic. In addition it integrates a contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure.", description="IM is a tool to manage virtual infrastructures on Cloud deployments", platforms=["any"], - install_requires=["ansible >= 1.9","paramiko >= 1.14","PyYAML","SOAPpy","boto >= 2.29","apache-libcloud >= 0.17","ply", "bottle", "netaddr", "scp"] + install_requires=["ansible == 1.9.4","paramiko >= 1.14","PyYAML","SOAPpy","boto >= 2.29","apache-libcloud >= 0.17","ply", "bottle", "netaddr", "scp"] ) From b48cddbf7cd4788302f8c46f93c44771c2b736ff Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 24 Feb 2016 12:11:44 +0100 Subject: [PATCH 181/509] Docker connector bugfix --- connectors/Docker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connectors/Docker.py b/connectors/Docker.py index 1d65b585c..b3aae54d8 100644 --- a/connectors/Docker.py +++ b/connectors/Docker.py @@ -230,11 +230,11 @@ def _generate_exposed_ports(self, outports): def _generate_port_bindings(self, outports, ssh_port): res = {} - res["22/tcp"] = [{"HostPort":ssh_port}] + res["22/tcp"] = [{"HostPort":str(ssh_port)}] if outports: for remote_port,_,local_port,local_protocol in outports: if local_port != 22: - res[str(local_port) + '/' + local_protocol] = [{"HostPort":remote_port}] + res[str(local_port) + '/' + local_protocol] = [{"HostPort":str(remote_port)}] return res From b5a1651a619d896085bc02b92969831bc8964bbd Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 24 Feb 2016 12:11:44 +0100 Subject: [PATCH 182/509] Docker connector bugfix --- connectors/Docker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connectors/Docker.py b/connectors/Docker.py index 1d65b585c..b3aae54d8 100644 --- a/connectors/Docker.py +++ b/connectors/Docker.py @@ -230,11 +230,11 @@ def _generate_exposed_ports(self, outports): def _generate_port_bindings(self, outports, ssh_port): res = {} - res["22/tcp"] = [{"HostPort":ssh_port}] + res["22/tcp"] = [{"HostPort":str(ssh_port)}] if outports: for remote_port,_,local_port,local_protocol in outports: if local_port != 22: - res[str(local_port) + '/' + local_protocol] = [{"HostPort":remote_port}] + res[str(local_port) + '/' + local_protocol] = [{"HostPort":str(remote_port)}] return res From fd320a1d6c10ddd3613b0fe697f9b8765d1c5435 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 29 Feb 2016 10:47:03 +0100 Subject: [PATCH 183/509] Bugfix in REST API with auth data with new lines --- IM/REST.py | 53 +++++++++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 4b29e3fb5..7108b8f71 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -25,7 +25,10 @@ from IM.tosca.Tosca import Tosca from bottle import HTTPError +# Combination of chars used to separate the lines in the AUTH header AUTH_LINE_SEPARATOR = '\\n' +# Combination of chars used to separate the lines inside the auth data (i.e. in a certificate) +AUTH_NEW_LINE_SEPARATOR = '\\\\n' app = bottle.Bottle() bottle_server = None @@ -111,11 +114,15 @@ def get_media_type(header): else: return accept +def get_auth_header(): + auth_data = bottle.request.headers['AUTHORIZATION'].replace(AUTH_NEW_LINE_SEPARATOR,"\n") + auth_data = auth_data.split(AUTH_LINE_SEPARATOR) + return Authentication(Authentication.read_auth_data(auth_data)) + @app.route('/infrastructures/:id', method='DELETE') def RESTDestroyInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -136,8 +143,7 @@ def RESTDestroyInfrastructure(id=None): @app.route('/infrastructures/:id', method='GET') def RESTGetInfrastructureInfo(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -167,8 +173,7 @@ def RESTGetInfrastructureInfo(id=None): @app.route('/infrastructures/:id/:prop', method='GET') def RESTGetInfrastructureProperty(id=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -208,8 +213,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): @app.route('/infrastructures', method='GET') def RESTGetInfrastructureList(): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -236,8 +240,7 @@ def RESTGetInfrastructureList(): @app.route('/infrastructures', method='POST') def RESTCreateInfrastructure(): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -282,8 +285,7 @@ def RESTCreateInfrastructure(): @app.route('/infrastructures/:infid/vms/:vmid', method='GET') def RESTGetVMInfo(infid=None, vmid=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -328,8 +330,7 @@ def RESTGetVMInfo(infid=None, vmid=None): @app.route('/infrastructures/:infid/vms/:vmid/:prop', method='GET') def RESTGetVMProperty(infid=None, vmid=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -376,8 +377,7 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): @app.route('/infrastructures/:id', method='POST') def RESTAddResource(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -446,8 +446,7 @@ def RESTAddResource(id=None): @app.route('/infrastructures/:infid/vms/:vmid', method='DELETE') def RESTRemoveResource(infid=None, vmid=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -486,8 +485,7 @@ def RESTRemoveResource(infid=None, vmid=None): @app.route('/infrastructures/:infid/vms/:vmid', method='PUT') def RESTAlterVM(infid=None, vmid=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -542,8 +540,7 @@ def RESTAlterVM(infid=None, vmid=None): @app.route('/infrastructures/:id/reconfigure', method='PUT') def RESTReconfigureInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -586,8 +583,7 @@ def RESTReconfigureInfrastructure(id=None): @app.route('/infrastructures/:id/start', method='PUT') def RESTStartInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -606,8 +602,7 @@ def RESTStartInfrastructure(id=None): @app.route('/infrastructures/:id/stop', method='PUT') def RESTStopInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -626,8 +621,7 @@ def RESTStopInfrastructure(id=None): @app.route('/infrastructures/:infid/vms/:vmid/start', method='PUT') def RESTStartVM(infid=None, vmid=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -654,8 +648,7 @@ def RESTStartVM(infid=None, vmid=None, prop=None): @app.route('/infrastructures/:infid/vms/:vmid/stop', method='PUT') def RESTStopVM(infid=None, vmid=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") From d3852c1a7a53b22bedb83e76e425611e190659d9 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 29 Feb 2016 10:47:03 +0100 Subject: [PATCH 184/509] Bugfix in REST API with auth data with new lines --- IM/REST.py | 53 +++++++++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 4b29e3fb5..7108b8f71 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -25,7 +25,10 @@ from IM.tosca.Tosca import Tosca from bottle import HTTPError +# Combination of chars used to separate the lines in the AUTH header AUTH_LINE_SEPARATOR = '\\n' +# Combination of chars used to separate the lines inside the auth data (i.e. in a certificate) +AUTH_NEW_LINE_SEPARATOR = '\\\\n' app = bottle.Bottle() bottle_server = None @@ -111,11 +114,15 @@ def get_media_type(header): else: return accept +def get_auth_header(): + auth_data = bottle.request.headers['AUTHORIZATION'].replace(AUTH_NEW_LINE_SEPARATOR,"\n") + auth_data = auth_data.split(AUTH_LINE_SEPARATOR) + return Authentication(Authentication.read_auth_data(auth_data)) + @app.route('/infrastructures/:id', method='DELETE') def RESTDestroyInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -136,8 +143,7 @@ def RESTDestroyInfrastructure(id=None): @app.route('/infrastructures/:id', method='GET') def RESTGetInfrastructureInfo(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -167,8 +173,7 @@ def RESTGetInfrastructureInfo(id=None): @app.route('/infrastructures/:id/:prop', method='GET') def RESTGetInfrastructureProperty(id=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -208,8 +213,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): @app.route('/infrastructures', method='GET') def RESTGetInfrastructureList(): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -236,8 +240,7 @@ def RESTGetInfrastructureList(): @app.route('/infrastructures', method='POST') def RESTCreateInfrastructure(): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -282,8 +285,7 @@ def RESTCreateInfrastructure(): @app.route('/infrastructures/:infid/vms/:vmid', method='GET') def RESTGetVMInfo(infid=None, vmid=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -328,8 +330,7 @@ def RESTGetVMInfo(infid=None, vmid=None): @app.route('/infrastructures/:infid/vms/:vmid/:prop', method='GET') def RESTGetVMProperty(infid=None, vmid=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -376,8 +377,7 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): @app.route('/infrastructures/:id', method='POST') def RESTAddResource(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -446,8 +446,7 @@ def RESTAddResource(id=None): @app.route('/infrastructures/:infid/vms/:vmid', method='DELETE') def RESTRemoveResource(infid=None, vmid=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -486,8 +485,7 @@ def RESTRemoveResource(infid=None, vmid=None): @app.route('/infrastructures/:infid/vms/:vmid', method='PUT') def RESTAlterVM(infid=None, vmid=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -542,8 +540,7 @@ def RESTAlterVM(infid=None, vmid=None): @app.route('/infrastructures/:id/reconfigure', method='PUT') def RESTReconfigureInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -586,8 +583,7 @@ def RESTReconfigureInfrastructure(id=None): @app.route('/infrastructures/:id/start', method='PUT') def RESTStartInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -606,8 +602,7 @@ def RESTStartInfrastructure(id=None): @app.route('/infrastructures/:id/stop', method='PUT') def RESTStopInfrastructure(id=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -626,8 +621,7 @@ def RESTStopInfrastructure(id=None): @app.route('/infrastructures/:infid/vms/:vmid/start', method='PUT') def RESTStartVM(infid=None, vmid=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") @@ -654,8 +648,7 @@ def RESTStartVM(infid=None, vmid=None, prop=None): @app.route('/infrastructures/:infid/vms/:vmid/stop', method='PUT') def RESTStopVM(infid=None, vmid=None, prop=None): try: - auth_data = bottle.request.headers['AUTHORIZATION'].split(AUTH_LINE_SEPARATOR) - auth = Authentication(Authentication.read_auth_data(auth_data)) + auth = get_auth_header() except: bottle.abort(401, "No authentication data provided") From b99ea0992a83dcbf6f35d7d2473e19645e8e0f0d Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 29 Feb 2016 10:47:35 +0100 Subject: [PATCH 185/509] Add IM_INFRASTRUCTURE_ID variable --- IM/ConfManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 778bd104c..45b11cd36 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -405,7 +405,8 @@ def generate_inventory(self, tmp_dir): out.write(all_vars) out.write('IM_MASTER_HOSTNAME=' + master_name + '\n') out.write('IM_MASTER_FQDN=' + master_name + "." + masterdom + '\n') - out.write('IM_MASTER_DOMAIN=' + masterdom + '\n\n') + out.write('IM_MASTER_DOMAIN=' + masterdom + '\n') + out.write('IM_INFRASTRUCTURE_ID=' + self.inf.id + '\n\n') if windows: out.write('[windows]\n' + windows + "\n") From 27bbac1026dc44bece71b4e59f09298a9be2effe Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 29 Feb 2016 10:47:35 +0100 Subject: [PATCH 186/509] Add IM_INFRASTRUCTURE_ID variable --- IM/ConfManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 778bd104c..45b11cd36 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -405,7 +405,8 @@ def generate_inventory(self, tmp_dir): out.write(all_vars) out.write('IM_MASTER_HOSTNAME=' + master_name + '\n') out.write('IM_MASTER_FQDN=' + master_name + "." + masterdom + '\n') - out.write('IM_MASTER_DOMAIN=' + masterdom + '\n\n') + out.write('IM_MASTER_DOMAIN=' + masterdom + '\n') + out.write('IM_INFRASTRUCTURE_ID=' + self.inf.id + '\n\n') if windows: out.write('[windows]\n' + windows + "\n") From ddc0c504f5c14199bda8fb098dd3f8b773a68cfe Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 1 Mar 2016 09:49:55 +0100 Subject: [PATCH 187/509] Minor change --- IM/tosca/Tosca.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 3c7b8ada9..07a161d00 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -238,17 +238,18 @@ def _get_relationship_template(rel, src, trgt): def _get_relationships_interfaces(relationships, node): res = {} for src, trgt, rel in relationships: - rel_tpl = Tosca._get_relationship_template(rel, src, trgt) - if src.name == node.name: - for name in ['pre_configure_source', 'post_configure_source', 'add_source']: - for iface in rel_tpl.interfaces: - if iface.name == name: - res[name] = iface - elif trgt.name == node.name: - for name in ['pre_configure_target', 'post_configure_target', 'add_target','target_changed','remove_target']: - for iface in rel_tpl.interfaces: - if iface.name == name: - res[name] = iface + rel_tpl = Tosca._get_relationship_template(rel, src, trgt) + if rel_tpl.interfaces: + if src.name == node.name: + for name in ['pre_configure_source', 'post_configure_source', 'add_source']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface + elif trgt.name == node.name: + for name in ['pre_configure_target', 'post_configure_target', 'add_target','target_changed','remove_target']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface return res def _get_artifact_full_uri(self, node, artifact_name): From ecea354968a8b3f0e3f4e5e70f2ea8e9158841ae Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 1 Mar 2016 09:49:55 +0100 Subject: [PATCH 188/509] Minor change --- IM/tosca/Tosca.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 3c7b8ada9..07a161d00 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -238,17 +238,18 @@ def _get_relationship_template(rel, src, trgt): def _get_relationships_interfaces(relationships, node): res = {} for src, trgt, rel in relationships: - rel_tpl = Tosca._get_relationship_template(rel, src, trgt) - if src.name == node.name: - for name in ['pre_configure_source', 'post_configure_source', 'add_source']: - for iface in rel_tpl.interfaces: - if iface.name == name: - res[name] = iface - elif trgt.name == node.name: - for name in ['pre_configure_target', 'post_configure_target', 'add_target','target_changed','remove_target']: - for iface in rel_tpl.interfaces: - if iface.name == name: - res[name] = iface + rel_tpl = Tosca._get_relationship_template(rel, src, trgt) + if rel_tpl.interfaces: + if src.name == node.name: + for name in ['pre_configure_source', 'post_configure_source', 'add_source']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface + elif trgt.name == node.name: + for name in ['pre_configure_target', 'post_configure_target', 'add_target','target_changed','remove_target']: + for iface in rel_tpl.interfaces: + if iface.name == name: + res[name] = iface return res def _get_artifact_full_uri(self, node, artifact_name): From 2def21697ea5447502b09cbfff19f0930ad2596b Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 1 Mar 2016 12:23:51 +0100 Subject: [PATCH 189/509] Bugfix in ansible_executor_v2 with version ansible 2.0.1 --- IM/ansible/ansible_executor_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/ansible/ansible_executor_v2.py b/IM/ansible/ansible_executor_v2.py index d78624b74..9b4af0ad8 100644 --- a/IM/ansible/ansible_executor_v2.py +++ b/IM/ansible/ansible_executor_v2.py @@ -356,6 +356,6 @@ def run(self): finally: if self._tqm is not None: - self._cleanup() + self._tqm.cleanup() return result \ No newline at end of file From 22309986a997da4ce67d18fd62fc4699a5a4d418 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 1 Mar 2016 12:23:51 +0100 Subject: [PATCH 190/509] Bugfix in ansible_executor_v2 with version ansible 2.0.1 --- IM/ansible/ansible_executor_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/ansible/ansible_executor_v2.py b/IM/ansible/ansible_executor_v2.py index d78624b74..9b4af0ad8 100644 --- a/IM/ansible/ansible_executor_v2.py +++ b/IM/ansible/ansible_executor_v2.py @@ -356,6 +356,6 @@ def run(self): finally: if self._tqm is not None: - self._cleanup() + self._tqm.cleanup() return result \ No newline at end of file From 4484302086c6fa6f1b1f8f2514d3ef379b48a61d Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 1 Mar 2016 16:05:35 +0100 Subject: [PATCH 191/509] Add README to docker --- docker/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docker/README.md diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..1565b430b --- /dev/null +++ b/docker/README.md @@ -0,0 +1,32 @@ + IM - Infrastructure Manager (With TOSCA Support) +================================================= + +IM is a tool that deploys complex and customized virtual infrastructures on IaaS +Cloud deployments (such as AWS, OpenStack, etc.). It eases the access and the +usability of IaaS clouds by automating the VMI (Virtual Machine Image) +selection, deployment, configuration, software installation, monitoring and +update of the virtual infrastructure. It supports APIs from a large number of virtual +platforms, making user applications cloud-agnostic. In addition it integrates a +contextualization system to enable the installation and configuration of all the +user required applications providing the user with a fully functional +infrastructure. + +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/) has +added support to TOSCA documents as input for the infrastructure creation. + +Read the documentation and more at http://www.grycap.upv.es/im. + +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. + +DOCKER IMAGE +============= + +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. + +How to launch the IM service using docker: + +```sh +sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im +``` From 0e9a8bad01ff2a53287cf12eefc5cf8d139b6eb6 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 1 Mar 2016 16:05:35 +0100 Subject: [PATCH 192/509] Add README to docker --- docker/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docker/README.md diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..1565b430b --- /dev/null +++ b/docker/README.md @@ -0,0 +1,32 @@ + IM - Infrastructure Manager (With TOSCA Support) +================================================= + +IM is a tool that deploys complex and customized virtual infrastructures on IaaS +Cloud deployments (such as AWS, OpenStack, etc.). It eases the access and the +usability of IaaS clouds by automating the VMI (Virtual Machine Image) +selection, deployment, configuration, software installation, monitoring and +update of the virtual infrastructure. It supports APIs from a large number of virtual +platforms, making user applications cloud-agnostic. In addition it integrates a +contextualization system to enable the installation and configuration of all the +user required applications providing the user with a fully functional +infrastructure. + +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/) has +added support to TOSCA documents as input for the infrastructure creation. + +Read the documentation and more at http://www.grycap.upv.es/im. + +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. + +DOCKER IMAGE +============= + +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. + +How to launch the IM service using docker: + +```sh +sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im +``` From 5449e609a380a933ad0a3ddf92185d6fd8404315 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 11:27:13 +0100 Subject: [PATCH 193/509] Minor change --- contextualization/conf-ansible.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 255e5065a..eadac520c 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -9,16 +9,6 @@ - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" - - # Disable IPv6 - - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" - with_items: - - 'net.ipv6.conf.all.disable_ipv6' - - 'net.ipv6.conf.default.disable_ipv6' - - 'net.ipv6.conf.lo.disable_ipv6' - ignore_errors: yes - - command: sysctl -p - ignore_errors: yes - name: Apt-get update apt: update_cache=yes From 72dc07e6dea521311698aa074dc7995bd7896da7 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 11:27:13 +0100 Subject: [PATCH 194/509] Minor change --- contextualization/conf-ansible.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 255e5065a..eadac520c 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -9,16 +9,6 @@ - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" - - # Disable IPv6 - - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" - with_items: - - 'net.ipv6.conf.all.disable_ipv6' - - 'net.ipv6.conf.default.disable_ipv6' - - 'net.ipv6.conf.lo.disable_ipv6' - ignore_errors: yes - - command: sysctl -p - ignore_errors: yes - name: Apt-get update apt: update_cache=yes From be29c0cb4ad726ce3d39b7d23c19dcc268ed7249 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 12:25:59 +0100 Subject: [PATCH 195/509] Bugfix to setup not installing tosca package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6eb4c1f81..a8e07c916 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', include_package_data = True, - packages=['IM', 'IM.ansible','IM.connectors'], + packages=['IM', 'IM.ansible','IM.connectors','IM.tosca'], scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From f4be5b392ae939b495aac0efe8a558900c98b241 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 12:25:59 +0100 Subject: [PATCH 196/509] Bugfix to setup not installing tosca package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6eb4c1f81..a8e07c916 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', include_package_data = True, - packages=['IM', 'IM.ansible','IM.connectors'], + packages=['IM', 'IM.ansible','IM.connectors','IM.tosca'], scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From f71e67ff589ff4265516eda160b33fad92928d0d Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 13:39:02 +0100 Subject: [PATCH 197/509] Bugfix using incorrect path of radl, that now is external --- IM/tosca/Tosca.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 07a161d00..a89f91f87 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -9,7 +9,7 @@ from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute -from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize class Tosca: """ @@ -505,6 +505,7 @@ def _get_attribute_result(self, func, node, inf_info): * { get_attribute: [ server, private_address ] } * { get_attribute: [ HOST, private_address ] } * { get_attribute: [ SELF, private_address ] } + * { get_attribute: [ HOST, private_address, 0 ] } """ node_name = func.args[0] attribute_name = func.args[1] From e91c547bc446188b4850e93fc3270a9e2f07b628 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 13:39:02 +0100 Subject: [PATCH 198/509] Bugfix using incorrect path of radl, that now is external --- IM/tosca/Tosca.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 07a161d00..a89f91f87 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -9,7 +9,7 @@ from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute -from IM.radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize class Tosca: """ @@ -505,6 +505,7 @@ def _get_attribute_result(self, func, node, inf_info): * { get_attribute: [ server, private_address ] } * { get_attribute: [ HOST, private_address ] } * { get_attribute: [ SELF, private_address ] } + * { get_attribute: [ HOST, private_address, 0 ] } """ node_name = func.args[0] attribute_name = func.args[1] From ab35a992e9fcfe46142e0904b038a73fddc6df60 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 15:19:27 +0100 Subject: [PATCH 199/509] Bugfix in the ssh_known_hosts template in CentOS 7 --- .../utils/templates/ssh_known_hosts.conf | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf b/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf index a7c7a0c04..dae9fee33 100644 --- a/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf +++ b/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf @@ -16,20 +16,32 @@ along with this program. If not, see . #} {% for host in groups['all'] %} +{% if hostvars[host]['ansible_ssh_host_key_dsa_public'] is defined %} {{ hostvars[host]['ansible_fqdn'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} -{{ hostvars[host]['ansible_fqdn'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['ansible_hostname'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} +{% endif %} +{% if hostvars[host]['ansible_ssh_host_key_rsa_public'] is defined %} +{{ hostvars[host]['ansible_fqdn'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['ansible_hostname'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} +{% endif %} {% for ip in hostvars[host]['ansible_all_ipv4_addresses'] %} +{% if hostvars[host]['ansible_ssh_host_key_dsa_public'] is defined %} {{ ip }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} +{% endif %} +{% if hostvars[host]['ansible_ssh_host_key_rsa_public'] is defined %} {{ ip }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} +{% endif %} {% endfor %} {% if hostvars[host]['IM_NODE_NET_1_FQDN'] is defined %} +{% if hostvars[host]['ansible_ssh_host_key_dsa_public'] is defined %} {{ hostvars[host]['IM_NODE_NET_1_IP'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_HOSTNAME'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_FQDN'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} +{% endif %} +{% if hostvars[host]['ansible_ssh_host_key_rsa_public'] is defined %} {{ hostvars[host]['IM_NODE_NET_1_IP'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_HOSTNAME'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_FQDN'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {% endif %} +{% endif %} {% endfor %} From 6374ba3c9d4f99cab70f363ebbcc008e70abf36e Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 15:19:27 +0100 Subject: [PATCH 200/509] Bugfix in the ssh_known_hosts template in CentOS 7 --- .../utils/templates/ssh_known_hosts.conf | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf b/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf index a7c7a0c04..dae9fee33 100644 --- a/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf +++ b/contextualization/AnsibleRecipes/utils/templates/ssh_known_hosts.conf @@ -16,20 +16,32 @@ along with this program. If not, see . #} {% for host in groups['all'] %} +{% if hostvars[host]['ansible_ssh_host_key_dsa_public'] is defined %} {{ hostvars[host]['ansible_fqdn'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} -{{ hostvars[host]['ansible_fqdn'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['ansible_hostname'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} +{% endif %} +{% if hostvars[host]['ansible_ssh_host_key_rsa_public'] is defined %} +{{ hostvars[host]['ansible_fqdn'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['ansible_hostname'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} +{% endif %} {% for ip in hostvars[host]['ansible_all_ipv4_addresses'] %} +{% if hostvars[host]['ansible_ssh_host_key_dsa_public'] is defined %} {{ ip }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} +{% endif %} +{% if hostvars[host]['ansible_ssh_host_key_rsa_public'] is defined %} {{ ip }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} +{% endif %} {% endfor %} {% if hostvars[host]['IM_NODE_NET_1_FQDN'] is defined %} +{% if hostvars[host]['ansible_ssh_host_key_dsa_public'] is defined %} {{ hostvars[host]['IM_NODE_NET_1_IP'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_HOSTNAME'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_FQDN'] }} ssh-dss {{ hostvars[host]['ansible_ssh_host_key_dsa_public'] }} +{% endif %} +{% if hostvars[host]['ansible_ssh_host_key_rsa_public'] is defined %} {{ hostvars[host]['IM_NODE_NET_1_IP'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_HOSTNAME'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {{ hostvars[host]['IM_NODE_NET_1_FQDN'] }} ssh-rsa {{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }} {% endif %} +{% endif %} {% endfor %} From 530bf707b5e503c0a3c7e8e45ffb8e3c539c8998 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 3 Mar 2016 15:20:12 +0100 Subject: [PATCH 201/509] Bugfix with the index 0 in the get_attribute function --- IM/tosca/Tosca.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a89f91f87..d05a2f4d6 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -539,7 +539,7 @@ def _get_attribute_result(self, func, node, inf_info): Tosca.logger.warn("There are no VM associated with the name %s." % node.name) return None else: - # Always assume that there will be only one VM per group + # As default assume that there will be only one VM per group vm = vm_list[node.name][0] if len(vm_list[node.name]) Date: Thu, 3 Mar 2016 15:20:12 +0100 Subject: [PATCH 202/509] Bugfix with the index 0 in the get_attribute function --- IM/tosca/Tosca.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a89f91f87..d05a2f4d6 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -539,7 +539,7 @@ def _get_attribute_result(self, func, node, inf_info): Tosca.logger.warn("There are no VM associated with the name %s." % node.name) return None else: - # Always assume that there will be only one VM per group + # As default assume that there will be only one VM per group vm = vm_list[node.name][0] if len(vm_list[node.name]) Date: Fri, 4 Mar 2016 10:38:42 +0100 Subject: [PATCH 203/509] Add docker-devel --- docker-devel/Dockerfile | 40 ++++++++++++++++++++++++++++++++++++++++ docker-devel/ansible.cfg | 17 +++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 docker-devel/Dockerfile create mode 100644 docker-devel/ansible.cfg diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile new file mode 100644 index 000000000..bc9504894 --- /dev/null +++ b/docker-devel/Dockerfile @@ -0,0 +1,40 @@ +# Dockerfile to create a container with the IM service +FROM ubuntu:14.04 +MAINTAINER Miguel Caballer +LABEL version="1.4.3" +LABEL description="Container image to run the IM service. (http://www.grycap.upv.es/im)" + +EXPOSE 8899 8800 + +RUN apt-get update && apt-get install -y \ + gcc \ + python-dev \ + python-pip \ + python-soappy \ + python-dateutil \ + python-pbr \ + python-mock \ + python-nose \ + openssh-client \ + sshpass \ + git \ + libssl-dev \ + libffi-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install CherryPy to enable HTTPS in REST API +RUN pip install CherryPy pyOpenSSL + +# Install im - 'devel' branch +RUN cd tmp \ + && git clone -b devel https://github.com/grycap/im.git \ + && cd im \ + && python setup.py install + +# Turn on the REST services +RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg + +COPY ansible.cfg /etc/ansible/ansible.cfg + +CMD im_service.py diff --git a/docker-devel/ansible.cfg b/docker-devel/ansible.cfg new file mode 100644 index 000000000..2ae9bcc53 --- /dev/null +++ b/docker-devel/ansible.cfg @@ -0,0 +1,17 @@ +[defaults] +transport = smart +host_key_checking = False +become_user = root +become_method = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True From a4b37148dee1f1d805193687099fa353519fd14c Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 4 Mar 2016 12:56:23 +0100 Subject: [PATCH 204/509] Add README to docker-devel --- docker-devel/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docker-devel/README.md diff --git a/docker-devel/README.md b/docker-devel/README.md new file mode 100644 index 000000000..1565b430b --- /dev/null +++ b/docker-devel/README.md @@ -0,0 +1,32 @@ + IM - Infrastructure Manager (With TOSCA Support) +================================================= + +IM is a tool that deploys complex and customized virtual infrastructures on IaaS +Cloud deployments (such as AWS, OpenStack, etc.). It eases the access and the +usability of IaaS clouds by automating the VMI (Virtual Machine Image) +selection, deployment, configuration, software installation, monitoring and +update of the virtual infrastructure. It supports APIs from a large number of virtual +platforms, making user applications cloud-agnostic. In addition it integrates a +contextualization system to enable the installation and configuration of all the +user required applications providing the user with a fully functional +infrastructure. + +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/) has +added support to TOSCA documents as input for the infrastructure creation. + +Read the documentation and more at http://www.grycap.upv.es/im. + +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. + +DOCKER IMAGE +============= + +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. + +How to launch the IM service using docker: + +```sh +sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im +``` From 6f93f8dd16c1f27873689335dae886e24bd0bad4 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 4 Mar 2016 12:56:23 +0100 Subject: [PATCH 205/509] Add README to docker-devel --- docker-devel/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docker-devel/README.md diff --git a/docker-devel/README.md b/docker-devel/README.md new file mode 100644 index 000000000..1565b430b --- /dev/null +++ b/docker-devel/README.md @@ -0,0 +1,32 @@ + IM - Infrastructure Manager (With TOSCA Support) +================================================= + +IM is a tool that deploys complex and customized virtual infrastructures on IaaS +Cloud deployments (such as AWS, OpenStack, etc.). It eases the access and the +usability of IaaS clouds by automating the VMI (Virtual Machine Image) +selection, deployment, configuration, software installation, monitoring and +update of the virtual infrastructure. It supports APIs from a large number of virtual +platforms, making user applications cloud-agnostic. In addition it integrates a +contextualization system to enable the installation and configuration of all the +user required applications providing the user with a fully functional +infrastructure. + +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/) has +added support to TOSCA documents as input for the infrastructure creation. + +Read the documentation and more at http://www.grycap.upv.es/im. + +There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos +of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. + +DOCKER IMAGE +============= + +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. + +How to launch the IM service using docker: + +```sh +sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im +``` From fd5f07652897826dddaa8e08d40a0369d59d8b4f Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 9 Mar 2016 14:42:31 +0100 Subject: [PATCH 206/509] Show log info in the REST API module --- IM/REST.py | 89 +++++++++++++++++------------------------------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 7108b8f71..757a43b8e 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -14,16 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from InfrastructureInfo import IncorrectVMException, DeletedVMException -from InfrastructureManager import InfrastructureManager, DeletedInfrastructureException, IncorrectInfrastructureException, UnauthorizedUserException -from auth import Authentication +import logging import threading import bottle import json + +from bottle import HTTPError + +from InfrastructureInfo import IncorrectVMException, DeletedVMException +from InfrastructureManager import InfrastructureManager, DeletedInfrastructureException, IncorrectInfrastructureException, UnauthorizedUserException +from auth import Authentication from config import Config from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json from IM.tosca.Tosca import Tosca -from bottle import HTTPError + +logger = logging.getLogger('InfrastructureManager') # Combination of chars used to separate the lines in the AUTH header AUTH_LINE_SEPARATOR = '\\n' @@ -132,13 +137,11 @@ def RESTDestroyInfrastructure(id=None): return "" except DeletedInfrastructureException, ex: bottle.abort(404, "Error Destroying Inf: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Destroying Inf: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Destroying Inf") bottle.abort(400, "Error Destroying Inf: " + str(ex)) - return False @app.route('/infrastructures/:id', method='GET') def RESTGetInfrastructureInfo(id=None): @@ -163,11 +166,10 @@ def RESTGetInfrastructureInfo(id=None): return res except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting Inf. info") bottle.abort(400, "Error Getting Inf. info: " + str(ex)) @app.route('/infrastructures/:id/:prop', method='GET') @@ -202,13 +204,12 @@ def RESTGetInfrastructureProperty(id=None, prop=None): except HTTPError, ex: raise ex except DeletedInfrastructureException, ex: - bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False + bottle.abort(404, "Error Getting Inf. prop: " + str(ex)) except IncorrectInfrastructureException, ex: - bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False + bottle.abort(404, "Error Getting Inf. prop: " + str(ex)) except Exception, ex: - bottle.abort(400, "Error Getting Inf. info: " + str(ex)) + logger.exception("Error Getting Inf. prop") + bottle.abort(400, "Error Getting Inf. prop: " + str(ex)) @app.route('/infrastructures', method='GET') def RESTGetInfrastructureList(): @@ -231,10 +232,9 @@ def RESTGetInfrastructureList(): return res except UnauthorizedUserException, ex: bottle.abort(401, "Error Getting Inf. List: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting Inf. List") bottle.abort(400, "Error Getting Inf. List: " + str(ex)) - return False @app.route('/infrastructures', method='POST') @@ -277,10 +277,9 @@ def RESTCreateInfrastructure(): raise ex except UnauthorizedUserException, ex: bottle.abort(401, "Error Getting Inf. info: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Creating Inf.") bottle.abort(400, "Error Creating Inf.: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid', method='GET') def RESTGetVMInfo(infid=None, vmid=None): @@ -313,19 +312,15 @@ def RESTGetVMInfo(infid=None, vmid=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting VM info") bottle.abort(400, "Error Getting VM info: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid/:prop', method='GET') def RESTGetVMProperty(infid=None, vmid=None, prop=None): @@ -360,19 +355,15 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting VM property") bottle.abort(400, "Error Getting VM property: " + str(ex)) - return False @app.route('/infrastructures/:id', method='POST') def RESTAddResource(id=None): @@ -435,13 +426,11 @@ def RESTAddResource(id=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Adding resources: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Adding resources: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Adding resources") bottle.abort(400, "Error Adding resources: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid', method='DELETE') def RESTRemoveResource(infid=None, vmid=None): @@ -468,19 +457,15 @@ def RESTRemoveResource(infid=None, vmid=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Removing resources") bottle.abort(400, "Error Removing resources: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid', method='PUT') def RESTAlterVM(infid=None, vmid=None): @@ -523,19 +508,15 @@ def RESTAlterVM(infid=None, vmid=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except Exception, ex: + logger.exception("Error modifying resources") bottle.abort(400, "Error modifying resources: " + str(ex)) - return False @app.route('/infrastructures/:id/reconfigure', method='PUT') def RESTReconfigureInfrastructure(id=None): @@ -572,13 +553,11 @@ def RESTReconfigureInfrastructure(id=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) - return False except Exception, ex: + logger.exception("Error reconfiguring infrastructure") bottle.abort(400, "Error reconfiguring infrastructure: " + str(ex)) - return False @app.route('/infrastructures/:id/start', method='PUT') def RESTStartInfrastructure(id=None): @@ -591,13 +570,11 @@ def RESTStartInfrastructure(id=None): return InfrastructureManager.StartInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error starting infrastructure: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error starting infrastructure: " + str(ex)) - return False except Exception, ex: + logger.exception("Error starting infrastructure") bottle.abort(400, "Error starting infrastructure: " + str(ex)) - return False @app.route('/infrastructures/:id/stop', method='PUT') def RESTStopInfrastructure(id=None): @@ -610,13 +587,11 @@ def RESTStopInfrastructure(id=None): return InfrastructureManager.StopInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error stopping infrastructure: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error stopping infrastructure: " + str(ex)) - return False except Exception, ex: + logger.exception("Error stopping infrastructure") bottle.abort(400, "Error stopping infrastructure: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid/start', method='PUT') def RESTStartVM(infid=None, vmid=None, prop=None): @@ -631,19 +606,15 @@ def RESTStartVM(infid=None, vmid=None, prop=None): return info except DeletedInfrastructureException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except Exception, ex: + logger.exception("Error starting VM") bottle.abort(400, "Error starting VM: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid/stop', method='PUT') def RESTStopVM(infid=None, vmid=None, prop=None): @@ -658,19 +629,15 @@ def RESTStopVM(infid=None, vmid=None, prop=None): return info except DeletedInfrastructureException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except Exception, ex: + logger.exception("Error stopping VM") bottle.abort(400, "Error stopping VM: " + str(ex)) - return False @app.route('/version', method='GET') def RESTGeVersion(): @@ -679,5 +646,5 @@ def RESTGeVersion(): bottle.response.content_type = "text/plain" return version except Exception, ex: + logger.exception("Error getting IM state") bottle.abort(400, "Error getting IM state: " + str(ex)) - return False From 3075da57b68f5d2235d3beb6bc175ff5e91a77a8 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 9 Mar 2016 14:42:31 +0100 Subject: [PATCH 207/509] Show log info in the REST API module --- IM/REST.py | 89 +++++++++++++++++------------------------------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 7108b8f71..757a43b8e 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -14,16 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from InfrastructureInfo import IncorrectVMException, DeletedVMException -from InfrastructureManager import InfrastructureManager, DeletedInfrastructureException, IncorrectInfrastructureException, UnauthorizedUserException -from auth import Authentication +import logging import threading import bottle import json + +from bottle import HTTPError + +from InfrastructureInfo import IncorrectVMException, DeletedVMException +from InfrastructureManager import InfrastructureManager, DeletedInfrastructureException, IncorrectInfrastructureException, UnauthorizedUserException +from auth import Authentication from config import Config from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json from IM.tosca.Tosca import Tosca -from bottle import HTTPError + +logger = logging.getLogger('InfrastructureManager') # Combination of chars used to separate the lines in the AUTH header AUTH_LINE_SEPARATOR = '\\n' @@ -132,13 +137,11 @@ def RESTDestroyInfrastructure(id=None): return "" except DeletedInfrastructureException, ex: bottle.abort(404, "Error Destroying Inf: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Destroying Inf: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Destroying Inf") bottle.abort(400, "Error Destroying Inf: " + str(ex)) - return False @app.route('/infrastructures/:id', method='GET') def RESTGetInfrastructureInfo(id=None): @@ -163,11 +166,10 @@ def RESTGetInfrastructureInfo(id=None): return res except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting Inf. info") bottle.abort(400, "Error Getting Inf. info: " + str(ex)) @app.route('/infrastructures/:id/:prop', method='GET') @@ -202,13 +204,12 @@ def RESTGetInfrastructureProperty(id=None, prop=None): except HTTPError, ex: raise ex except DeletedInfrastructureException, ex: - bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False + bottle.abort(404, "Error Getting Inf. prop: " + str(ex)) except IncorrectInfrastructureException, ex: - bottle.abort(404, "Error Getting Inf. info: " + str(ex)) - return False + bottle.abort(404, "Error Getting Inf. prop: " + str(ex)) except Exception, ex: - bottle.abort(400, "Error Getting Inf. info: " + str(ex)) + logger.exception("Error Getting Inf. prop") + bottle.abort(400, "Error Getting Inf. prop: " + str(ex)) @app.route('/infrastructures', method='GET') def RESTGetInfrastructureList(): @@ -231,10 +232,9 @@ def RESTGetInfrastructureList(): return res except UnauthorizedUserException, ex: bottle.abort(401, "Error Getting Inf. List: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting Inf. List") bottle.abort(400, "Error Getting Inf. List: " + str(ex)) - return False @app.route('/infrastructures', method='POST') @@ -277,10 +277,9 @@ def RESTCreateInfrastructure(): raise ex except UnauthorizedUserException, ex: bottle.abort(401, "Error Getting Inf. info: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Creating Inf.") bottle.abort(400, "Error Creating Inf.: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid', method='GET') def RESTGetVMInfo(infid=None, vmid=None): @@ -313,19 +312,15 @@ def RESTGetVMInfo(infid=None, vmid=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error Getting VM. info: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting VM info") bottle.abort(400, "Error Getting VM info: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid/:prop', method='GET') def RESTGetVMProperty(infid=None, vmid=None, prop=None): @@ -360,19 +355,15 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error Getting VM. property: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Getting VM property") bottle.abort(400, "Error Getting VM property: " + str(ex)) - return False @app.route('/infrastructures/:id', method='POST') def RESTAddResource(id=None): @@ -435,13 +426,11 @@ def RESTAddResource(id=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Adding resources: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Adding resources: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Adding resources") bottle.abort(400, "Error Adding resources: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid', method='DELETE') def RESTRemoveResource(infid=None, vmid=None): @@ -468,19 +457,15 @@ def RESTRemoveResource(infid=None, vmid=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error Removing resources: " + str(ex)) - return False except Exception, ex: + logger.exception("Error Removing resources") bottle.abort(400, "Error Removing resources: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid', method='PUT') def RESTAlterVM(infid=None, vmid=None): @@ -523,19 +508,15 @@ def RESTAlterVM(infid=None, vmid=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error modifying resources: " + str(ex)) - return False except Exception, ex: + logger.exception("Error modifying resources") bottle.abort(400, "Error modifying resources: " + str(ex)) - return False @app.route('/infrastructures/:id/reconfigure', method='PUT') def RESTReconfigureInfrastructure(id=None): @@ -572,13 +553,11 @@ def RESTReconfigureInfrastructure(id=None): raise ex except DeletedInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error reconfiguring infrastructure: " + str(ex)) - return False except Exception, ex: + logger.exception("Error reconfiguring infrastructure") bottle.abort(400, "Error reconfiguring infrastructure: " + str(ex)) - return False @app.route('/infrastructures/:id/start', method='PUT') def RESTStartInfrastructure(id=None): @@ -591,13 +570,11 @@ def RESTStartInfrastructure(id=None): return InfrastructureManager.StartInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error starting infrastructure: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error starting infrastructure: " + str(ex)) - return False except Exception, ex: + logger.exception("Error starting infrastructure") bottle.abort(400, "Error starting infrastructure: " + str(ex)) - return False @app.route('/infrastructures/:id/stop', method='PUT') def RESTStopInfrastructure(id=None): @@ -610,13 +587,11 @@ def RESTStopInfrastructure(id=None): return InfrastructureManager.StopInfrastructure(id, auth) except DeletedInfrastructureException, ex: bottle.abort(404, "Error stopping infrastructure: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error stopping infrastructure: " + str(ex)) - return False except Exception, ex: + logger.exception("Error stopping infrastructure") bottle.abort(400, "Error stopping infrastructure: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid/start', method='PUT') def RESTStartVM(infid=None, vmid=None, prop=None): @@ -631,19 +606,15 @@ def RESTStartVM(infid=None, vmid=None, prop=None): return info except DeletedInfrastructureException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error starting VM: " + str(ex)) - return False except Exception, ex: + logger.exception("Error starting VM") bottle.abort(400, "Error starting VM: " + str(ex)) - return False @app.route('/infrastructures/:infid/vms/:vmid/stop', method='PUT') def RESTStopVM(infid=None, vmid=None, prop=None): @@ -658,19 +629,15 @@ def RESTStopVM(infid=None, vmid=None, prop=None): return info except DeletedInfrastructureException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except IncorrectInfrastructureException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except DeletedVMException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except IncorrectVMException, ex: bottle.abort(404, "Error stopping VM: " + str(ex)) - return False except Exception, ex: + logger.exception("Error stopping VM") bottle.abort(400, "Error stopping VM: " + str(ex)) - return False @app.route('/version', method='GET') def RESTGeVersion(): @@ -679,5 +646,5 @@ def RESTGeVersion(): bottle.response.content_type = "text/plain" return version except Exception, ex: + logger.exception("Error getting IM state") bottle.abort(400, "Error getting IM state: " + str(ex)) - return False From c4f183bccceaa361d6148c12cd4fa7eba0e15ce9 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:25:57 +0100 Subject: [PATCH 208/509] Add IM_NODE_CLOUD_TYPE var --- IM/ConfManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 115bc661b..62f0d2e94 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -392,6 +392,7 @@ def generate_inventory(self, tmp_dir): node_line += ' IM_NODE_DOMAIN=' + nodedom node_line += ' IM_NODE_NUM=' + str(vm.im_id) node_line += ' IM_NODE_VMID=' + str(vm.id) + node_line += ' IM_NODE_CLOUD_TYPE=' + vm.cloud.type node_line += ifaces_im_vars for app in vm.getInstalledApplications(): From 9e6474c37602e4d1ac43639ce964d4b1410dccc2 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:25:57 +0100 Subject: [PATCH 209/509] Add IM_NODE_CLOUD_TYPE var --- IM/ConfManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 115bc661b..62f0d2e94 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -392,6 +392,7 @@ def generate_inventory(self, tmp_dir): node_line += ' IM_NODE_DOMAIN=' + nodedom node_line += ' IM_NODE_NUM=' + str(vm.im_id) node_line += ' IM_NODE_VMID=' + str(vm.id) + node_line += ' IM_NODE_CLOUD_TYPE=' + vm.cloud.type node_line += ifaces_im_vars for app in vm.getInstalledApplications(): From f70ceac4ecd8b2050017f11e046c0f30fe65d761 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:26:09 +0100 Subject: [PATCH 210/509] Update VM status in OpenNebula --- IM/connectors/OpenNebula.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 4cee92776..d8a76bcc8 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -233,23 +233,29 @@ def updateVMInfo(self, vm, auth_data): vm.info.systems[0].setValue('instance_name', res_vm.NAME) # update the state of the VM - if res_vm.STATE == 3: - if res_vm.LCM_STATE == 3: - res_state = VirtualMachine.RUNNING + if res_vm.STATE < 3 : + res_state = VirtualMachine.PENDING + elif res_vm.STATE == 3: + if res_vm.LCM_STATE < 3: + res_state = VirtualMachine.PENDING elif res_vm.LCM_STATE == 5 or res_vm.LCM_STATE == 6: res_state = VirtualMachine.STOPPED + elif res_vm.LCM_STATE == 14: + res_state = VirtualMachine.FAILED + elif res_vm.LCM_STATE == 16: + res_state = VirtualMachine.UNKNOWN + elif res_vm.LCM_STATE == 12 or res_vm.LCM_STATE == 13 or res_vm.LCM_STATE == 18: + res_state = VirtualMachine.OFF else: - res_state = VirtualMachine.PENDING - elif res_vm.STATE < 3 : - res_state = VirtualMachine.PENDING - elif res_vm.LCM_STATE == 15: - res_state = VirtualMachine.UNKNOWN - elif res_vm.STATE == 7: - res_state = VirtualMachine.FAILED + res_state = VirtualMachine.RUNNING elif res_vm.STATE == 4 or res_vm.STATE == 5: res_state = VirtualMachine.STOPPED - else: + elif res_vm.STATE == 7: + res_state = VirtualMachine.FAILED + elif res_vm.STATE == 6 or res_vm.STATE == 8 or res_vm.STATE == 9: res_state = VirtualMachine.OFF + else: + res_state = VirtualMachine.UNKNOWN vm.state = res_state # Update network data From 9c80db783383806894298c61ee5521f4d8758926 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:26:09 +0100 Subject: [PATCH 211/509] Update VM status in OpenNebula --- IM/connectors/OpenNebula.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 4cee92776..d8a76bcc8 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -233,23 +233,29 @@ def updateVMInfo(self, vm, auth_data): vm.info.systems[0].setValue('instance_name', res_vm.NAME) # update the state of the VM - if res_vm.STATE == 3: - if res_vm.LCM_STATE == 3: - res_state = VirtualMachine.RUNNING + if res_vm.STATE < 3 : + res_state = VirtualMachine.PENDING + elif res_vm.STATE == 3: + if res_vm.LCM_STATE < 3: + res_state = VirtualMachine.PENDING elif res_vm.LCM_STATE == 5 or res_vm.LCM_STATE == 6: res_state = VirtualMachine.STOPPED + elif res_vm.LCM_STATE == 14: + res_state = VirtualMachine.FAILED + elif res_vm.LCM_STATE == 16: + res_state = VirtualMachine.UNKNOWN + elif res_vm.LCM_STATE == 12 or res_vm.LCM_STATE == 13 or res_vm.LCM_STATE == 18: + res_state = VirtualMachine.OFF else: - res_state = VirtualMachine.PENDING - elif res_vm.STATE < 3 : - res_state = VirtualMachine.PENDING - elif res_vm.LCM_STATE == 15: - res_state = VirtualMachine.UNKNOWN - elif res_vm.STATE == 7: - res_state = VirtualMachine.FAILED + res_state = VirtualMachine.RUNNING elif res_vm.STATE == 4 or res_vm.STATE == 5: res_state = VirtualMachine.STOPPED - else: + elif res_vm.STATE == 7: + res_state = VirtualMachine.FAILED + elif res_vm.STATE == 6 or res_vm.STATE == 8 or res_vm.STATE == 9: res_state = VirtualMachine.OFF + else: + res_state = VirtualMachine.UNKNOWN vm.state = res_state # Update network data From 58644d17766124d409174721d87a628c20dfa2d1 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:26:58 +0100 Subject: [PATCH 212/509] Add extra_ports in FogBow connector --- IM/connectors/FogBow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/IM/connectors/FogBow.py b/IM/connectors/FogBow.py index 527d9590a..1534efc94 100644 --- a/IM/connectors/FogBow.py +++ b/IM/connectors/FogBow.py @@ -212,7 +212,11 @@ def updateVMInfo(self, vm, auth_data): vm.setIps([parts[0]], []) if len(parts) > 1: vm.setSSHPort(int(parts[1])) - + + extra_ports = self.get_occi_attribute_value(output, 'org.fogbowcloud.request.extra-ports') + if extra_ports: + vm.info.systems[0].addFeature(Feature("fogbow.extra-ports", "=", extra_ports), conflict="other", missing="other") + ssh_user = self.get_occi_attribute_value(output, 'org.fogbowcloud.request.ssh-username') if ssh_user: vm.info.systems[0].addFeature(Feature("disk.0.os.credentials.username", "=", ssh_user), conflict="other", missing="other") From 184baf88d8c1ab0aca0e43e82fc0620d8301f1b4 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:26:58 +0100 Subject: [PATCH 213/509] Add extra_ports in FogBow connector --- IM/connectors/FogBow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/IM/connectors/FogBow.py b/IM/connectors/FogBow.py index 527d9590a..1534efc94 100644 --- a/IM/connectors/FogBow.py +++ b/IM/connectors/FogBow.py @@ -212,7 +212,11 @@ def updateVMInfo(self, vm, auth_data): vm.setIps([parts[0]], []) if len(parts) > 1: vm.setSSHPort(int(parts[1])) - + + extra_ports = self.get_occi_attribute_value(output, 'org.fogbowcloud.request.extra-ports') + if extra_ports: + vm.info.systems[0].addFeature(Feature("fogbow.extra-ports", "=", extra_ports), conflict="other", missing="other") + ssh_user = self.get_occi_attribute_value(output, 'org.fogbowcloud.request.ssh-username') if ssh_user: vm.info.systems[0].addFeature(Feature("disk.0.os.credentials.username", "=", ssh_user), conflict="other", missing="other") From 7d85eb582ef190d359d12bcf4d05acdaca3a2eac Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:27:31 +0100 Subject: [PATCH 214/509] Enable password access in OCCI conector --- IM/connectors/OCCI.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/IM/connectors/OCCI.py b/IM/connectors/OCCI.py index 690af69c1..bb3ec27df 100644 --- a/IM/connectors/OCCI.py +++ b/IM/connectors/OCCI.py @@ -546,23 +546,30 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): i = 0 public_key = system.getValue('disk.0.os.credentials.public_key') + password = system.getValue('disk.0.os.credentials.password') - if not public_key: - # We must generate them - (public_key, private_key) = self.keygen() - system.setValue('disk.0.os.credentials.private_key', private_key) + if public_key: + if password: + system.delValue('disk.0.os.credentials.password') + password = None + else: + if not password: + # We must generate them + (public_key, private_key) = self.keygen() + system.setValue('disk.0.os.credentials.private_key', private_key) user = system.getValue('disk.0.os.credentials.username') if not user: user = "cloudadm" system.setValue('disk.0.os.credentials.username', user) + user_data = "" + if public_key: # Add user cloud init data - cloud_config_str = self.get_cloud_init_data(radl) - cloud_config = self.gen_cloud_config(public_key, user, cloud_config_str) - user_data = base64.b64encode(cloud_config).replace("\n","") - - self.logger.debug("Cloud init: " + cloud_config) + cloud_config_str = self.get_cloud_init_data(radl) + cloud_config = self.gen_cloud_config(public_key, user, cloud_config_str) + user_data = base64.b64encode(cloud_config).replace("\n","") + self.logger.debug("Cloud init: " + cloud_config) # Get the info about the OCCI server (GET /-/) occi_info = self.query_occi(auth_data) @@ -623,7 +630,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): # See: https://wiki.egi.eu/wiki/HOWTO10 #body += 'X-OCCI-Attribute: org.openstack.credentials.publickey.name="my_key"' #body += 'X-OCCI-Attribute: org.openstack.credentials.publickey.data="ssh-rsa BAA...zxe ==user@host"' - body += 'X-OCCI-Attribute: org.openstack.compute.user_data="' + user_data + '"\n' + if user_data: + body += 'X-OCCI-Attribute: org.openstack.compute.user_data="' + user_data + '"\n' # Add volume links for device, volume_id in volumes.iteritems(): @@ -819,9 +827,9 @@ def get_keystone_uri(occi, auth_data): return www_auth_head.split('=')[1].replace("'","") else: return None - except SSLError: + except SSLError, ex: occi.logger.exception("Error with the credentials when contacting with the OCCI server.") - raise Exception("Error with the credentials when contacting with the OCCI server. Check your proxy file.") + raise Exception("Error with the credentials when contacting with the OCCI server: %s. Check your proxy file." % str(ex)) except: occi.logger.exception("Error contacting with the OCCI server.") return None @@ -892,4 +900,4 @@ def get_keystone_token(occi, keystone_uri, auth): occi.logger.exception("Error obtaining Keystone Token.") return None finally: - occi.delete_proxy(conn) + occi.delete_proxy(conn) \ No newline at end of file From 1917907ed405842a032021c51f6872f5895ec7f6 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:27:31 +0100 Subject: [PATCH 215/509] Enable password access in OCCI conector --- IM/connectors/OCCI.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/IM/connectors/OCCI.py b/IM/connectors/OCCI.py index 690af69c1..bb3ec27df 100644 --- a/IM/connectors/OCCI.py +++ b/IM/connectors/OCCI.py @@ -546,23 +546,30 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): i = 0 public_key = system.getValue('disk.0.os.credentials.public_key') + password = system.getValue('disk.0.os.credentials.password') - if not public_key: - # We must generate them - (public_key, private_key) = self.keygen() - system.setValue('disk.0.os.credentials.private_key', private_key) + if public_key: + if password: + system.delValue('disk.0.os.credentials.password') + password = None + else: + if not password: + # We must generate them + (public_key, private_key) = self.keygen() + system.setValue('disk.0.os.credentials.private_key', private_key) user = system.getValue('disk.0.os.credentials.username') if not user: user = "cloudadm" system.setValue('disk.0.os.credentials.username', user) + user_data = "" + if public_key: # Add user cloud init data - cloud_config_str = self.get_cloud_init_data(radl) - cloud_config = self.gen_cloud_config(public_key, user, cloud_config_str) - user_data = base64.b64encode(cloud_config).replace("\n","") - - self.logger.debug("Cloud init: " + cloud_config) + cloud_config_str = self.get_cloud_init_data(radl) + cloud_config = self.gen_cloud_config(public_key, user, cloud_config_str) + user_data = base64.b64encode(cloud_config).replace("\n","") + self.logger.debug("Cloud init: " + cloud_config) # Get the info about the OCCI server (GET /-/) occi_info = self.query_occi(auth_data) @@ -623,7 +630,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): # See: https://wiki.egi.eu/wiki/HOWTO10 #body += 'X-OCCI-Attribute: org.openstack.credentials.publickey.name="my_key"' #body += 'X-OCCI-Attribute: org.openstack.credentials.publickey.data="ssh-rsa BAA...zxe ==user@host"' - body += 'X-OCCI-Attribute: org.openstack.compute.user_data="' + user_data + '"\n' + if user_data: + body += 'X-OCCI-Attribute: org.openstack.compute.user_data="' + user_data + '"\n' # Add volume links for device, volume_id in volumes.iteritems(): @@ -819,9 +827,9 @@ def get_keystone_uri(occi, auth_data): return www_auth_head.split('=')[1].replace("'","") else: return None - except SSLError: + except SSLError, ex: occi.logger.exception("Error with the credentials when contacting with the OCCI server.") - raise Exception("Error with the credentials when contacting with the OCCI server. Check your proxy file.") + raise Exception("Error with the credentials when contacting with the OCCI server: %s. Check your proxy file." % str(ex)) except: occi.logger.exception("Error contacting with the OCCI server.") return None @@ -892,4 +900,4 @@ def get_keystone_token(occi, keystone_uri, auth): occi.logger.exception("Error obtaining Keystone Token.") return None finally: - occi.delete_proxy(conn) + occi.delete_proxy(conn) \ No newline at end of file From fe6b532e56475b18fa146e0cd266986c27be45bd Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:28:49 +0100 Subject: [PATCH 216/509] Add support for onedata clients --- IM/tosca/Tosca.py | 69 +++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index d05a2f4d6..11f3c4836 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -44,9 +44,9 @@ def to_radl(self, inf_info = None): relationships = [] for node in self.tosca.nodetemplates: # Store relationships to check later - for relationship, target in node.relationships.iteritems(): - source = node - relationships.append((source, target, relationship)) + for relationship, trgt in node.relationships.iteritems(): + src = node + relationships.append((src, trgt, relationship)) radl = RADL() interfaces = {} @@ -897,13 +897,14 @@ def _gen_system(node, nodetemplates): # Find associated BlockStorages disks = Tosca._get_attached_disks(node, nodetemplates) - for size, unit, location, device, num in disks: - res.setValue('disk.%d.size' % num, size, unit) + for size, unit, location, device, num, fstype in disks: + if size: + res.setValue('disk.%d.size' % num, size, unit) if device: res.setValue('disk.%d.device' % num, device) if location: res.setValue('disk.%d.mount_path' % num, location) - res.setValue('disk.%d.fstype' % num, "ext4") + res.setValue('disk.%d.fstype' % num, fstype) return res @@ -936,43 +937,33 @@ def _get_attached_disks(node, nodetemplates): """ disks = [] count = 1 - for requires in node.requirements: - for value in requires.values(): + + for rel, trgt in node.relationships.iteritems(): + src = node + rel_tpl = Tosca._get_relationship_template(rel, src, trgt) + # TODO: ver root_type + if rel.type.endswith("AttachesTo"): + rel_tpl.entity_tpl + props = rel_tpl.get_properties_objects() + size = None location = None - device = None - if isinstance(value, dict): - if 'relationship' in value: - rel = value.get('relationship') - - rel_type = None - if isinstance(rel, dict) and 'type' in rel: - rel_type = rel.get('type') - else: - rel_type = rel - - if rel_type and rel_type.endswith("AttachesTo"): - if isinstance(rel, dict) and 'properties' in rel: - prop = rel.get('properties') - if isinstance(prop, dict): - location = prop.get('location', None) - device = prop.get('device', None) - - # seet a default device - if not device: - device = "hdb" + # set a default device + device = "hdb" + + for prop in props: + if prop.name == "location": + location = prop.value + elif prop.name == "device": + device = prop.value - for node_name in value.values(): - for n in nodetemplates: - if n.name == node_name: - size, unit = Tosca._get_size_and_unit(n.get_property_value('size')) - break - - disks.append((size, unit, location, device, count)) - count += 1 + if trgt.type_definition.type == "tosca.nodes.BlockStorage": + size, unit = Tosca._get_size_and_unit(trgt.get_property_value('size')) + disks.append((size, unit, location, device, count, "ext4")) + count += 1 else: - Tosca.logger.error("ERROR: expected dict in requires values.") - + Tosca.logger.debug("Attached item of type %s ignored." % trgt.type_definition.type) + return disks @staticmethod From 80b87901b62d5a2b13e93e7154c7eede1739c80b Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 14 Mar 2016 11:28:49 +0100 Subject: [PATCH 217/509] Add support for onedata clients --- IM/tosca/Tosca.py | 69 +++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index d05a2f4d6..11f3c4836 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -44,9 +44,9 @@ def to_radl(self, inf_info = None): relationships = [] for node in self.tosca.nodetemplates: # Store relationships to check later - for relationship, target in node.relationships.iteritems(): - source = node - relationships.append((source, target, relationship)) + for relationship, trgt in node.relationships.iteritems(): + src = node + relationships.append((src, trgt, relationship)) radl = RADL() interfaces = {} @@ -897,13 +897,14 @@ def _gen_system(node, nodetemplates): # Find associated BlockStorages disks = Tosca._get_attached_disks(node, nodetemplates) - for size, unit, location, device, num in disks: - res.setValue('disk.%d.size' % num, size, unit) + for size, unit, location, device, num, fstype in disks: + if size: + res.setValue('disk.%d.size' % num, size, unit) if device: res.setValue('disk.%d.device' % num, device) if location: res.setValue('disk.%d.mount_path' % num, location) - res.setValue('disk.%d.fstype' % num, "ext4") + res.setValue('disk.%d.fstype' % num, fstype) return res @@ -936,43 +937,33 @@ def _get_attached_disks(node, nodetemplates): """ disks = [] count = 1 - for requires in node.requirements: - for value in requires.values(): + + for rel, trgt in node.relationships.iteritems(): + src = node + rel_tpl = Tosca._get_relationship_template(rel, src, trgt) + # TODO: ver root_type + if rel.type.endswith("AttachesTo"): + rel_tpl.entity_tpl + props = rel_tpl.get_properties_objects() + size = None location = None - device = None - if isinstance(value, dict): - if 'relationship' in value: - rel = value.get('relationship') - - rel_type = None - if isinstance(rel, dict) and 'type' in rel: - rel_type = rel.get('type') - else: - rel_type = rel - - if rel_type and rel_type.endswith("AttachesTo"): - if isinstance(rel, dict) and 'properties' in rel: - prop = rel.get('properties') - if isinstance(prop, dict): - location = prop.get('location', None) - device = prop.get('device', None) - - # seet a default device - if not device: - device = "hdb" + # set a default device + device = "hdb" + + for prop in props: + if prop.name == "location": + location = prop.value + elif prop.name == "device": + device = prop.value - for node_name in value.values(): - for n in nodetemplates: - if n.name == node_name: - size, unit = Tosca._get_size_and_unit(n.get_property_value('size')) - break - - disks.append((size, unit, location, device, count)) - count += 1 + if trgt.type_definition.type == "tosca.nodes.BlockStorage": + size, unit = Tosca._get_size_and_unit(trgt.get_property_value('size')) + disks.append((size, unit, location, device, count, "ext4")) + count += 1 else: - Tosca.logger.error("ERROR: expected dict in requires values.") - + Tosca.logger.debug("Attached item of type %s ignored." % trgt.type_definition.type) + return disks @staticmethod From b2ba1c2fd4487a80c6a962db19c093e9e1fc8361 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 12:25:09 +0100 Subject: [PATCH 218/509] Enable to user image name and add IMAGE_UNAME config variable for OpenNebula conector --- IM/config.py | 1 + IM/connectors/OpenNebula.py | 9 ++++++++- etc/im.cfg | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/IM/config.py b/IM/config.py index ef2561857..a968951ac 100644 --- a/IM/config.py +++ b/IM/config.py @@ -93,6 +93,7 @@ class Config: class ConfigOpenNebula: TEMPLATE_CONTEXT = '' TEMPLATE_OTHER = 'GRAPHICS = [type="vnc",listen="0.0.0.0"]' + IMAGE_UNAME = '' if config.has_section("OpenNebula"): parse_options(config, 'OpenNebula', ConfigOpenNebula) \ No newline at end of file diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index d8a76bcc8..413b7556d 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -389,7 +389,14 @@ def getONETemplate(self, radl, auth_data): url = uriparse(system.getValue("disk.0.image.url")) path = url[2] - disks = 'DISK = [ IMAGE_ID = "%s" ]' % path[1:] + if path[1:].isdigit(): + disks = 'DISK = [ IMAGE_ID = "%s" ]' % path[1:] + else: + if ConfigOpenNebula.IMAGE_UNAME: + # This only works if the user owns the image + disks = 'DISK = [ IMAGE = "%s" ]' % path[1:] + else: + disks = 'DISK = [ IMAGE = "%s", IMAGE_UNAME = "%s" ]' % (path[1:], ConfigOpenNebula.IMAGE_UNAME) cont = 1 while system.getValue("disk." + str(cont) + ".image.url") or (system.getValue("disk." + str(cont) + ".size") and system.getValue("disk." + str(cont) + ".device")): disk_image = system.getValue("disk." + str(cont) + ".image.url") diff --git a/etc/im.cfg b/etc/im.cfg index 7de9274aa..d453196f5 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -109,5 +109,7 @@ PLAYBOOK_RETRIES = 3 TEMPLATE_CONTEXT = # Text to add to the ONE Template different to NAME, CPU, VCPU, MEMORY, OS, DISK and CONTEXT TEMPLATE_OTHER = GRAPHICS = [type="vnc",listen="0.0.0.0", keymap="es"] +# Set the IMAGE_UNAME value in case of using the name of the disk image in the Template +IMAGE_UNAME = oneadmin From 68f90045dbea05c6903ea5fe1df00f9285ffabec Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 12:25:09 +0100 Subject: [PATCH 219/509] Enable to user image name and add IMAGE_UNAME config variable for OpenNebula conector --- IM/config.py | 1 + IM/connectors/OpenNebula.py | 9 ++++++++- etc/im.cfg | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/IM/config.py b/IM/config.py index ef2561857..a968951ac 100644 --- a/IM/config.py +++ b/IM/config.py @@ -93,6 +93,7 @@ class Config: class ConfigOpenNebula: TEMPLATE_CONTEXT = '' TEMPLATE_OTHER = 'GRAPHICS = [type="vnc",listen="0.0.0.0"]' + IMAGE_UNAME = '' if config.has_section("OpenNebula"): parse_options(config, 'OpenNebula', ConfigOpenNebula) \ No newline at end of file diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index d8a76bcc8..413b7556d 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -389,7 +389,14 @@ def getONETemplate(self, radl, auth_data): url = uriparse(system.getValue("disk.0.image.url")) path = url[2] - disks = 'DISK = [ IMAGE_ID = "%s" ]' % path[1:] + if path[1:].isdigit(): + disks = 'DISK = [ IMAGE_ID = "%s" ]' % path[1:] + else: + if ConfigOpenNebula.IMAGE_UNAME: + # This only works if the user owns the image + disks = 'DISK = [ IMAGE = "%s" ]' % path[1:] + else: + disks = 'DISK = [ IMAGE = "%s", IMAGE_UNAME = "%s" ]' % (path[1:], ConfigOpenNebula.IMAGE_UNAME) cont = 1 while system.getValue("disk." + str(cont) + ".image.url") or (system.getValue("disk." + str(cont) + ".size") and system.getValue("disk." + str(cont) + ".device")): disk_image = system.getValue("disk." + str(cont) + ".image.url") diff --git a/etc/im.cfg b/etc/im.cfg index 7de9274aa..d453196f5 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -109,5 +109,7 @@ PLAYBOOK_RETRIES = 3 TEMPLATE_CONTEXT = # Text to add to the ONE Template different to NAME, CPU, VCPU, MEMORY, OS, DISK and CONTEXT TEMPLATE_OTHER = GRAPHICS = [type="vnc",listen="0.0.0.0", keymap="es"] +# Set the IMAGE_UNAME value in case of using the name of the disk image in the Template +IMAGE_UNAME = oneadmin From 2f3232ec4ad06beb9a5276fd77fd912106e10177 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 12:25:57 +0100 Subject: [PATCH 220/509] Add suport in TOSCA for specifying the image and credentials --- IM/tosca/Tosca.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 11f3c4836..c5dca7874 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -867,6 +867,8 @@ def _gen_system(node, nodetemplates): 'type':'disk.0.os.name', 'distribution':'disk.0.os.flavour', 'version': 'disk.0.os.version', + 'image': 'disk.0.image.url', + 'credential': 'disk.0.os.credentials', 'num_cpus': 'cpu.count', 'disk_size': 'disk.0.size', 'mem_size': 'memory.size', @@ -882,9 +884,20 @@ def _gen_system(node, nodetemplates): value = prop.value if prop.name in ['disk_size', 'mem_size']: value, unit = Tosca._get_size_and_unit(prop.value) - - if prop.name == "version": - value= str(value) + elif prop.name == "version": + value = str(value) + elif prop.name == "image": + if value.find("://") == -1: + value = "docker://%s" % value + elif prop.name == "credential": + # Currently oly supports user/pass credentials + if 'token' in value and value['token']: + feature = Feature("disk.0.os.credentials.password", "=", value['token']) + res.addFeature(feature) + if not 'user' in value or not value['user']: + raise Exception("User must be specified in the image credentials.") + name = "disk.0.os.credentials.username" + value = value['user'] if isinstance(value, float) or isinstance(value, int): operator = ">=" From 1f8392b70f3cbd5e132668a38bd0cc268be9b783 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 12:25:57 +0100 Subject: [PATCH 221/509] Add suport in TOSCA for specifying the image and credentials --- IM/tosca/Tosca.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 11f3c4836..c5dca7874 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -867,6 +867,8 @@ def _gen_system(node, nodetemplates): 'type':'disk.0.os.name', 'distribution':'disk.0.os.flavour', 'version': 'disk.0.os.version', + 'image': 'disk.0.image.url', + 'credential': 'disk.0.os.credentials', 'num_cpus': 'cpu.count', 'disk_size': 'disk.0.size', 'mem_size': 'memory.size', @@ -882,9 +884,20 @@ def _gen_system(node, nodetemplates): value = prop.value if prop.name in ['disk_size', 'mem_size']: value, unit = Tosca._get_size_and_unit(prop.value) - - if prop.name == "version": - value= str(value) + elif prop.name == "version": + value = str(value) + elif prop.name == "image": + if value.find("://") == -1: + value = "docker://%s" % value + elif prop.name == "credential": + # Currently oly supports user/pass credentials + if 'token' in value and value['token']: + feature = Feature("disk.0.os.credentials.password", "=", value['token']) + res.addFeature(feature) + if not 'user' in value or not value['user']: + raise Exception("User must be specified in the image credentials.") + name = "disk.0.os.credentials.username" + value = value['user'] if isinstance(value, float) or isinstance(value, int): operator = ">=" From ed14c12f4d5966e625ee6c4cb9883ab8c4bf8971 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 12:49:23 +0100 Subject: [PATCH 222/509] Add new org.fogbowcloud.order.resource-kind occi attribute --- IM/connectors/FogBow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IM/connectors/FogBow.py b/IM/connectors/FogBow.py index 1534efc94..2383e6dfe 100644 --- a/IM/connectors/FogBow.py +++ b/IM/connectors/FogBow.py @@ -270,6 +270,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.request.instance-count=1') conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.request.type="one-time"') + conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.order.resource-kind="compute"') requirements = "" if system.getValue('instance_type'): @@ -466,4 +467,4 @@ def create_token(params): except: return None else: - raise Exception("Incorrect auth data, auth_url, username, password and tenant must be specified") \ No newline at end of file + raise Exception("Incorrect auth data, auth_url, username, password and tenant must be specified") From 84ae2c7ceedd3d2fc74c813b7df4c4b41c43717e Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 12:49:23 +0100 Subject: [PATCH 223/509] Add new org.fogbowcloud.order.resource-kind occi attribute --- IM/connectors/FogBow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IM/connectors/FogBow.py b/IM/connectors/FogBow.py index 1534efc94..2383e6dfe 100644 --- a/IM/connectors/FogBow.py +++ b/IM/connectors/FogBow.py @@ -270,6 +270,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.request.instance-count=1') conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.request.type="one-time"') + conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.order.resource-kind="compute"') requirements = "" if system.getValue('instance_type'): @@ -466,4 +467,4 @@ def create_token(params): except: return None else: - raise Exception("Incorrect auth data, auth_url, username, password and tenant must be specified") \ No newline at end of file + raise Exception("Incorrect auth data, auth_url, username, password and tenant must be specified") From ef98d3c4ff9c949befb4677982e2966defa65e0a Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 15:13:21 +0100 Subject: [PATCH 224/509] Add support for credential attribute --- IM/tosca/Tosca.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index c5dca7874..fad548386 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -548,6 +548,22 @@ def _get_attribute_result(self, func, node, inf_info): return vm.id elif attribute_name == "tosca_name": return node.name + elif attribute_name == "credential": + if node.type == "tosca.nodes.indigo.Compute": + res = [] + for vm in vm_list[node.name]: + user, password, _, private_key = vm.getCredentialValues() + val = {"user" : user} + if password: + val["password"] = password + if private_key: + val["private_key"] = private_key + res.append(val) + if index is not None: + res = res[index] + return res + else: + return vm.getPrivateIP() elif attribute_name == "private_address": if node.type == "tosca.nodes.indigo.Compute": res = [vm.getPrivateIP() for vm in vm_list[node.name]] From 2bbf6d6884dd676dcbee48c48f956f92ffa7d2a7 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 15 Mar 2016 15:13:21 +0100 Subject: [PATCH 225/509] Add support for credential attribute --- IM/tosca/Tosca.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index c5dca7874..fad548386 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -548,6 +548,22 @@ def _get_attribute_result(self, func, node, inf_info): return vm.id elif attribute_name == "tosca_name": return node.name + elif attribute_name == "credential": + if node.type == "tosca.nodes.indigo.Compute": + res = [] + for vm in vm_list[node.name]: + user, password, _, private_key = vm.getCredentialValues() + val = {"user" : user} + if password: + val["password"] = password + if private_key: + val["private_key"] = private_key + res.append(val) + if index is not None: + res = res[index] + return res + else: + return vm.getPrivateIP() elif attribute_name == "private_address": if node.type == "tosca.nodes.indigo.Compute": res = [vm.getPrivateIP() for vm in vm_list[node.name]] From d9fd6ab31dd5ed581e0c8150e36207dccf595e89 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Mar 2016 09:16:13 +0100 Subject: [PATCH 226/509] If there are no configuration recipes, disable contextualization step --- IM/tosca/Tosca.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index fad548386..9d09ac354 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -112,6 +112,9 @@ def to_radl(self, inf_info = None): if cont_intems: radl.contextualize = contextualize(cont_intems) + else: + # If there are no configures, disable contextualization + radl.contextualize = contextualize({}) return all_removal_list, self._complete_radl_networks(radl) From 1d14e8f4e53153603eae11a897762bd270993f8e Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 16 Mar 2016 09:16:13 +0100 Subject: [PATCH 227/509] If there are no configuration recipes, disable contextualization step --- IM/tosca/Tosca.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index fad548386..9d09ac354 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -112,6 +112,9 @@ def to_radl(self, inf_info = None): if cont_intems: radl.contextualize = contextualize(cont_intems) + else: + # If there are no configures, disable contextualization + radl.contextualize = contextualize({}) return all_removal_list, self._complete_radl_networks(radl) From 0ddfa7cd9e358f1b7d853ad51a4807c2af9c25d5 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 21 Mar 2016 09:19:52 +0100 Subject: [PATCH 228/509] Return the credentials in correct TOSCA format --- IM/tosca/Tosca.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 9d09ac354..e9e897e62 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -558,9 +558,10 @@ def _get_attribute_result(self, func, node, inf_info): user, password, _, private_key = vm.getCredentialValues() val = {"user" : user} if password: - val["password"] = password + val["token"] = password if private_key: - val["private_key"] = private_key + val["token_type"] = "private_key" + val["token"] = private_key res.append(val) if index is not None: res = res[index] From 1c0256505b6bb56dfa5821ae25477338d752a281 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 21 Mar 2016 09:19:52 +0100 Subject: [PATCH 229/509] Return the credentials in correct TOSCA format --- IM/tosca/Tosca.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 9d09ac354..e9e897e62 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -558,9 +558,10 @@ def _get_attribute_result(self, func, node, inf_info): user, password, _, private_key = vm.getCredentialValues() val = {"user" : user} if password: - val["password"] = password + val["token"] = password if private_key: - val["private_key"] = private_key + val["token_type"] = "private_key" + val["token"] = private_key res.append(val) if index is not None: res = res[index] From 6dc7d42f7c4b1adbaaa46c0a65bc2c2307b6760f Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 22 Mar 2016 17:07:32 +0100 Subject: [PATCH 230/509] Add remove header function to recipes and add suport to galaxy roles in TOSCA --- IM/tosca/Tosca.py | 53 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index e9e897e62..3d08d4748 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -9,7 +9,7 @@ from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute -from radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from radl.radl import system, deploy, network, Feature, Features, configure, contextualize_item, RADL, contextualize class Tosca: """ @@ -361,6 +361,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): variables += ' %s: "%s" ' % (var_name, var_value) + "\n" variables += "\n" + script_content = self._remove_recipe_header(script_content) recipe_list.append(script_content) else: recipe = "- tasks:\n" @@ -395,6 +396,29 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): else: return None + def _remove_recipe_header(self, script_content): + """ + Removes the "host" and "connection" elements from the recipe + to make it "RADL" compatible + """ + + try: + yamlo = yaml.load(script_content) + if not isinstance(yamlo, list): + Tosca.logger.warn("Error parsing YAML: " + script_content + "\n.Do not remove header.") + return script_content + except Exception: + Tosca.logger.exception("Error parsing YAML: " + script_content + "\n.Do not remove header.") + return script_content + + for elem in yamlo: + if 'host' in elem: + del elem['host'] + if 'connection' in elem: + del elem['connection'] + + return yaml.dump(yamlo, default_flow_style=False, explicit_start=True, width=256) + @staticmethod def _is_artifact(function): """Returns True if the provided function is a Tosca get_artifact function. @@ -873,7 +897,30 @@ def _gen_network(node): res.setValue("provider_id", network_name) return res - + + @staticmethod + def _add_ansible_roles(node, nodetemplates, system): + for other_node in nodetemplates: + root_type = Tosca._get_root_parent_type(other_node).type + if root_type == "tosca.nodes.Compute": + compute = other_node + else: + # Select the host to host this element + compute = Tosca._find_host_compute(other_node, nodetemplates) + + if compute and compute.name == node.name: + # Get the artifacts to see if there is a ansible galaxy role + # and add it as an "ansible.modules" app requirement in RADL + artifacts = other_node.type_definition.get_value('artifacts',other_node.entity_tpl,True) + if artifacts: + for name, artifact in artifacts.items(): + name + if ('type' in artifact and artifact['type'] == 'tosca.artifacts.AnsibleGalaxy.role' and + 'file' in artifact and artifact['file']): + app_features = Features() + app_features.addFeature(Feature('name', '=', 'ansible.modules.' + artifact['file'])) + feature = Feature('disk.0.applications', 'contains', app_features) + system.addFeature(feature) @staticmethod def _gen_system(node, nodetemplates): @@ -939,6 +986,8 @@ def _gen_system(node, nodetemplates): res.setValue('disk.%d.mount_path' % num, location) res.setValue('disk.%d.fstype' % num, fstype) + Tosca._add_ansible_roles(node, nodetemplates, res) + return res @staticmethod From 066d3a45977a97c39d92a97b387569e1c11d79b6 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 22 Mar 2016 17:07:32 +0100 Subject: [PATCH 231/509] Add remove header function to recipes and add suport to galaxy roles in TOSCA --- IM/tosca/Tosca.py | 53 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index e9e897e62..3d08d4748 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -9,7 +9,7 @@ from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef from toscaparser.functions import Function, is_function, get_function, GetAttribute -from radl.radl import system, deploy, network, Feature, configure, contextualize_item, RADL, contextualize +from radl.radl import system, deploy, network, Feature, Features, configure, contextualize_item, RADL, contextualize class Tosca: """ @@ -361,6 +361,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): variables += ' %s: "%s" ' % (var_name, var_value) + "\n" variables += "\n" + script_content = self._remove_recipe_header(script_content) recipe_list.append(script_content) else: recipe = "- tasks:\n" @@ -395,6 +396,29 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): else: return None + def _remove_recipe_header(self, script_content): + """ + Removes the "host" and "connection" elements from the recipe + to make it "RADL" compatible + """ + + try: + yamlo = yaml.load(script_content) + if not isinstance(yamlo, list): + Tosca.logger.warn("Error parsing YAML: " + script_content + "\n.Do not remove header.") + return script_content + except Exception: + Tosca.logger.exception("Error parsing YAML: " + script_content + "\n.Do not remove header.") + return script_content + + for elem in yamlo: + if 'host' in elem: + del elem['host'] + if 'connection' in elem: + del elem['connection'] + + return yaml.dump(yamlo, default_flow_style=False, explicit_start=True, width=256) + @staticmethod def _is_artifact(function): """Returns True if the provided function is a Tosca get_artifact function. @@ -873,7 +897,30 @@ def _gen_network(node): res.setValue("provider_id", network_name) return res - + + @staticmethod + def _add_ansible_roles(node, nodetemplates, system): + for other_node in nodetemplates: + root_type = Tosca._get_root_parent_type(other_node).type + if root_type == "tosca.nodes.Compute": + compute = other_node + else: + # Select the host to host this element + compute = Tosca._find_host_compute(other_node, nodetemplates) + + if compute and compute.name == node.name: + # Get the artifacts to see if there is a ansible galaxy role + # and add it as an "ansible.modules" app requirement in RADL + artifacts = other_node.type_definition.get_value('artifacts',other_node.entity_tpl,True) + if artifacts: + for name, artifact in artifacts.items(): + name + if ('type' in artifact and artifact['type'] == 'tosca.artifacts.AnsibleGalaxy.role' and + 'file' in artifact and artifact['file']): + app_features = Features() + app_features.addFeature(Feature('name', '=', 'ansible.modules.' + artifact['file'])) + feature = Feature('disk.0.applications', 'contains', app_features) + system.addFeature(feature) @staticmethod def _gen_system(node, nodetemplates): @@ -939,6 +986,8 @@ def _gen_system(node, nodetemplates): res.setValue('disk.%d.mount_path' % num, location) res.setValue('disk.%d.fstype' % num, fstype) + Tosca._add_ansible_roles(node, nodetemplates, res) + return res @staticmethod From e933958a8cb2aa67d528a94be6571e008f9db4af Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Apr 2016 18:19:38 +0200 Subject: [PATCH 232/509] Minor change in ConfManager --- IM/ConfManager.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 09f7ad9ca..56a0bbd21 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -331,17 +331,10 @@ def generate_inventory(self, tmp_dir): all_vars += 'IM_' + group.upper() + '_NUM_VMS=' + str(len(vm_group[group])) + '\n' for vm in vm_group[group]: - # is the master node - if self.inf.vm_master and vm.id == self.inf.vm_master.id: - # first try to use the private IP + # first try to use the public IP + ip = vm.getPublicIP() + if not ip: ip = vm.getPrivateIP() - if not ip: - ip = vm.getPublicIP() - else: - # first try to use the public IP - ip = vm.getPublicIP() - if not ip: - ip = vm.getPrivateIP() if not ip: ConfManager.logger.warn("Inf ID: " + str(self.inf.id) + ": The VM ID: " + str(vm.id) + " does not have an IP. It will not be included in the inventory file.") From 5a5cc3b6f527c909d54a515f0eb94cf22045a1af Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Apr 2016 18:19:38 +0200 Subject: [PATCH 233/509] Minor change in ConfManager --- IM/ConfManager.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 09f7ad9ca..56a0bbd21 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -331,17 +331,10 @@ def generate_inventory(self, tmp_dir): all_vars += 'IM_' + group.upper() + '_NUM_VMS=' + str(len(vm_group[group])) + '\n' for vm in vm_group[group]: - # is the master node - if self.inf.vm_master and vm.id == self.inf.vm_master.id: - # first try to use the private IP + # first try to use the public IP + ip = vm.getPublicIP() + if not ip: ip = vm.getPrivateIP() - if not ip: - ip = vm.getPublicIP() - else: - # first try to use the public IP - ip = vm.getPublicIP() - if not ip: - ip = vm.getPrivateIP() if not ip: ConfManager.logger.warn("Inf ID: " + str(self.inf.id) + ": The VM ID: " + str(vm.id) + " does not have an IP. It will not be included in the inventory file.") From b58900cb2850c4f6dc7cd6af97633547d2be20d3 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Apr 2016 18:20:31 +0200 Subject: [PATCH 234/509] Bugfix removing host instead of hosts YAML tag --- IM/tosca/Tosca.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 3d08d4748..604f309d3 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -398,7 +398,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): def _remove_recipe_header(self, script_content): """ - Removes the "host" and "connection" elements from the recipe + Removes the "hosts" and "connection" elements from the recipe to make it "RADL" compatible """ @@ -412,8 +412,8 @@ def _remove_recipe_header(self, script_content): return script_content for elem in yamlo: - if 'host' in elem: - del elem['host'] + if 'hosts' in elem: + del elem['hosts'] if 'connection' in elem: del elem['connection'] @@ -900,6 +900,11 @@ def _gen_network(node): @staticmethod def _add_ansible_roles(node, nodetemplates, system): + """ + Find all the roles to be applied to this node and + add them to the system as ansible.modules.* in 'disk.0.applications' + """ + for other_node in nodetemplates: root_type = Tosca._get_root_parent_type(other_node).type if root_type == "tosca.nodes.Compute": From 670e9827a9499617ee298a7914d8594135647df0 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Apr 2016 18:20:31 +0200 Subject: [PATCH 235/509] Bugfix removing host instead of hosts YAML tag --- IM/tosca/Tosca.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 3d08d4748..604f309d3 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -398,7 +398,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): def _remove_recipe_header(self, script_content): """ - Removes the "host" and "connection" elements from the recipe + Removes the "hosts" and "connection" elements from the recipe to make it "RADL" compatible """ @@ -412,8 +412,8 @@ def _remove_recipe_header(self, script_content): return script_content for elem in yamlo: - if 'host' in elem: - del elem['host'] + if 'hosts' in elem: + del elem['hosts'] if 'connection' in elem: del elem['connection'] @@ -900,6 +900,11 @@ def _gen_network(node): @staticmethod def _add_ansible_roles(node, nodetemplates, system): + """ + Find all the roles to be applied to this node and + add them to the system as ansible.modules.* in 'disk.0.applications' + """ + for other_node in nodetemplates: root_type = Tosca._get_root_parent_type(other_node).type if root_type == "tosca.nodes.Compute": From 613c7bcdab1acf381a3eb5cf861f5cd17796d09c Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Apr 2016 18:21:01 +0200 Subject: [PATCH 236/509] Update docs --- doc/source/radl.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/source/radl.rst b/doc/source/radl.rst index 32e95e27d..df375b405 100644 --- a/doc/source/radl.rst +++ b/doc/source/radl.rst @@ -334,16 +334,21 @@ machine. The supported features are: There are a **special** type of application that starts with ``ansible.modules.``. These applications installs `ansible roles `_ that can be used in the ``configure`` sections of the RADL. + These roles will be installed with the ``ansible-galaxy`` tool so the format of the string + after ``ansible.modules.`` must follow one of the supported formats of this tool (see + `Ansible Galaxy docs `_ for more info): + There are three type of ansible modules: * `Ansible Galaxy `_ roles: ``ansible.modules.micafer.hadoop``: The user specifies the name of the galaxy role afther the string ``ansible.modules.`` - * HTTP URL: ``ansible.modules.http://server.com/hadoop.tgz``: The user specifies an HTTP URL afther the - the string ``ansible.modules.``. The file must be compressed. it must contain only one directory - with the same name of the compressed file (without extension) with the ansible role content. - * Git Repo: ``ansible.modules.git://github.com/micafer/ansible-role-hadoop|hadoop``: The user specifies a Git repo + * HTTP URL: ``ansible.modules.https://github.com/micafer/ansible-role-hadoop/archive/master.tar.gz|hadoop``: The user + specifies an HTTP URL afther the string ``ansible.modules.``. The file must be compressed. + It must contain the ansible role content. Furthermore the user can specify the rolename using + a ``|`` afther the url, as shown in the example. + * Git Repo: ``ansible.modules.git+https://github.com/micafer/ansible-role-hadoop|hadoop``: The user specifies a Git repo (using the git scheme in the URL) afther the string ``ansible.modules.``. Furthermore the - user must specify the rolname using a | afther the url, ash shown in the example. + user can specify the rolename using a ``|`` afther the url, as shown in the example. Parametric Values From 69fe097ad8f20966e8f34c4cf04a7acef4eeb5e3 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Apr 2016 18:21:01 +0200 Subject: [PATCH 237/509] Update docs --- doc/source/radl.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/source/radl.rst b/doc/source/radl.rst index 32e95e27d..df375b405 100644 --- a/doc/source/radl.rst +++ b/doc/source/radl.rst @@ -334,16 +334,21 @@ machine. The supported features are: There are a **special** type of application that starts with ``ansible.modules.``. These applications installs `ansible roles `_ that can be used in the ``configure`` sections of the RADL. + These roles will be installed with the ``ansible-galaxy`` tool so the format of the string + after ``ansible.modules.`` must follow one of the supported formats of this tool (see + `Ansible Galaxy docs `_ for more info): + There are three type of ansible modules: * `Ansible Galaxy `_ roles: ``ansible.modules.micafer.hadoop``: The user specifies the name of the galaxy role afther the string ``ansible.modules.`` - * HTTP URL: ``ansible.modules.http://server.com/hadoop.tgz``: The user specifies an HTTP URL afther the - the string ``ansible.modules.``. The file must be compressed. it must contain only one directory - with the same name of the compressed file (without extension) with the ansible role content. - * Git Repo: ``ansible.modules.git://github.com/micafer/ansible-role-hadoop|hadoop``: The user specifies a Git repo + * HTTP URL: ``ansible.modules.https://github.com/micafer/ansible-role-hadoop/archive/master.tar.gz|hadoop``: The user + specifies an HTTP URL afther the string ``ansible.modules.``. The file must be compressed. + It must contain the ansible role content. Furthermore the user can specify the rolename using + a ``|`` afther the url, as shown in the example. + * Git Repo: ``ansible.modules.git+https://github.com/micafer/ansible-role-hadoop|hadoop``: The user specifies a Git repo (using the git scheme in the URL) afther the string ``ansible.modules.``. Furthermore the - user must specify the rolname using a | afther the url, ash shown in the example. + user can specify the rolename using a ``|`` afther the url, as shown in the example. Parametric Values From 8db1e00b7114e07fc7527d88d10d857a90a35927 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 12 Apr 2016 08:54:52 +0200 Subject: [PATCH 238/509] Bugfix using content-type instead of accept header in return error --- IM/REST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index 8288377a3..3639ecf28 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -116,7 +116,7 @@ def run(host, port): bottle.run(app, server=bottle_server, quiet=True) def return_error(code, msg): - content_type = get_media_type('Content-Type') + content_type = get_media_type('Accept') if content_type == "application/json": bottle.response.status = code From 9751a3fb29ef2582c27b0252aa79d7403bb39335 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 12 Apr 2016 08:54:52 +0200 Subject: [PATCH 239/509] Bugfix using content-type instead of accept header in return error --- IM/REST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index 8288377a3..3639ecf28 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -116,7 +116,7 @@ def run(host, port): bottle.run(app, server=bottle_server, quiet=True) def return_error(code, msg): - content_type = get_media_type('Content-Type') + content_type = get_media_type('Accept') if content_type == "application/json": bottle.response.status = code From 91ee457536faf5d4d28d2141913bf5150814338f Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Apr 2016 16:10:24 +0200 Subject: [PATCH 240/509] Add new test for JSON error response --- test/TestREST.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/TestREST.py b/test/TestREST.py index 8da20b2f2..094709c90 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -125,6 +125,14 @@ def test_15_get_incorrect_info(self): resp = self.server.getresponse() resp.read() self.assertEqual(resp.status, 404, msg="Incorrect error message: " + str(resp.status)) + + def test_16_get_incorrect_info_json(self): + self.server.request('GET', "/infrastructures/999999", headers = {'AUTHORIZATION' : self.auth_data, 'Accept' : 'application/json'}) + resp = self.server.getresponse() + output = resp.read() + self.assertEqual(resp.status, 404, msg="Incorrect error message: " + str(resp.status)) + res = json.loads(output) + self.assertEqual(res['code'], 404, msg="Incorrect error message: " + output) def test_18_get_info_without_auth_data(self): self.server.request('GET', "/infrastructures/0") From 6ee5585a8efbec69389c3ff0d03e3531ecb13e27 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Apr 2016 16:10:24 +0200 Subject: [PATCH 241/509] Add new test for JSON error response --- test/TestREST.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/TestREST.py b/test/TestREST.py index 8da20b2f2..094709c90 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -125,6 +125,14 @@ def test_15_get_incorrect_info(self): resp = self.server.getresponse() resp.read() self.assertEqual(resp.status, 404, msg="Incorrect error message: " + str(resp.status)) + + def test_16_get_incorrect_info_json(self): + self.server.request('GET', "/infrastructures/999999", headers = {'AUTHORIZATION' : self.auth_data, 'Accept' : 'application/json'}) + resp = self.server.getresponse() + output = resp.read() + self.assertEqual(resp.status, 404, msg="Incorrect error message: " + str(resp.status)) + res = json.loads(output) + self.assertEqual(res['code'], 404, msg="Incorrect error message: " + output) def test_18_get_info_without_auth_data(self): self.server.request('GET', "/infrastructures/0") From 88bd89b90911818204a0b83c84388369489763f1 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Apr 2016 18:29:18 +0200 Subject: [PATCH 242/509] Add support for new Concat tosca-parser class --- IM/tosca/Tosca.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 604f309d3..f55643960 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -8,7 +8,7 @@ from IM.uriparse import uriparse from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef -from toscaparser.functions import Function, is_function, get_function, GetAttribute +from toscaparser.functions import Function, is_function, get_function, GetAttribute, Concat from radl.radl import system, deploy, network, Feature, Features, configure, contextualize_item, RADL, contextualize class Tosca: @@ -680,6 +680,8 @@ def _final_function_result(self, func, node, inf_info=None): while isinstance(func, Function): if isinstance(func, GetAttribute): func = self._get_attribute_result(func, node, inf_info) + elif isinstance(func, Concat): + func = self._get_intrinsic_value({"concat": func.args}, node, inf_info) else: func = func.result() From 96edbb26fd1dc1f2cf5597055e3bd2313b902609 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Apr 2016 18:29:18 +0200 Subject: [PATCH 243/509] Add support for new Concat tosca-parser class --- IM/tosca/Tosca.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 604f309d3..f55643960 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -8,7 +8,7 @@ from IM.uriparse import uriparse from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef -from toscaparser.functions import Function, is_function, get_function, GetAttribute +from toscaparser.functions import Function, is_function, get_function, GetAttribute, Concat from radl.radl import system, deploy, network, Feature, Features, configure, contextualize_item, RADL, contextualize class Tosca: @@ -680,6 +680,8 @@ def _final_function_result(self, func, node, inf_info=None): while isinstance(func, Function): if isinstance(func, GetAttribute): func = self._get_attribute_result(func, node, inf_info) + elif isinstance(func, Concat): + func = self._get_intrinsic_value({"concat": func.args}, node, inf_info) else: func = func.result() From 9387d04dc0b30fd30ae7cba9f7cf967c71b7497b Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Apr 2016 18:31:53 +0200 Subject: [PATCH 244/509] Bugfixes with accept types --- IM/REST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index c54ddbf58..87d03e672 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -159,7 +159,7 @@ def format_resutl(res, default_type = "text/plain"): accept = get_media_type('Accept') if accept: - if "application/json" in accept: + if "application/json" in accept or "application/*" in accept: bottle.response.content_type = "application/json" if isinstance(res, RADL): info = dump_radl_json(res, enter="", indent="") @@ -241,7 +241,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): res = InfrastructureManager.GetInfrastructureRADL(id, auth) elif prop == "state": accept = get_media_type('Accept') - if accept and "application/json" not in accept: + if accept and "application/json" not in accept and "*/*" not in accept and "application/*" not in accept : return return_error(415, "Unsupported Accept Media Types: %s" % accept) bottle.response.content_type = "application/json" res = InfrastructureManager.GetInfrastructureState(id, auth) @@ -249,7 +249,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): return res elif prop == "outputs": accept = get_media_type('Accept') - if accept and "application/json" not in accept: + if accept and "application/json" not in accept and "*/*" not in accept and "application/*" not in accept : return return_error(415, "Unsupported Accept Media Types: %s" % accept) bottle.response.content_type = "application/json" sel_inf = InfrastructureManager.get_infrastructure(id, auth) From f009e80dc5cdf8651f8ebf85037cd3cc663f1994 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Apr 2016 18:31:53 +0200 Subject: [PATCH 245/509] Bugfixes with accept types --- IM/REST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index c54ddbf58..87d03e672 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -159,7 +159,7 @@ def format_resutl(res, default_type = "text/plain"): accept = get_media_type('Accept') if accept: - if "application/json" in accept: + if "application/json" in accept or "application/*" in accept: bottle.response.content_type = "application/json" if isinstance(res, RADL): info = dump_radl_json(res, enter="", indent="") @@ -241,7 +241,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): res = InfrastructureManager.GetInfrastructureRADL(id, auth) elif prop == "state": accept = get_media_type('Accept') - if accept and "application/json" not in accept: + if accept and "application/json" not in accept and "*/*" not in accept and "application/*" not in accept : return return_error(415, "Unsupported Accept Media Types: %s" % accept) bottle.response.content_type = "application/json" res = InfrastructureManager.GetInfrastructureState(id, auth) @@ -249,7 +249,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): return res elif prop == "outputs": accept = get_media_type('Accept') - if accept and "application/json" not in accept: + if accept and "application/json" not in accept and "*/*" not in accept and "application/*" not in accept : return return_error(415, "Unsupported Accept Media Types: %s" % accept) bottle.response.content_type = "application/json" sel_inf = InfrastructureManager.get_infrastructure(id, auth) From 82136a893c447001a732d279f881ef7986062ac1 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 21 Apr 2016 17:03:11 +0200 Subject: [PATCH 246/509] Update Tests for new JSON results --- test/TestREST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index 1fbcfeb40..49249ae41 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -243,8 +243,8 @@ def test_45_getstate(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure state:" + output) res = json.loads(output) - state = res['state'] - vm_states = res['vm_states'] + state = res['state']['state'] + vm_states = res['state']['vm_states'] self.assertEqual(state, "configured", msg="Unexpected inf state: " + state + ". It must be 'configured'.") for vm_id, vm_state in vm_states.iteritems(): self.assertEqual(vm_state, "configured", msg="Unexpected vm state: " + vm_state + " in VM ID " + str(vm_id) + ". It must be 'configured'.") @@ -495,7 +495,7 @@ def test_94_get_outputs(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR getting TOSCA outputs:" + output) res = json.loads(output) - server_url = str(res['server_url'][0]) + server_url = str(res['outputs']['server_url'][0]) self.assertRegexpMatches(server_url, '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', msg="Unexpected outputs: " + output) def test_95_add_tosca(self): From f621046d6af94a6108596c4baca2587f664bb302 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 25 Apr 2016 11:44:28 +0200 Subject: [PATCH 247/509] Update README --- README.md | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 98830b629..f11b3b11f 100644 --- a/README.md +++ b/README.md @@ -102,33 +102,25 @@ First install the requirements: On Debian Systems: ``` -$ apt-get -y install git python-setuptools python-dev gcc python-soappy python-pip python-pbr python-dateutil +$ apt-get -y install git python-pip python-dev python-soappy ``` On RedHat Systems: ``` -$ yum remove python-paramiko python-crypto -$ yum -y install git python-setuptools python-devel gcc SOAPpy python-dateutil python-six python-requests -$ easy_install pip -$ pip install pbr +$ yum -y install epel-release +$ yum -y install git gcc python-devel python-pip SOAPpy python-importlib python-requests ``` Then install the TOSCA parser: ``` -$ cd /tmp -$ git clone --recursive https://github.com/indigo-dc/tosca-parser.git -$ cd tosca-parser -$ python setup.py install +$ pip install git+http://github.com/indigo-dc/tosca-parser ``` Finally install the IM service: ``` -$ cd /tmp -$ git clone --recursive https://github.com/indigo-dc/im.git -$ cd im -$ python setup.py install +$ pip install git+http://github.com/indigo-dc/im ``` From 48814e642dc15850d1c3b0089c103d97c26f675e Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 25 Apr 2016 11:44:53 +0200 Subject: [PATCH 248/509] Minor changes --- IM/tosca/Tosca.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index f55643960..f75eb4c75 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -522,6 +522,7 @@ def _get_attribute_result(self, func, node, inf_info): * Node template name | HOST. * Attribute name. + * Index (optional) If the HOST keyword is passed as the node template name argument the function will search each node template along the HostedOn relationship @@ -583,6 +584,7 @@ def _get_attribute_result(self, func, node, inf_info): val = {"user" : user} if password: val["token"] = password + val["token_type"] = "password" if private_key: val["token_type"] = "private_key" val["token"] = private_key From 0621c96348a978a6ec09671fc23a9233bb9eabec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 27 Apr 2016 11:51:57 +0200 Subject: [PATCH 249/509] Added latest tag to FROM in Dockerfile --- docker-devel/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 87fcf08d7..66a73c729 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile to create a container with the IM service -FROM grycapjenkins/im-base +FROM grycapjenkins/im-base:latest MAINTAINER Miguel Caballer LABEL version="1.4.4" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" From bfdb9e886122c91d8e35f661a25b75247366b766 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 27 Apr 2016 16:49:54 +0200 Subject: [PATCH 250/509] Add ansible playbook to install IM --- ansible_install.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 ansible_install.yaml diff --git a/ansible_install.yaml b/ansible_install.yaml new file mode 100644 index 000000000..598c83905 --- /dev/null +++ b/ansible_install.yaml @@ -0,0 +1,20 @@ +- hosts: localhost + connection: local + tasks: + - name: Yum install epel-release + action: yum pkg=epel-release state=installed + when: ansible_os_family == "RedHat" + + - name: Yum install requisites + action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests state=installed + when: ansible_os_family == "RedHat" + + - name: Apt-get install requisites + apt: pkg=git,python-pip,python-dev,python-soappy state=installed update_cache=yes cache_valid_time=3600 + when: ansible_os_family == "Debian" + + - name: pip install tosca-parser + pip: name=git+http://github.com/indigo-dc/tosca-parser editable=false + + - name: pip install IM + pip: name=git+http://github.com/indigo-dc/im editable=false From fd7b42c8c5f835f7d083a25c6309f4b5d283d9af Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Apr 2016 09:27:47 +0200 Subject: [PATCH 251/509] Bugfix in InfrastructureInfo --- IM/InfrastructureInfo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 1bfd0ae81..59a6c00bc 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -87,6 +87,8 @@ def __init__(self): """Flag to specify that the configuration threads of this inf has finished successfully or with errors.""" self.conf_threads = [] """ List of configuration threads.""" + self.extra_info = {} + """ Extra information about the Infrastructure.""" def __getstate__(self): """ From d8acec67f4789f19275ea47a694585f0423350bf Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Apr 2016 10:01:58 +0200 Subject: [PATCH 252/509] Minor change --- test/TestREST.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/TestREST.py b/test/TestREST.py index a89f1b26a..e591bb64a 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -101,6 +101,13 @@ def wait_inf_state(self, state, timeout, incorrect_states=[], vm_ids=None): self.assertEqual(resp.status, 200, msg="ERROR getting VM info:" + vm_state) + if vm_state == VirtualMachine.UNCONFIGURED: + self.server.request('GET', "/infrastructures/" + self.inf_id + "/contmsg", + headers = {'AUTHORIZATION' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + print output + self.assertFalse(vm_state in err_states, msg=("ERROR waiting for a state. '%s' state was expected " "and '%s' was obtained in the VM %s" % (state, vm_state, From a049b3051e17c0526f46d4c41b5503f5dc5e3db2 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Apr 2016 10:03:22 +0200 Subject: [PATCH 253/509] Minor change --- test/TestREST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TestREST.py b/test/TestREST.py index e591bb64a..23f245735 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -103,7 +103,7 @@ def wait_inf_state(self, state, timeout, incorrect_states=[], vm_ids=None): if vm_state == VirtualMachine.UNCONFIGURED: self.server.request('GET', "/infrastructures/" + self.inf_id + "/contmsg", - headers = {'AUTHORIZATION' : self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) print output From 752bd22d0d8cf7a8050e6298250f7ad1d10206a2 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Apr 2016 10:55:59 +0200 Subject: [PATCH 254/509] Bugfix in TestREST --- test/TestREST.py | 347 ++---------------------------------------- test/tosca_add.yml | 106 +++++++++++++ test/tosca_create.yml | 103 +++++++++++++ test/tosca_remove.yml | 108 +++++++++++++ 4 files changed, 326 insertions(+), 338 deletions(-) create mode 100644 test/tosca_add.yml create mode 100644 test/tosca_create.yml create mode 100644 test/tosca_remove.yml diff --git a/test/TestREST.py b/test/TestREST.py index 23f245735..a2e7f33fa 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -160,17 +160,6 @@ def test_16_get_incorrect_info_json(self): self.assertEqual(res['code'], 404, msg="Incorrect error message: " + output) - def test_16_get_incorrect_info_json(self): - self.server.request('GET', "/infrastructures/999999", headers={ - 'AUTHORIZATION': self.auth_data, 'Accept': 'application/json'}) - resp = self.server.getresponse() - output = resp.read() - self.assertEqual(resp.status, 404, - msg="Incorrect error message: " + str(resp.status)) - res = json.loads(output) - self.assertEqual(res['code'], 404, - msg="Incorrect error message: " + output) - def test_18_get_info_without_auth_data(self): self.server.request('GET', "/infrastructures/0") resp = self.server.getresponse() @@ -179,11 +168,8 @@ def test_18_get_info_without_auth_data(self): msg="Incorrect error message: " + str(resp.status)) def test_20_create(self): - f = open(RADL_FILE) - radl = "" - for line in f.readlines(): - radl += line - f.close() + with open(RADL_FILE) as f: + radl = f.read() self.server.request('POST', "/infrastructures", body=radl, headers={'AUTHORIZATION': self.auth_data}) @@ -473,111 +459,8 @@ def test_93_create_tosca(self): """ Test the CreateInfrastructure IM function with a TOSCA document """ - tosca = """ -tosca_definitions_version: tosca_simple_yaml_1_0 - -description: TOSCA test for the IM - -repositories: - indigo_repository: - description: INDIGO Custom types repository - url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ - -imports: - - indigo_custom_types: - file: custom_types.yaml - repository: indigo_repository - -topology_template: - inputs: - db_name: - type: string - default: world - db_user: - type: string - default: dbuser - db_password: - type: string - default: pass - mysql_root_password: - type: string - default: mypass - - node_templates: - - apache: - type: tosca.nodes.WebServer.Apache - requirements: - - host: web_server - - web_server: - type: tosca.nodes.indigo.Compute - properties: - public_ip: yes - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - # Guest Operating System properties - os: - properties: - # host Operating System image properties - type: linux - distribution: ubuntu - - test_db: - type: tosca.nodes.indigo.Database.MySQL - properties: - name: { get_input: db_name } - user: { get_input: db_user } - password: { get_input: db_password } - root_password: { get_input: mysql_root_password } - artifacts: - db_content: - file: http://downloads.mysql.com/docs/world.sql.gz - type: tosca.artifacts.File - requirements: - - host: - node: mysql - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_import.yml - inputs: - db_name: { get_property: [ SELF, name ] } - db_data: { get_artifact: [ SELF, db_content ] } - db_name: { get_property: [ SELF, name ] } - db_user: { get_property: [ SELF, user ] } - - mysql: - type: tosca.nodes.DBMS.MySQL - properties: - root_password: { get_input: mysql_root_password } - requirements: - - host: - node: db_server - - db_server: - type: tosca.nodes.Compute - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - disk_size: 10 GB - mem_size: 4 GB - os: - properties: - architecture: x86_64 - type: linux - distribution: ubuntu - - outputs: - server_url: - value: { get_attribute: [ web_server, public_address ] } - """ + with open(TESTS_PATH + '/test_create.yml') as f: + tosca = f.read() self.server.request('POST', "/infrastructures", body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) @@ -608,113 +491,8 @@ def test_95_add_tosca(self): """ Test the AddResource IM function with a TOSCA document """ - tosca = """ -tosca_definitions_version: tosca_simple_yaml_1_0 - -description: TOSCA test for the IM - -repositories: - indigo_repository: - description: INDIGO Custom types repository - url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ - -imports: - - indigo_custom_types: - file: custom_types.yaml - repository: indigo_repository - -topology_template: - inputs: - db_name: - type: string - default: world - db_user: - type: string - default: dbuser - db_password: - type: string - default: pass - mysql_root_password: - type: string - default: mypass - - node_templates: - apache: - type: tosca.nodes.WebServer.Apache - requirements: - - host: web_server - - web_server: - type: tosca.nodes.indigo.Compute - properties: - public_ip: yes - capabilities: - scalable: - properties: - count: 2 - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - # Guest Operating System properties - os: - properties: - # host Operating System image properties - type: linux - distribution: ubuntu - - test_db: - type: tosca.nodes.indigo.Database.MySQL - properties: - name: { get_input: db_name } - user: { get_input: db_user } - password: { get_input: db_password } - root_password: { get_input: mysql_root_password } - artifacts: - db_content: - file: http://downloads.mysql.com/docs/world.sql.gz - type: tosca.artifacts.File - requirements: - - host: - node: mysql - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_import.yml - inputs: - db_name: { get_property: [ SELF, name ] } - db_data: { get_artifact: [ SELF, db_content ] } - db_name: { get_property: [ SELF, name ] } - db_user: { get_property: [ SELF, user ] } - - mysql: - type: tosca.nodes.DBMS.MySQL - properties: - root_password: { get_input: mysql_root_password } - requirements: - - host: - node: db_server - - db_server: - type: tosca.nodes.Compute - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - disk_size: 10 GB - mem_size: 4 GB - os: - properties: - architecture: x86_64 - type: linux - distribution: ubuntu - - outputs: - server_url: - value: { get_attribute: [ web_server, public_address ] } - """ + with open(TESTS_PATH + '/test_add.yml') as f: + tosca = f.read() self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) @@ -738,117 +516,10 @@ def test_95_add_tosca(self): def test_96_remove_tosca(self): """ - Test the AddResource IM function with a TOSCA document + Test the RemoveResource IM function with a TOSCA document """ - tosca = """ -tosca_definitions_version: tosca_simple_yaml_1_0 - -description: TOSCA test for the IM - -repositories: - indigo_repository: - description: INDIGO Custom types repository - url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ - -imports: - - indigo_custom_types: - file: custom_types.yaml - repository: indigo_repository - -topology_template: - inputs: - db_name: - type: string - default: world - db_user: - type: string - default: dbuser - db_password: - type: string - default: pass - mysql_root_password: - type: string - default: mypass - - node_templates: - - apache: - type: tosca.nodes.WebServer.Apache - requirements: - - host: web_server - - web_server: - type: tosca.nodes.indigo.Compute - properties: - public_ip: yes - capabilities: - scalable: - properties: - count: 1 - removal_list: ['2'] - # Host container properties - host: - properties: - num_cpus: 1 - mem_size: 1 GB - # Guest Operating System properties - os: - properties: - # host Operating System image properties - type: linux - distribution: ubuntu - - test_db: - type: tosca.nodes.indigo.Database.MySQL - properties: - name: { get_input: db_name } - user: { get_input: db_user } - password: { get_input: db_password } - root_password: { get_input: mysql_root_password } - artifacts: - db_content: - file: http://downloads.mysql.com/docs/world.sql.gz - type: tosca.artifacts.File - requirements: - - host: - node: mysql - interfaces: - Standard: - configure: - implementation: mysql/mysql_db_import.yml - inputs: - db_name: { get_property: [ SELF, name ] } - db_data: { get_artifact: [ SELF, db_content ] } - db_name: { get_property: [ SELF, name ] } - db_user: { get_property: [ SELF, user ] } - - mysql: - type: tosca.nodes.DBMS.MySQL - properties: - root_password: { get_input: mysql_root_password } - requirements: - - host: - node: db_server - - db_server: - type: tosca.nodes.Compute - capabilities: - # Host container properties - host: - properties: - num_cpus: 1 - disk_size: 10 GB - mem_size: 4 GB - os: - properties: - architecture: x86_64 - type: linux - distribution: ubuntu - - outputs: - server_url: - value: { get_attribute: [ web_server, public_address ] } - """ + with open(TESTS_PATH + '/test_remove.yml') as f: + tosca = f.read() self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) diff --git a/test/tosca_add.yml b/test/tosca_add.yml new file mode 100644 index 000000000..b63d0935b --- /dev/null +++ b/test/tosca_add.yml @@ -0,0 +1,106 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + +repositories: + indigo_repository: + description: INDIGO Custom types repository + url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ + +imports: + - indigo_custom_types: + file: custom_types.yaml + repository: indigo_repository + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + scalable: + properties: + count: 2 + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.indigo.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + + outputs: + server_url: + value: { get_attribute: [ web_server, public_address ] } \ No newline at end of file diff --git a/test/tosca_create.yml b/test/tosca_create.yml new file mode 100644 index 000000000..b57740a18 --- /dev/null +++ b/test/tosca_create.yml @@ -0,0 +1,103 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + +repositories: + indigo_repository: + description: INDIGO Custom types repository + url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ + +imports: + - indigo_custom_types: + file: custom_types.yaml + repository: indigo_repository + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.indigo.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + + outputs: + server_url: + value: { get_attribute: [ web_server, public_address ] } \ No newline at end of file diff --git a/test/tosca_remove.yml b/test/tosca_remove.yml new file mode 100644 index 000000000..07d16006f --- /dev/null +++ b/test/tosca_remove.yml @@ -0,0 +1,108 @@ + +tosca_definitions_version: tosca_simple_yaml_1_0 + +description: TOSCA test for the IM + +repositories: + indigo_repository: + description: INDIGO Custom types repository + url: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/ + +imports: + - indigo_custom_types: + file: custom_types.yaml + repository: indigo_repository + +topology_template: + inputs: + db_name: + type: string + default: world + db_user: + type: string + default: dbuser + db_password: + type: string + default: pass + mysql_root_password: + type: string + default: mypass + + node_templates: + + apache: + type: tosca.nodes.WebServer.Apache + requirements: + - host: web_server + + web_server: + type: tosca.nodes.indigo.Compute + properties: + public_ip: yes + capabilities: + scalable: + properties: + count: 1 + removal_list: ['2'] + # Host container properties + host: + properties: + num_cpus: 1 + mem_size: 1 GB + # Guest Operating System properties + os: + properties: + # host Operating System image properties + type: linux + distribution: ubuntu + + test_db: + type: tosca.nodes.indigo.Database.MySQL + properties: + name: { get_input: db_name } + user: { get_input: db_user } + password: { get_input: db_password } + root_password: { get_input: mysql_root_password } + artifacts: + db_content: + file: http://downloads.mysql.com/docs/world.sql.gz + type: tosca.artifacts.File + requirements: + - host: + node: mysql + interfaces: + Standard: + configure: + implementation: mysql/mysql_db_import.yml + inputs: + db_name: { get_property: [ SELF, name ] } + db_data: { get_artifact: [ SELF, db_content ] } + db_name: { get_property: [ SELF, name ] } + db_user: { get_property: [ SELF, user ] } + + mysql: + type: tosca.nodes.DBMS.MySQL + properties: + root_password: { get_input: mysql_root_password } + requirements: + - host: + node: db_server + + db_server: + type: tosca.nodes.Compute + capabilities: + # Host container properties + host: + properties: + num_cpus: 1 + disk_size: 10 GB + mem_size: 4 GB + os: + properties: + architecture: x86_64 + type: linux + distribution: ubuntu + + outputs: + server_url: + value: { get_attribute: [ web_server, public_address ] } \ No newline at end of file From babd17dd122e6ccef30a4bef4f4318fea9b32ab8 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Apr 2016 11:49:13 +0200 Subject: [PATCH 255/509] Bugfix in TestREST --- test/TestREST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/TestREST.py b/test/TestREST.py index a2e7f33fa..ca36618fb 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -459,7 +459,7 @@ def test_93_create_tosca(self): """ Test the CreateInfrastructure IM function with a TOSCA document """ - with open(TESTS_PATH + '/test_create.yml') as f: + with open(TESTS_PATH + '/tosca_create.yml') as f: tosca = f.read() self.server.request('POST', "/infrastructures", body=tosca, @@ -491,7 +491,7 @@ def test_95_add_tosca(self): """ Test the AddResource IM function with a TOSCA document """ - with open(TESTS_PATH + '/test_add.yml') as f: + with open(TESTS_PATH + '/tosca_add.yml') as f: tosca = f.read() self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, @@ -518,7 +518,7 @@ def test_96_remove_tosca(self): """ Test the RemoveResource IM function with a TOSCA document """ - with open(TESTS_PATH + '/test_remove.yml') as f: + with open(TESTS_PATH + '/tosca_remove.yml') as f: tosca = f.read() self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, From 51d333f225278fc139e33f3a4eeee9bc6ec99b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Thu, 28 Apr 2016 15:42:59 +0200 Subject: [PATCH 256/509] Update Dockerfile The pbr library needs to be installed from pip. The python-pbr library is not updated to the version needed. --- docker/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2ced65ef3..7dd3ebfdf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,7 +10,6 @@ RUN apt-get update && apt-get install -y \ python-dev \ python-pip \ python-soappy \ - python-pbr \ python-dateutil \ openssh-client \ sshpass \ @@ -21,7 +20,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Install CherryPy to enable HTTPS in REST API -RUN pip install CherryPy pyOpenSSL +RUN pip install pbr CherryPy pyOpenSSL # Install tosca-parser RUN cd tmp \ From 64a7ed857537d48a611f4b94084fb5477712cc9b Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 28 Apr 2016 15:54:08 +0200 Subject: [PATCH 257/509] Add support for Endpoints --- IM/tosca/Tosca.py | 55 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a142f496c..3ccd67143 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -144,6 +144,27 @@ def _get_num_instances(self, sys_name, inf_info): return current_num + @staticmethod + def _format_outports(ports_dict): + res = "" + for port in ports_dict.values(): + # TODO: format ranges + protocol = "tcp" + if "protocol" in port: + protocol = port["protocol"] + if "source" in port: + remote_port = port["source"] + if "target" in port: + local_port = port["target"] + else: + local_port = remote_port + + if res: + res += "," + res += "%s/%s-%s/%s" % (remote_port, protocol, local_port, protocol) + + return res + @staticmethod def _add_node_nets(node, radl, system, nodetemplates): @@ -162,12 +183,26 @@ def _add_node_nets(node, radl, system, nodetemplates): system.setValue('net_interface.%d.ip' % num, ip) else: public_ip = False - node_props = node.get_properties_objects() - if node_props: - for prop in node_props: - if prop.name == "public_ip": - public_ip = prop.value - break + + # This is the solution using the public_ip property + node_props = node.get_properties() + if node_props and "public_ip" in node_props: + public_ip = node_props["public_ip"].value + + # This is the solution using endpoints + dns_name = None + ports = {} + node_caps = node.get_capabilities() + if node_caps: + if "endpoint" in node_caps: + cap_props = node_caps["endpoint"].get_properties() + if cap_props and "network_name" in cap_props: + if cap_props["network_name"].value == "PUBLIC": + public_ip = True + if "dns_name" in node_caps: + dns_name = node_caps["dns_name"].value + if "ports" in node_caps: + ports = node_caps["ports"] # If the node needs a public IP if public_ip: @@ -195,8 +230,12 @@ def _add_node_nets(node, radl, system, nodetemplates): radl.networks.append(public_net) num_net = system.getNumNetworkIfaces() - system.setValue('net_interface.' + str(num_net) + - '.connection', public_net.id) + if ports: + public_net.setValue("outports", Tosca._format_outports(ports)) + + system.setValue('net_interface.%d.connection' % num_net, public_net.id) + if dns_name: + system.setValue('net_interface.%d.dns_name' % num_net, dns_name) # The private net is always added private_nets = [] From 52988e76e58acbd7a1b03c96b612848d618158be Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 29 Apr 2016 13:07:11 +0200 Subject: [PATCH 258/509] Minor change --- IM/REST.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index aba9f2612..4e579189c 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -322,7 +322,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): bottle.response.content_type = "application/json" sel_inf = InfrastructureManager.get_infrastructure(id, auth) if "TOSCA" in sel_inf.extra_info: - res = Tosca(sel_inf.extra_info["TOSCA"]).get_outputs(sel_inf) + res = sel_inf.extra_info["TOSCA"].get_outputs(sel_inf) else: bottle.abort( 403, "'outputs' infrastructure property is not valid in this infrastructure") @@ -382,8 +382,8 @@ def RESTCreateInfrastructure(): if "application/json" in content_type: radl_data = parse_radl_json(radl_data) elif "text/yaml" in content_type: - tosca_data = radl_data - _, radl_data = Tosca(radl_data).to_radl() + tosca_data = Tosca(radl_data) + _, radl_data = tosca_data.to_radl() elif "text/plain" in content_type or "*/*" in content_type or "text/*" in content_type: content_type = "text/plain" else: @@ -493,9 +493,9 @@ def RESTAddResource(id=None): if "application/json" in content_type: radl_data = parse_radl_json(radl_data) elif "text/yaml" in content_type: - tosca_data = radl_data + tosca_data = Tosca(radl_data) sel_inf = InfrastructureManager.get_infrastructure(id, auth) - remove_list, radl_data = Tosca(radl_data).to_radl(sel_inf) + remove_list, radl_data = tosca_data.to_radl(sel_inf) elif "text/plain" in content_type or "*/*" in content_type or "text/*" in content_type: content_type = "text/plain" else: From 42d8b8e58f22ca0ef9f1065c42b554ccf57ffd14 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 29 Apr 2016 13:07:29 +0200 Subject: [PATCH 259/509] Bugfix managing endpoints --- IM/tosca/Tosca.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 3ccd67143..2e305403f 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -199,10 +199,10 @@ def _add_node_nets(node, radl, system, nodetemplates): if cap_props and "network_name" in cap_props: if cap_props["network_name"].value == "PUBLIC": public_ip = True - if "dns_name" in node_caps: - dns_name = node_caps["dns_name"].value - if "ports" in node_caps: - ports = node_caps["ports"] + if "dns_name" in cap_props: + dns_name = cap_props["dns_name"].value + if "ports" in cap_props: + ports = cap_props["ports"].value # If the node needs a public IP if public_ip: From ac9a1ad90cfe2a587a65e52ace5f354dacc229dd Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 2 May 2016 13:14:21 +0200 Subject: [PATCH 260/509] Bugfix managing endpoints --- IM/tosca/Tosca.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 2e305403f..d3d235af5 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -199,10 +199,10 @@ def _add_node_nets(node, radl, system, nodetemplates): if cap_props and "network_name" in cap_props: if cap_props["network_name"].value == "PUBLIC": public_ip = True - if "dns_name" in cap_props: - dns_name = cap_props["dns_name"].value - if "ports" in cap_props: - ports = cap_props["ports"].value + if cap_props and "dns_name" in cap_props: + dns_name = cap_props["dns_name"].value + if cap_props and "ports" in cap_props: + ports = cap_props["ports"].value # If the node needs a public IP if public_ip: From 4e1e930da8ff57c7ecd419d5eac6b83254a0752d Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 3 May 2016 10:07:16 +0200 Subject: [PATCH 261/509] Bugfix in dockerfile --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e4f84eb49..05127daa2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update && apt-get install -y \ # Install CherryPy to enable HTTPS in REST API RUN pip install setuptools --upgrade -I -RUN pip install pbr CherryPy pyOpenSSL +RUN pip install pbr CherryPy pyOpenSSL --upgrade -I # Install tosca-parser RUN cd tmp \ From 407b488adf14cf842d59ac7a147a8de9d1dfd583 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 3 May 2016 12:08:58 +0200 Subject: [PATCH 262/509] Update install recipe to changes in chryptography lib --- ansible_install.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index 598c83905..ddd6c911c 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -6,13 +6,22 @@ when: ansible_os_family == "RedHat" - name: Yum install requisites - action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests state=installed + action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests,libffi-devel,openssl-devel state=installed when: ansible_os_family == "RedHat" - name: Apt-get install requisites - apt: pkg=git,python-pip,python-dev,python-soappy state=installed update_cache=yes cache_valid_time=3600 + apt: pkg=git,python-pip,python-dev,python-soappy,libssl-dev,libffi-dev state=installed update_cache=yes cache_valid_time=3600 when: ansible_os_family == "Debian" + - name: pip upgrade setuptools + pip: name=setuptools extra_args="-I" state=latest + + - name: pip install pbr,CherryPy and pyOpenSSL to enable HTTPS in REST API + pip: name=pbr,CherryPy,pyOpenSSL extra_args="-I" state=latest + + - name: pip install tosca-parser + pip: name=git+http://github.com/indigo-dc/tosca-parser editable=false + - name: pip install tosca-parser pip: name=git+http://github.com/indigo-dc/tosca-parser editable=false From 3d09a96a8501e09a290fd070f42e416f9dc0d183 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 3 May 2016 13:51:46 +0200 Subject: [PATCH 263/509] Bugfix with two VMs with public ip --- contextualization/ctxt_agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contextualization/ctxt_agent.py b/contextualization/ctxt_agent.py index 7bdf96c47..5cf88bb09 100755 --- a/contextualization/ctxt_agent.py +++ b/contextualization/ctxt_agent.py @@ -351,10 +351,10 @@ def replace_vm_ip(vm_data): with open(filename) as f: inventoy_data = "" for line in f: - line = re.sub(" ansible_host=%s" % vm_data[ - 'ip'], " ansible_host=%s" % vm_data['ctxt_ip'] + "_", line) - line = re.sub(" ansible_ssh_host=%s" % vm_data[ - 'ip'], " ansible_ssh_host=%s" % vm_data['ctxt_ip'] + "_", line) + line = re.sub(" ansible_host=%s " % vm_data['ip'], + " ansible_host=%s " % vm_data['ctxt_ip'], line) + line = re.sub(" ansible_ssh_host=%s " % vm_data['ip'], + " ansible_ssh_host=%s " % vm_data['ctxt_ip'], line) inventoy_data += line with open(filename, 'w+') as f: From 0e02503bf7cafd3c41d23163bc67e15305d1231d Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 9 May 2016 18:10:24 +0200 Subject: [PATCH 264/509] First approach to support INDIGO IAM --- IM/InfrastructureManager.py | 75 +++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 8d23d0ea4..45b1599e6 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -38,6 +38,11 @@ from config import Config from IM.VirtualMachine import VirtualMachine +from oic.oic import Client +from oic.exception import PyoidcError +from oic.utils.authn.client import CLIENT_AUTHN_METHOD +from jwkest.jwt import JWT + if Config.MAX_SIMULTANEOUS_LAUNCHES > 1: from multiprocessing.pool import ThreadPool @@ -280,6 +285,7 @@ def Reconfigure(inf_id, radl_data, auth, vm_list=None): Return: "" if success. """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Reconfiguring the inf: " + str(inf_id)) @@ -382,6 +388,7 @@ def AddResource(inf_id, radl_data, auth, context=True, failed_clouds=[]): Return(list of int): ids of the new virtual machine created. """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Adding resources to inf: " + str(inf_id)) @@ -605,6 +612,7 @@ def RemoveResource(inf_id, vm_list, auth, context=True): Return(int): number of undeployed virtual machines. """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Removing the VMs: " + str(vm_list) + " from inf ID: '" + str(inf_id) + "'") @@ -665,6 +673,8 @@ def GetVMProperty(inf_id, vm_id, property_name, auth): Return: a str with the property value """ + auth = InfrastructureManager.check_auth_data(auth) + radl = InfrastructureManager.GetVMInfo(inf_id, vm_id, auth) res = None @@ -685,6 +695,7 @@ def GetVMInfo(inf_id, vm_id, auth): Return: a str with the information about the VM """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Get information about the vm: '" + str(vm_id) + "' from inf: " + str(inf_id)) @@ -716,6 +727,7 @@ def GetVMContMsg(inf_id, vm_id, auth): Return: a str with the contextualization log of the VM """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Get contextualization log of the vm: '" + str(vm_id) + "' from inf: " + str(inf_id)) @@ -740,6 +752,7 @@ def AlterVM(inf_id, vm_id, radl_data, auth): Return: a str with the information about the VM """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Modifying the VM: '" + str(vm_id) + "' from inf: " + str(inf_id)) @@ -785,6 +798,7 @@ def GetInfrastructureRADL(inf_id, auth): Return: str with the RADL """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Getting RADL of the inf: " + str(inf_id)) @@ -807,6 +821,8 @@ def GetInfrastructureInfo(inf_id, auth): Return: a list of str: list of virtual machine ids. """ + auth = InfrastructureManager.check_auth_data(auth) + InfrastructureManager.logger.info( "Getting information about the inf: " + str(inf_id)) @@ -831,6 +847,7 @@ def GetInfrastructureContMsg(inf_id, auth): Return: a str with the cont msg """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Getting cont msg of the inf: " + str(inf_id)) @@ -860,6 +877,7 @@ def GetInfrastructureState(inf_id, auth): - 'state': str with the aggregated state of the infrastructure - 'vm_states': a dict indexed with the id of the VM and its state as value """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Getting state of the inf: " + str(inf_id)) @@ -930,6 +948,7 @@ def StopInfrastructure(inf_id, auth): Return(str): error messages; empty string means all was ok. """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Stopping the infrastructure id: " + str(inf_id)) @@ -982,6 +1001,7 @@ def StartInfrastructure(inf_id, auth): Return(str): error messages; empty string means all was ok. """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Starting the infrastructure id: " + str(inf_id)) @@ -1023,6 +1043,7 @@ def StartVM(inf_id, vm_id, auth): Return(str): error messages; empty string means all was ok. """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Starting the VM id %s from the infrastructure id: %s" % (vm_id, inf_id)) @@ -1056,6 +1077,8 @@ def StopVM(inf_id, vm_id, auth): Return(str): error messages; empty string means all was ok. """ + # First check the auth data + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Stopping the VM id %s from the infrastructure id: %s" % (vm_id, inf_id)) @@ -1101,6 +1124,8 @@ def DestroyInfrastructure(inf_id, auth): Return: None. """ + # First check the auth data + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info( "Destroying the infrastructure id: " + str(inf_id)) @@ -1174,6 +1199,47 @@ def check_im_user(auth): else: return True + @staticmethod + def check_iam_token(im_auth): + token = im_auth["token"] + try: + # decode the token to get the issuer + decoded_token = JWT().unpack(token) + iss = json.loads(decoded_token.part[1])['iss'] + # create the client using the iss url + client = Client(client_authn_method=CLIENT_AUTHN_METHOD) + client.userinfo_endpoint = "%s/userinfo" % iss + + userinfo = client.do_user_info_request(token=token) + # convert to username to use it in the rest of the IM + del im_auth['token'] + im_auth['username'] = str(userinfo.get("preferred_username")) + im_auth['password'] = str(userinfo.get("sub")) + except PyoidcError, oiex: + InfrastructureManager.logger.debug( + "Incorrect auth token: %s" % str(oiex)) + raise UnauthorizedUserException() + except Exception, ex: + InfrastructureManager.logger.exception( + "Error trying to validate auth token: %s" % str(ex)) + raise Exception("Error trying to validate auth token: %s" % str(ex)) + + @staticmethod + def check_auth_data(auth): + # First check if it is configured to check the users from a list + im_auth = auth.getAuthInfo("InfrastructureManager")[0] + + # First check if the IAM token is included + if "token" in im_auth: + InfrastructureManager.check_iam_token(im_auth) + else: + # if not assume the basic user/password auth data + if not InfrastructureManager.check_im_user(im_auth): + raise UnauthorizedUserException() + + # We have to check if TTS is needed for other auth item + return auth + @staticmethod def CreateInfrastructure(radl, auth): """ @@ -1190,9 +1256,8 @@ def CreateInfrastructure(radl, auth): Return(int): the new infrastructure ID if successful. """ - # First check if it is configured to check the users from a list - if not InfrastructureManager.check_im_user(auth.getAuthInfo("InfrastructureManager")): - raise UnauthorizedUserException() + # First check the auth data + auth = InfrastructureManager.check_auth_data(auth) if not auth.getAuthInfo("InfrastructureManager"): raise Exception( @@ -1232,6 +1297,7 @@ def GetInfrastructureList(auth): Return(list of int): list of infrastructure ids. """ + auth = InfrastructureManager.check_auth_data(auth) InfrastructureManager.logger.info("Listing the user infrastructures") @@ -1251,6 +1317,7 @@ def GetInfrastructureList(auth): @staticmethod def ExportInfrastructure(inf_id, delete, auth_data): auth = Authentication(auth_data) + auth = InfrastructureManager.check_auth_data(auth) sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) str_inf = pickle.dumps(sel_inf) @@ -1265,6 +1332,8 @@ def ExportInfrastructure(inf_id, delete, auth_data): @staticmethod def ImportInfrastructure(str_inf, auth_data): auth = Authentication(auth_data) + auth = InfrastructureManager.check_auth_data(auth) + try: new_inf = pickle.loads(str_inf) except Exception, ex: From b0d47c3f6721afa7075126108e9b82180c4848ae Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 9 May 2016 18:38:22 +0200 Subject: [PATCH 265/509] Add pyopenid as requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 808e1fae3..8f97b7698 100644 --- a/setup.py +++ b/setup.py @@ -51,5 +51,5 @@ description="IM is a tool to manage virtual infrastructures on Cloud deployments", platforms=["any"], install_requires=["ansible >= 1.8, < 2", "paramiko >= 1.14", "PyYAML", "SOAPpy", - "boto >= 2.29", "apache-libcloud >= 0.17", "RADL", "bottle", "netaddr", "scp"] + "boto >= 2.29", "apache-libcloud >= 0.17", "RADL", "bottle", "netaddr", "scp", "oic"] ) From 6ddb573edfce404cd3ce0754eef30a7d7b551153 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 09:42:16 +0200 Subject: [PATCH 266/509] Improve the auth comparation --- IM/InfrastructureInfo.py | 30 +++++++++++++++++++++++++++++- IM/InfrastructureManager.py | 12 +++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 59a6c00bc..c06417b97 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys import logging import threading import time @@ -26,6 +25,8 @@ from radl.radl import RADL, Feature, deploy, system, contextualize_item from config import Config from Queue import PriorityQueue +from jwkest.jwt import JWT +import json class IncorrectVMException(Exception): @@ -480,3 +481,30 @@ def Contextualize(self, auth, vm_list=None): # update the ConfManager auth self.cm.auth = auth self.cm.init_time = time.time() + + def is_authorized(self, auth): + """ + Checks if the auth data provided is authorized to access this infrastructure + """ + if self.auth is not None: + self_im_auth = self.auth.getAuthInfo("InfrastructureManager")[0] + other_im_auth = auth.getAuthInfo("InfrastructureManager")[0] + + for elem in ['username','password']: + if elem not in other_im_auth: + return False + if self_im_auth[elem] != other_im_auth[elem]: + return False + + if 'token' in self_im_auth: + if 'token' not in other_im_auth: + return False + decoded_token = json.loads(JWT().unpack(other_im_auth['token']).part[1]) + password = str(decoded_token['iss']) + str(decoded_token['sub']) + # check that the token provided is associated with the current owner of the inf. + if self_im_auth['password'] != password: + return False + + return True + else: + return False \ No newline at end of file diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 45b1599e6..66cb9ee29 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -255,7 +255,7 @@ def get_infrastructure(inf_id, auth): "Error, incorrect infrastructure ID") raise IncorrectInfrastructureException() sel_inf = InfrastructureManager.infrastructure_list[inf_id] - if sel_inf.auth is not None and not sel_inf.auth.compare(auth, 'InfrastructureManager'): + if not sel_inf.is_authorized(auth): InfrastructureManager.logger.error("Access Error") raise IncorrectInfrastructureException() if sel_inf.deleted: @@ -1204,17 +1204,15 @@ def check_iam_token(im_auth): token = im_auth["token"] try: # decode the token to get the issuer - decoded_token = JWT().unpack(token) - iss = json.loads(decoded_token.part[1])['iss'] + decoded_token = json.loads(JWT().unpack(token).part[1]) # create the client using the iss url client = Client(client_authn_method=CLIENT_AUTHN_METHOD) - client.userinfo_endpoint = "%s/userinfo" % iss + client.userinfo_endpoint = "%s/userinfo" % decoded_token['iss'] userinfo = client.do_user_info_request(token=token) # convert to username to use it in the rest of the IM - del im_auth['token'] im_auth['username'] = str(userinfo.get("preferred_username")) - im_auth['password'] = str(userinfo.get("sub")) + im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) except PyoidcError, oiex: InfrastructureManager.logger.debug( "Incorrect auth token: %s" % str(oiex)) @@ -1309,7 +1307,7 @@ def GetInfrastructureList(auth): res = [] for elem in InfrastructureManager.infrastructure_list.values(): - if elem.auth is not None and elem.auth.compare(auth, 'InfrastructureManager') and not elem.deleted: + if elem.is_authorized(auth) and not elem.deleted: res.append(elem.id) return res From b175acb627eaf975fb7caf6a96410ccdb4366a17 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 10:37:58 +0200 Subject: [PATCH 267/509] Update Dockerfile to new oic requirements --- docker-devel/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 66a73c729..8891bf47d 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -17,6 +17,9 @@ RUN cd tmp \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg +# Force to reinstall pycryptodome to overwrite the pycrypto lib +RUN pip install pycryptodome --upgrade --force + # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg From dcd0531b3a03ba3e505e03e703307dd43a108391 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 10:49:38 +0200 Subject: [PATCH 268/509] Add test with an IAM token --- test/TestREST.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/TestREST.py b/test/TestREST.py index ca36618fb..b227e2c30 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -140,6 +140,26 @@ def test_10_list(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR listing user infrastructures:" + output) + + def test_12_list_with_incorrect_token(self): + f = open(AUTH_FILE) + token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" + "LCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDYyODY5MjgxLCJpYXQiOjE" + "0NjI4NjU2ODEsImp0aSI6Ijc1M2M4ZTI1LWU3MGMtNGI5MS05YWJhLTcxNDI5NTg3MzUzOSJ9.iA9nv7QdkmfgJPSQ_77_eKrvh" + "P1xwZ1Z91xzrZ0Bzue0ark4qRMlHCdZvad1tunURaSsHHMsFYQ3H7oQj-ZSYWOfr1KxMaIo4pWaVHrW8qsCMLmqdNfubR54GmTh" + "M4cA2ZdNZa8neVT8jUvzR1YX-5cz7sp2gWbW9LAwejoXDtk") + auth_data = "type = InfrastructureManager; token = %s\\n" % token + for line in f.readlines(): + if line.find("type = InfrastructureManager") == -1: + auth_data += line.strip() + "\\n" + f.close() + + self.server.request('GET', "/infrastructures", + headers={'AUTHORIZATION': auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 401, + msg="ERROR using an invalid token. A 401 error is expected:" + output) def test_15_get_incorrect_info(self): self.server.request('GET', "/infrastructures/999999", From 97d38ecfb2a39fb17c2440f7968c47e499b91db7 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 10:51:23 +0200 Subject: [PATCH 269/509] Minor changes --- IM/InfrastructureInfo.py | 10 +++++----- test/TestREST.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index c06417b97..8fa0e6b7b 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -489,16 +489,16 @@ def is_authorized(self, auth): if self.auth is not None: self_im_auth = self.auth.getAuthInfo("InfrastructureManager")[0] other_im_auth = auth.getAuthInfo("InfrastructureManager")[0] - - for elem in ['username','password']: + + for elem in ['username', 'password']: if elem not in other_im_auth: return False if self_im_auth[elem] != other_im_auth[elem]: return False - + if 'token' in self_im_auth: if 'token' not in other_im_auth: - return False + return False decoded_token = json.loads(JWT().unpack(other_im_auth['token']).part[1]) password = str(decoded_token['iss']) + str(decoded_token['sub']) # check that the token provided is associated with the current owner of the inf. @@ -507,4 +507,4 @@ def is_authorized(self, auth): return True else: - return False \ No newline at end of file + return False diff --git a/test/TestREST.py b/test/TestREST.py index b227e2c30..33a2354ac 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -140,7 +140,7 @@ def test_10_list(self): output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR listing user infrastructures:" + output) - + def test_12_list_with_incorrect_token(self): f = open(AUTH_FILE) token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" @@ -153,7 +153,7 @@ def test_12_list_with_incorrect_token(self): if line.find("type = InfrastructureManager") == -1: auth_data += line.strip() + "\\n" f.close() - + self.server.request('GET', "/infrastructures", headers={'AUTHORIZATION': auth_data}) resp = self.server.getresponse() From db52316dae58387c132472ee3d46cbd0108ff6e5 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 12:11:29 +0200 Subject: [PATCH 270/509] Update Dockerfile to new oic requirements --- docker-devel/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 8891bf47d..608331427 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -17,8 +17,8 @@ RUN cd tmp \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg -# Force to reinstall pycryptodome to overwrite the pycrypto lib -RUN pip install pycryptodome --upgrade --force +# Remove and install again pycrypto to avoid problems with pycryptodome +RUN echo y | pip uninstall pycrypto && pip install pycrypto # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg From fe1cfccaf34c0b63224fcc8b7a85146c5d221112 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 16:05:51 +0200 Subject: [PATCH 271/509] Remove oic and use interal functions to contac IAM --- IM/InfrastructureInfo.py | 5 ++- IM/InfrastructureManager.py | 30 +++++++--------- IM/openid/JWT.py | 72 +++++++++++++++++++++++++++++++++++++ IM/openid/OpenIDClient.py | 47 ++++++++++++++++++++++++ IM/openid/__init__.py | 0 docker-devel/Dockerfile | 3 -- setup.py | 2 +- 7 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 IM/openid/JWT.py create mode 100644 IM/openid/OpenIDClient.py create mode 100644 IM/openid/__init__.py diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 8fa0e6b7b..4b6c2d969 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -25,8 +25,7 @@ from radl.radl import RADL, Feature, deploy, system, contextualize_item from config import Config from Queue import PriorityQueue -from jwkest.jwt import JWT -import json +from IM.openid.JWT import JWT class IncorrectVMException(Exception): @@ -499,7 +498,7 @@ def is_authorized(self, auth): if 'token' in self_im_auth: if 'token' not in other_im_auth: return False - decoded_token = json.loads(JWT().unpack(other_im_auth['token']).part[1]) + decoded_token = JWT().get_info(other_im_auth['token']) password = str(decoded_token['iss']) + str(decoded_token['sub']) # check that the token provided is associated with the current owner of the inf. if self_im_auth['password'] != password: diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 66cb9ee29..09a98f022 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -37,11 +37,8 @@ from config import Config from IM.VirtualMachine import VirtualMachine - -from oic.oic import Client -from oic.exception import PyoidcError -from oic.utils.authn.client import CLIENT_AUTHN_METHOD -from jwkest.jwt import JWT +from IM.openid.JWT import JWT +from IM.openid.OpenIDClient import OpenIDClient if Config.MAX_SIMULTANEOUS_LAUNCHES > 1: from multiprocessing.pool import ThreadPool @@ -1204,19 +1201,16 @@ def check_iam_token(im_auth): token = im_auth["token"] try: # decode the token to get the issuer - decoded_token = json.loads(JWT().unpack(token).part[1]) - # create the client using the iss url - client = Client(client_authn_method=CLIENT_AUTHN_METHOD) - client.userinfo_endpoint = "%s/userinfo" % decoded_token['iss'] - - userinfo = client.do_user_info_request(token=token) - # convert to username to use it in the rest of the IM - im_auth['username'] = str(userinfo.get("preferred_username")) - im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) - except PyoidcError, oiex: - InfrastructureManager.logger.debug( - "Incorrect auth token: %s" % str(oiex)) - raise UnauthorizedUserException() + decoded_token = JWT().get_info(token) + success, userinfo = OpenIDClient.get_user_info_request(token) + if success: + # convert to username to use it in the rest of the IM + im_auth['username'] = str(userinfo.get("preferred_username")) + im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) + else: + InfrastructureManager.logger.error( + "Incorrect auth token: %s" % userinfo) + raise UnauthorizedUserException("Invalid InfrastructureManager credentials %s" % userinfo) except Exception, ex: InfrastructureManager.logger.exception( "Error trying to validate auth token: %s" % str(ex)) diff --git a/IM/openid/JWT.py b/IM/openid/JWT.py new file mode 100644 index 000000000..4ffdfd175 --- /dev/null +++ b/IM/openid/JWT.py @@ -0,0 +1,72 @@ +import json +import base64 +import re + +_b64_re = re.compile(b"^[A-Za-z0-9_-]*$") + +def add_padding(b): + # add padding chars + m = len(b) % 4 + if m == 1: + # NOTE: for some reason b64decode raises *TypeError* if the + # padding is incorrect. + raise Exception(b, "incorrect padding") + elif m == 2: + b += b"==" + elif m == 3: + b += b"=" + return b + +def b64d(b): + """Decode some base64-encoded bytes. + + Raises BadSyntax if the string contains invalid characters or padding. + + :param b: bytes + """ + + cb = b.rstrip(b"=") # shouldn't but there you are + + # Python's base64 functions ignore invalid characters, so we need to + # check for them explicitly. + if not _b64_re.match(cb): + raise Exception(cb, "base64-encoded data contains illegal characters") + + if cb == b: + b = add_padding(b) + + return base64.urlsafe_b64decode(b) + +class JWT(object): + def __init__(self): + self.headers = {'alg': None} + self.b64part = ['eyJhbGciOm51bGx9'] + self.part = ['{"alg":null}'] + + def unpack(self, token): + """ + Unpacks a JWT into its parts and base64 decodes the parts + individually + + :param token: The JWT + """ + try: + token = token.encode("utf-8") + except UnicodeDecodeError: + pass + + part = tuple(token.split(b".")) + self.b64part = part + self.part = [b64d(p) for p in part] + self.headers = json.loads(self.part[0].decode()) + return self + + def get_info(self, token): + """ + Unpacks a JWT into its parts and base64 decodes the parts + individually, returning the part 1 json decoded. + + :param token: The JWT + """ + self.unpack(token) + return json.loads(self.part[1]) diff --git a/IM/openid/OpenIDClient.py b/IM/openid/OpenIDClient.py new file mode 100644 index 000000000..1b999350e --- /dev/null +++ b/IM/openid/OpenIDClient.py @@ -0,0 +1,47 @@ +''' +Created on 10 de may. de 2016 + +@author: micafer +''' +import httplib +import urlparse +import json +from JWT import JWT + + +class OpenIDClient(object): + def __init__(self): + self.a = 1 + + @staticmethod + def get_connection(url): + """ + Get a HTTP/S connection with the specified server. + """ + parsed_url = urlparse.urlparse(url) + port = None + server = parsed_url[1] + if parsed_url[1].find(":") != -1: + parts = parsed_url[1].split(":") + server = parts[0] + port = int(parts[1]) + if parsed_url[0] == "https": + return httplib.HTTPSConnection(server, port) + else: + return httplib.HTTPConnection(server, port) + + @staticmethod + def get_user_info_request(token): + try: + decoded_token = JWT().get_info(token) + headers = {'Authorization': 'Bearer %s' % token} + conn = OpenIDClient.get_connection(decoded_token['iss']) + conn.request('GET', "/userinfo", headers=headers) + resp = conn.getresponse() + + output = resp.read() + if resp.status != 200: + return False, resp.reason + "\n" + output + return True, json.loads(output) + except Exception, ex: + return False, str(ex) diff --git a/IM/openid/__init__.py b/IM/openid/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 608331427..66a73c729 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -17,9 +17,6 @@ RUN cd tmp \ && python setup.py install COPY ansible.cfg /etc/ansible/ansible.cfg -# Remove and install again pycrypto to avoid problems with pycryptodome -RUN echo y | pip uninstall pycrypto && pip install pycrypto - # Turn on the REST services RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg diff --git a/setup.py b/setup.py index 8f97b7698..808e1fae3 100644 --- a/setup.py +++ b/setup.py @@ -51,5 +51,5 @@ description="IM is a tool to manage virtual infrastructures on Cloud deployments", platforms=["any"], install_requires=["ansible >= 1.8, < 2", "paramiko >= 1.14", "PyYAML", "SOAPpy", - "boto >= 2.29", "apache-libcloud >= 0.17", "RADL", "bottle", "netaddr", "scp", "oic"] + "boto >= 2.29", "apache-libcloud >= 0.17", "RADL", "bottle", "netaddr", "scp"] ) From a066224e13cee843e7e3a33c3c47ce7e1f6fbb16 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 16:06:56 +0200 Subject: [PATCH 272/509] Minor changes --- IM/openid/JWT.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IM/openid/JWT.py b/IM/openid/JWT.py index 4ffdfd175..859378da6 100644 --- a/IM/openid/JWT.py +++ b/IM/openid/JWT.py @@ -4,6 +4,7 @@ _b64_re = re.compile(b"^[A-Za-z0-9_-]*$") + def add_padding(b): # add padding chars m = len(b) % 4 @@ -17,6 +18,7 @@ def add_padding(b): b += b"=" return b + def b64d(b): """Decode some base64-encoded bytes. @@ -37,6 +39,7 @@ def b64d(b): return base64.urlsafe_b64decode(b) + class JWT(object): def __init__(self): self.headers = {'alg': None} From 885c5eb927396b026e8ffa0bcd2f79a2245daed6 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 16:28:08 +0200 Subject: [PATCH 273/509] Minor changes --- IM/openid/JWT.py | 116 ++++++++++++++++++++------------------ IM/openid/OpenIDClient.py | 19 ++++++- 2 files changed, 78 insertions(+), 57 deletions(-) diff --git a/IM/openid/JWT.py b/IM/openid/JWT.py index 859378da6..5a470bcdd 100644 --- a/IM/openid/JWT.py +++ b/IM/openid/JWT.py @@ -1,75 +1,83 @@ +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public Licenslast_updatee for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Class to unpack the JWT IAM tokens +""" import json import base64 import re -_b64_re = re.compile(b"^[A-Za-z0-9_-]*$") - - -def add_padding(b): - # add padding chars - m = len(b) % 4 - if m == 1: - # NOTE: for some reason b64decode raises *TypeError* if the - # padding is incorrect. - raise Exception(b, "incorrect padding") - elif m == 2: - b += b"==" - elif m == 3: - b += b"=" - return b - - -def b64d(b): - """Decode some base64-encoded bytes. - - Raises BadSyntax if the string contains invalid characters or padding. - - :param b: bytes - """ - - cb = b.rstrip(b"=") # shouldn't but there you are - - # Python's base64 functions ignore invalid characters, so we need to - # check for them explicitly. - if not _b64_re.match(cb): - raise Exception(cb, "base64-encoded data contains illegal characters") - - if cb == b: - b = add_padding(b) - - return base64.urlsafe_b64decode(b) - class JWT(object): - def __init__(self): - self.headers = {'alg': None} - self.b64part = ['eyJhbGciOm51bGx9'] - self.part = ['{"alg":null}'] - def unpack(self, token): + @staticmethod + def b64d(b): + """Decode some base64-encoded bytes. + + Raises Exception if the string contains invalid characters or padding. + + :param b: bytes + """ + + cb = b.rstrip(b"=") # shouldn't but there you are + + # Python's base64 functions ignore invalid characters, so we need to + # check for them explicitly. + b64_re = re.compile(b"^[A-Za-z0-9_-]*$") + if not b64_re.match(cb): + raise Exception(cb, "base64-encoded data contains illegal characters") + + if cb == b: + b = JWT.add_padding(b) + + return base64.urlsafe_b64decode(b) + + @staticmethod + def add_padding(b): + # add padding chars + m = len(b) % 4 + if m == 1: + # NOTE: for some reason b64decode raises *TypeError* if the + # padding is incorrect. + raise Exception(b, "incorrect padding") + elif m == 2: + b += b"==" + elif m == 3: + b += b"=" + return b + + @staticmethod + def unpack(token): """ Unpacks a JWT into its parts and base64 decodes the parts individually :param token: The JWT """ - try: - token = token.encode("utf-8") - except UnicodeDecodeError: - pass - part = tuple(token.split(b".")) - self.b64part = part - self.part = [b64d(p) for p in part] - self.headers = json.loads(self.part[0].decode()) - return self + part = [JWT.b64d(p) for p in part] + return part - def get_info(self, token): + @staticmethod + def get_info(token): """ Unpacks a JWT into its parts and base64 decodes the parts individually, returning the part 1 json decoded. :param token: The JWT """ - self.unpack(token) - return json.loads(self.part[1]) + part = JWT.unpack(token) + return json.loads(part[1]) diff --git a/IM/openid/OpenIDClient.py b/IM/openid/OpenIDClient.py index 1b999350e..f9870859e 100644 --- a/IM/openid/OpenIDClient.py +++ b/IM/openid/OpenIDClient.py @@ -1,7 +1,20 @@ +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public Licenslast_updatee for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . ''' -Created on 10 de may. de 2016 - -@author: micafer +Class to contact with an OpenID server ''' import httplib import urlparse From 253f54d2f93e6772dd0c16abf9f3e53a4ee9f03f Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 16:31:45 +0200 Subject: [PATCH 274/509] Add IM.openid package to setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 808e1fae3..acd605742 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', include_package_data=True, - packages=['IM', 'IM.ansible', 'IM.connectors', 'IM.tosca'], + packages=['IM', 'IM.ansible', 'IM.connectors', 'IM.tosca', 'IM.openid'], scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From e600f535ceb7bcaf3c7a32c36933527481ed8815 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 16:34:26 +0200 Subject: [PATCH 275/509] Minor changes --- IM/openid/JWT.py | 12 ++++++------ IM/openid/OpenIDClient.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/IM/openid/JWT.py b/IM/openid/JWT.py index 5a470bcdd..9a7b7ec1c 100644 --- a/IM/openid/JWT.py +++ b/IM/openid/JWT.py @@ -26,23 +26,23 @@ class JWT(object): @staticmethod def b64d(b): """Decode some base64-encoded bytes. - + Raises Exception if the string contains invalid characters or padding. - + :param b: bytes """ - + cb = b.rstrip(b"=") # shouldn't but there you are - + # Python's base64 functions ignore invalid characters, so we need to # check for them explicitly. b64_re = re.compile(b"^[A-Za-z0-9_-]*$") if not b64_re.match(cb): raise Exception(cb, "base64-encoded data contains illegal characters") - + if cb == b: b = JWT.add_padding(b) - + return base64.urlsafe_b64decode(b) @staticmethod diff --git a/IM/openid/OpenIDClient.py b/IM/openid/OpenIDClient.py index f9870859e..e1732f799 100644 --- a/IM/openid/OpenIDClient.py +++ b/IM/openid/OpenIDClient.py @@ -23,9 +23,6 @@ class OpenIDClient(object): - def __init__(self): - self.a = 1 - @staticmethod def get_connection(url): """ @@ -45,6 +42,9 @@ def get_connection(url): @staticmethod def get_user_info_request(token): + """ + Get a the user info from a token + """ try: decoded_token = JWT().get_info(token) headers = {'Authorization': 'Bearer %s' % token} From 066986f90332175a24131ed618ae5f63acbda1d6 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 17:46:53 +0200 Subject: [PATCH 276/509] Remove ansible version from conf-ansible as the repos remove old versions --- contextualization/conf-ansible.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 5e4c4af5b..d6d99a205 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -2,11 +2,6 @@ - hosts: all become: yes become_method: sudo - vars: - # Use different variables because not allways theses three repos has the same last version - PIP_ANSIBLE_VERSION: 2.0.2 - APT_ANSIBLE_VERSION: 2.0.2.* - YUM_ANSIBLE_VERSION: 2.0.1.0 tasks: - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed @@ -32,11 +27,11 @@ when: ansible_distribution == "Ubuntu" - name: Ubuntu install Ansible with apt - apt: name="ansible={{ APT_ANSIBLE_VERSION }},python-pip,python-jinja2,sshpass,openssh-client,unzip" + apt: name=ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip when: ansible_distribution == "Ubuntu" - name: Yum install Ansible RH - yum: name=ansible-{{ YUM_ANSIBLE_VERSION }},python-pip,python-jinja2,sshpass,openssh-clients,wget + yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,wget when: ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6 and ansible_distribution != "Fedora" ############################################ In other systems use pip ################################################# @@ -73,11 +68,11 @@ when: ansible_os_family == "RedHat" and ansible_distribution_major_version < 6 - name: Install ansible with Pip - pip: name=ansible version={{ PIP_ANSIBLE_VERSION }} extra_args="-I" + pip: name=ansible extra_args="-I" when: ansible_os_family == "Suse" or (ansible_os_family == "Debian" and ansible_distribution != "Ubuntu") or ansible_distribution == "Fedora" - name: Install ansible with Pip 2.6 - pip: name=ansible version={{ PIP_ANSIBLE_VERSION }} executable=pip-2.6 + pip: name=ansible executable=pip-2.6 when: ansible_os_family == "RedHat" and ansible_distribution_major_version < 6 #################################### Now install and scp and pywinrm with pip ######################################## From dd43c67783ec135b5738fc6110a5c02f7c2838a4 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 17:47:11 +0200 Subject: [PATCH 277/509] Minor changes --- IM/openid/JWT.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/IM/openid/JWT.py b/IM/openid/JWT.py index 9a7b7ec1c..9355aa642 100644 --- a/IM/openid/JWT.py +++ b/IM/openid/JWT.py @@ -60,24 +60,14 @@ def add_padding(b): return b @staticmethod - def unpack(token): + def get_info(token): """ Unpacks a JWT into its parts and base64 decodes the parts - individually + individually, returning the part 1 json decoded, where the + token info is stored. - :param token: The JWT + :param token: The JWT token """ part = tuple(token.split(b".")) part = [JWT.b64d(p) for p in part] - return part - - @staticmethod - def get_info(token): - """ - Unpacks a JWT into its parts and base64 decodes the parts - individually, returning the part 1 json decoded. - - :param token: The JWT - """ - part = JWT.unpack(token) return json.loads(part[1]) From 409057c14917913aece2166501dae10f32a91038 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 May 2016 18:50:29 +0200 Subject: [PATCH 278/509] Correctly raise the UnauthorizedUserException error --- IM/InfrastructureManager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 09a98f022..ffbd4e08e 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1199,6 +1199,7 @@ def check_im_user(auth): @staticmethod def check_iam_token(im_auth): token = im_auth["token"] + success = False try: # decode the token to get the issuer decoded_token = JWT().get_info(token) @@ -1207,15 +1208,16 @@ def check_iam_token(im_auth): # convert to username to use it in the rest of the IM im_auth['username'] = str(userinfo.get("preferred_username")) im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) - else: - InfrastructureManager.logger.error( - "Incorrect auth token: %s" % userinfo) - raise UnauthorizedUserException("Invalid InfrastructureManager credentials %s" % userinfo) except Exception, ex: InfrastructureManager.logger.exception( "Error trying to validate auth token: %s" % str(ex)) raise Exception("Error trying to validate auth token: %s" % str(ex)) + if not success: + InfrastructureManager.logger.error( + "Incorrect auth token: %s" % userinfo) + raise UnauthorizedUserException("Invalid InfrastructureManager credentials %s" % userinfo) + @staticmethod def check_auth_data(auth): # First check if it is configured to check the users from a list From ac1a8209d267d4d2bdb3647dce3a715afa3256c4 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 11 May 2016 13:55:27 +0200 Subject: [PATCH 279/509] Improve unit tests --- test/test_im_logic.py | 244 ++++++++++++++++++++++++++++++++---------- 1 file changed, 190 insertions(+), 54 deletions(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index b9d8be63d..5d8b55723 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -17,6 +17,7 @@ # along with this program. If not, see . +import os import unittest import sys from mock import Mock @@ -32,9 +33,10 @@ from IM.InfrastructureManager import InfrastructureManager as IM from IM.auth import Authentication from radl.radl import RADL, system, deploy, Feature, SoftFeatures +from radl.radl_parse import parse_radl from IM.CloudInfo import CloudInfo from IM.connectors.CloudConnector import CloudConnector - +from IM.tosca.Tosca import Tosca class TestIM(unittest.TestCase): @@ -66,15 +68,18 @@ def register_cloudconnector(self, name, cloud_connector): def gen_launch_res(self, inf, radl, requested_radl, num_vm, auth_data): res = [] - for i in range(num_vm): + for _ in range(num_vm): cloud = CloudInfo() - cloud.type = "DeployedNode" + cloud.type = "Dummy" vm = VirtualMachine(inf, "1234", cloud, radl, requested_radl) - # create the mock for the vm finalize function - vm.finalize = Mock(return_value=(True, vm)) res.append((True, vm)) return res + def get_cloud_connector_mock(self, name="MyMock0"): + cloud = type(name, (CloudConnector, object), {}) + cloud.launch = Mock(side_effect=self.gen_launch_res) + return cloud + def test_inf_creation0(self): """Create infrastructure with empty RADL.""" @@ -102,16 +107,12 @@ def test_inf_auth(self): def test_inf_addresources_without_credentials(self): """Deploy single virtual machine without credentials to check that it raises the correct exception.""" - cloud = CloudConnector - radl = RADL() radl.add( system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er")])) radl.add(deploy("s0", 1)) - cloud.launch = Mock(side_effect=self.gen_launch_res) - self.register_cloudconnector("Mock", cloud) - auth0 = self.getAuth([0], [], [("Mock", 0)]) + auth0 = self.getAuth([0], [], [("Dummy", 0)]) infId = IM.CreateInfrastructure("", auth0) with self.assertRaises(Exception) as ex: @@ -123,19 +124,13 @@ def test_inf_addresources_without_credentials(self): def test_inf_addresources0(self): """Deploy single virtual machines and test reference.""" - - cloud = CloudConnector - radl = RADL() radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) radl.add(deploy("s0", 1)) - cloud.launch = Mock(side_effect=self.gen_launch_res) - self.register_cloudconnector("Mock", cloud) - auth0 = self.getAuth([0], [], [("Mock", 0)]) + auth0 = self.getAuth([0], [], [("Dummy", 0)]) infId = IM.CreateInfrastructure("", auth0) vms = IM.AddResource(infId, str(radl), auth0) @@ -155,14 +150,12 @@ def test_inf_addresources1(self): n = 80 # Machines to deploy Config.MAX_SIMULTANEOUS_LAUNCHES = n / 2 # Test the pool - cloud = CloudConnector radl = RADL() radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) radl.add(deploy("s0", n)) - cloud.launch = Mock(side_effect=self.gen_launch_res) + cloud = self.get_cloud_connector_mock() self.register_cloudconnector("Mock", cloud) auth0 = self.getAuth([0], [], [("Mock", 0)]) infId = IM.CreateInfrastructure("", auth0) @@ -179,13 +172,11 @@ def test_inf_addresources2(self): n0, n1 = 2, 5 # Machines to deploy radl = RADL() radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) radl.add(system("s1", [Feature("disk.0.image.url", "=", "mock1://wind.ows.suc.kz"), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.private_key", "=", "private_key")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.private_key", "=", "private_key")])) radl.add(deploy("s0", n0)) radl.add(deploy("s1", n1)) @@ -194,12 +185,10 @@ def test_inf_addresources2(self): def concreteSystem(s, cloud_id): url = s.getValue("disk.0.image.url") return [s.clone()] if url.partition(":")[0] == cloud_id else [] - cloud0 = type("MyMock0", (CloudConnector, object), {}) - cloud0.launch = Mock(side_effect=self.gen_launch_res) + cloud0 = self.get_cloud_connector_mock("MyMock0") cloud0.concreteSystem = lambda _0, s, _1: concreteSystem(s, "mock0") self.register_cloudconnector("Mock0", cloud0) - cloud1 = type("MyMock1", (CloudConnector, object), {}) - cloud1.launch = Mock(side_effect=self.gen_launch_res) + cloud1 = self.get_cloud_connector_mock("MyMock1") cloud1.concreteSystem = lambda _0, s, _1: concreteSystem(s, "mock1") self.register_cloudconnector("Mock1", cloud1) auth0 = self.getAuth([0], [], [("Mock0", 0), ("Mock1", 1)]) @@ -220,15 +209,13 @@ def test_inf_addresources3(self): radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), SoftFeatures( 10, [Feature("memory.size", "<=", 500)]), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) radl.add(system("s1", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), SoftFeatures( 10, [Feature("memory.size", ">=", 800)]), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) radl.add(deploy("s0", n0)) radl.add(deploy("s1", n1)) @@ -236,12 +223,10 @@ def test_inf_addresources3(self): def concreteSystem(s, mem): return [system(s.name, [Feature("memory.size", "=", mem)])] - cloud0 = type("MyMock0", (CloudConnector, object), {}) - cloud0.launch = Mock(side_effect=self.gen_launch_res) + cloud0 = self.get_cloud_connector_mock("MyMock0") cloud0.concreteSystem = lambda _0, s, _1: concreteSystem(s, 500) self.register_cloudconnector("Mock0", cloud0) - cloud1 = type("MyMock1", (CloudConnector, object), {}) - cloud1.launch = Mock(side_effect=self.gen_launch_res) + cloud1 = self.get_cloud_connector_mock("MyMock1") cloud1.concreteSystem = lambda _0, s, _1: concreteSystem(s, 1000) self.register_cloudconnector("Mock1", cloud1) auth0 = self.getAuth([0], [0], [("Mock0", 0), ("Mock1", 1)]) @@ -261,27 +246,178 @@ def test_inf_cloud_order(self): radl = RADL() radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), Feature("cpu.count", "=", 1), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) radl.add(deploy("s0", n0)) radl.add(system("s1", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), Feature("cpu.count", "=", 1), - Feature( - "disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) radl.add(deploy("s1", n1)) - cloud0 = type("MyMock0", (CloudConnector, object), {}) - cloud0.launch = Mock(side_effect=self.gen_launch_res) + cloud0 = self.get_cloud_connector_mock("MyMock0") self.register_cloudconnector("Mock0", cloud0) - cloud1 = type("MyMock1", (CloudConnector, object), {}) - cloud1.launch = Mock(side_effect=self.gen_launch_res) + cloud1 = self.get_cloud_connector_mock("MyMock1") self.register_cloudconnector("Mock1", cloud1) auth0 = self.getAuth([0], [0], [("Mock0", 0), ("Mock1", 1)]) infId = IM.CreateInfrastructure(str(radl), auth0) self.assertEqual(cloud0.launch.call_count, n0 + n1) IM.DestroyInfrastructure(infId, auth0) + def test_get_infrastructure_list(self): + """Get infrastructure List.""" + + auth0 = self.getAuth([0]) + infId = IM.CreateInfrastructure("", auth0) + inf_ids = IM.GetInfrastructureList(auth0) + self.assertEqual(inf_ids, [infId]) + IM.DestroyInfrastructure(infId, auth0) + + def test_reconfigure(self): + """Reconfigure.""" + radl = RADL() + radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + radl.add(deploy("s0", 1)) + + auth0 = self.getAuth([0], [], [("Dummy", 0)]) + infId = IM.CreateInfrastructure(str(radl), auth0) + + reconf_radl = """configure test (\n@begin\n---\n - tasks:\n - debug: msg="RECONFIGURERADL"\n@end\n)""" + IM.Reconfigure(infId, reconf_radl, auth0) + IM.Reconfigure(infId, reconf_radl, auth0, ['0']) + + IM.DestroyInfrastructure(infId, auth0) + + def test_inf_removeresources(self): + """Deploy 4 VMs and remove 2""" + radl = RADL() + radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + radl.add(deploy("s0", 4)) + + auth0 = self.getAuth([0], [], [("Dummy", 0)]) + infId = IM.CreateInfrastructure(str(radl), auth0) + cont = IM.RemoveResource(infId, ['0', '1'], auth0) + self.assertEqual(cont, 2) + vms = IM.GetInfrastructureInfo(infId, auth0) + self.assertEqual(sorted(vms), ['2', '3']) + + IM.DestroyInfrastructure(infId, auth0) + + def test_get_vm_info(self): + """ + Test GetVMInfo and GetVMProperty and GetVMContMsg and GetInfrastructureRADL and + GetInfrastructureContMsg and GetInfrastructureState. + """ + radl = RADL() + radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + radl.add(deploy("s0", 1)) + + auth0 = self.getAuth([0], [], [("Dummy", 0)]) + infId = IM.CreateInfrastructure(str(radl), auth0) + + radl_info = IM.GetVMInfo(infId, "0", auth0) + parsed_radl_info = parse_radl(str(radl_info)) + self.assertEqual(parsed_radl_info.systems[0].getValue("state"), "running") + + state = IM.GetVMProperty(infId, "0", "state", auth0) + self.assertEqual(state, "running") + + contmsg = IM.GetVMContMsg(infId, "0", auth0) + self.assertEqual(contmsg, "") + + contmsg = IM.GetInfrastructureContMsg(infId, auth0) + + state = IM.GetInfrastructureState(infId, auth0) + self.assertEqual(state["state"], "running") + self.assertEqual(state["vm_states"]["0"], "running") + + radl_info = IM.GetInfrastructureRADL(infId, auth0) + parsed_radl_info = parse_radl(str(radl_info)) + self.assertEqual(parsed_radl_info.systems[0].getValue("disk.0.os.credentials.username"), "user") + + IM.DestroyInfrastructure(infId, auth0) + + def test_altervm(self): + """Test AlterVM""" + radl = RADL() + radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("cpu.count", "=", 1), + Feature("memory.size", "=", 512, "M"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + radl.add(deploy("s0", 1)) + + auth0 = self.getAuth([0], [], [("Dummy", 0)]) + infId = IM.CreateInfrastructure(str(radl), auth0) + + radl = RADL() + radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("cpu.count", "=", 2), + Feature("memory.size", "=", 1024, "M"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + radl.add(deploy("s0", 1)) + + radl_info = IM.AlterVM(infId, "0", str(radl), auth0) + parsed_radl_info = parse_radl(str(radl_info)) + self.assertEqual(parsed_radl_info.systems[0].getValue("cpu.count"), 2) + self.assertEqual(parsed_radl_info.systems[0].getFeature('memory.size').getValue('M'), 1024) + + IM.DestroyInfrastructure(infId, auth0) + + def test_start_stop(self): + """Test Start and Stop operations""" + radl = RADL() + radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + radl.add(deploy("s0", 1)) + + auth0 = self.getAuth([0], [], [("Dummy", 0)]) + infId = IM.CreateInfrastructure(str(radl), auth0) + + res = IM.StopInfrastructure(infId, auth0) + self.assertEqual(res, "") + res = IM.StartInfrastructure(infId, auth0) + self.assertEqual(res, "") + + res = IM.StartVM(infId, "0", auth0) + self.assertEqual(res, "") + res = IM.StopVM(infId, "0", auth0) + self.assertEqual(res, "") + + IM.DestroyInfrastructure(infId, auth0) + + def test_export_import(self): + """Test ExportInfrastructure and ImportInfrastructure operations""" + radl = RADL() + radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + radl.add(deploy("s0", 1)) + + auth0 = self.getAuth([0], [], [("Dummy", 0)]) + infId = IM.CreateInfrastructure(str(radl), auth0) + + res = IM.ExportInfrastructure(infId, True, auth0) + new_inf_id = IM.ImportInfrastructure(res, auth0) + + IM.DestroyInfrastructure(new_inf_id, auth0) + + def test_tosca_to_radl(self): + """Test TOSCA RADL translation""" + TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) + with open(TESTS_PATH + '/tosca_create.yml') as f: + tosca_data = f.read() + tosca = Tosca(tosca_data) + _, radl = tosca.to_radl() + parse_radl(str(radl)) + if __name__ == "__main__": unittest.main() From 6320b86bc80b504419d946e0c6ffb64f7edc140e Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 11 May 2016 14:01:32 +0200 Subject: [PATCH 280/509] Minor changes --- test/test_im_logic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 5d8b55723..9d1337080 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -36,7 +36,8 @@ from radl.radl_parse import parse_radl from IM.CloudInfo import CloudInfo from IM.connectors.CloudConnector import CloudConnector -from IM.tosca.Tosca import Tosca +from IM.tosca.Tosca import Tosca + class TestIM(unittest.TestCase): @@ -393,7 +394,7 @@ def test_start_stop(self): self.assertEqual(res, "") IM.DestroyInfrastructure(infId, auth0) - + def test_export_import(self): """Test ExportInfrastructure and ImportInfrastructure operations""" radl = RADL() @@ -409,7 +410,7 @@ def test_export_import(self): new_inf_id = IM.ImportInfrastructure(res, auth0) IM.DestroyInfrastructure(new_inf_id, auth0) - + def test_tosca_to_radl(self): """Test TOSCA RADL translation""" TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) From 4c17c31f5750cf01c383c3433a6437b123f8e669 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 12 May 2016 17:02:49 +0200 Subject: [PATCH 281/509] Decrease number of VMs in tests --- test/test_im_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 9d1337080..c1acc6171 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -149,7 +149,7 @@ def test_inf_addresources0(self): def test_inf_addresources1(self): """Deploy n independent virtual machines.""" - n = 80 # Machines to deploy + n = 60 # Machines to deploy Config.MAX_SIMULTANEOUS_LAUNCHES = n / 2 # Test the pool radl = RADL() radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), From 7326b396b60227691dac7628cdecddce40053533 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 12 May 2016 17:40:11 +0200 Subject: [PATCH 282/509] Bugfix in ansible install recipe --- ansible_install.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index ddd6c911c..192c7dc9c 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -17,7 +17,7 @@ pip: name=setuptools extra_args="-I" state=latest - name: pip install pbr,CherryPy and pyOpenSSL to enable HTTPS in REST API - pip: name=pbr,CherryPy,pyOpenSSL extra_args="-I" state=latest + pip: name="pbr CherryPy pyOpenSSL" extra_args="-I" state=latest - name: pip install tosca-parser pip: name=git+http://github.com/indigo-dc/tosca-parser editable=false From 7d193ffbafe2173b0c106676cdba68cf94d7b2b6 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 12 May 2016 17:52:25 +0200 Subject: [PATCH 283/509] Install the packages with pip instead of using python setup.py install --- docker-devel/Dockerfile | 4 ++-- docker/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 66a73c729..a652a6977 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -8,13 +8,13 @@ LABEL description="Container image to run the IM service with TOSCA support. (ht RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ - && python setup.py install + && pip install /tmp/tosca-parser # Install im indigo tosca fork branch 'devel' RUN cd tmp \ && git clone --branch devel --recursive https://github.com/indigo-dc/im.git \ && cd im \ - && python setup.py install + && pip install /tmp/im COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services diff --git a/docker/Dockerfile b/docker/Dockerfile index 05127daa2..f6acdfc95 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,13 +27,13 @@ RUN pip install pbr CherryPy pyOpenSSL --upgrade -I RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ && cd tosca-parser \ - && python setup.py install + && pip install /tmp/tosca-parser # Install im indigo tosca fork RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/im.git \ && cd im \ - && python setup.py install + && pip install /tmp/im COPY ansible.cfg /etc/ansible/ansible.cfg # Turn on the REST services From 398cb02d16545bb50a082589a966f7f687277b33 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 13 May 2016 10:37:17 +0200 Subject: [PATCH 284/509] Add support for new endpoint credentials --- IM/tosca/Tosca.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index d3d235af5..7e6bdce1a 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -620,17 +620,28 @@ def _get_attribute_result(self, func, node, inf_info): * { get_attribute: [ HOST, private_address ] } * { get_attribute: [ SELF, private_address ] } * { get_attribute: [ HOST, private_address, 0 ] } + * { get_attribute: [ server, endpoint, credential, 0 ] } """ node_name = func.args[0] + capability_name = None attribute_name = func.args[1] - # TODO: Currently only supports indexes + index = None - if len(func.args) > 2: + # Currently only support 2,3 or 4 parameters + if len(func.args) == 3: try: index = int(func.args[2]) except: Tosca.logger.exception("Error getting get_attribute index.") pass + elif len(func.args) == 4: + capability_name = func.args[1] + attribute_name = func.args[2] + try: + index = int(func.args[3]) + except: + Tosca.logger.exception("Error getting get_attribute index.") + pass if node_name == "HOST": node = self._find_host_compute(node, self.tosca.nodetemplates) @@ -664,7 +675,7 @@ def _get_attribute_result(self, func, node, inf_info): return vm.id elif attribute_name == "tosca_name": return node.name - elif attribute_name == "credential": + elif attribute_name == "credential" and capability_name == "endpoint": if node.type == "tosca.nodes.indigo.Compute": res = [] for vm in vm_list[node.name]: From 337689f7cb94e3be4acdf64845695b090c4cabc0 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 13 May 2016 10:42:24 +0200 Subject: [PATCH 285/509] Change TOSCA tests to use endpoints --- test/tosca_add.yml | 13 +++++++++++-- test/tosca_create.yml | 12 ++++++++++-- test/tosca_remove.yml | 13 +++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/test/tosca_add.yml b/test/tosca_add.yml index b63d0935b..b394be9a4 100644 --- a/test/tosca_add.yml +++ b/test/tosca_add.yml @@ -36,8 +36,17 @@ topology_template: web_server: type: tosca.nodes.indigo.Compute - properties: - public_ip: yes + capabilities: + endpoint: + properties: + network_name: PUBLIC + ports: + ssh_port: + protocol: tcp + source: 22 + http_port: + protocol: tcp + source: 80 capabilities: scalable: properties: diff --git a/test/tosca_create.yml b/test/tosca_create.yml index b57740a18..320acd021 100644 --- a/test/tosca_create.yml +++ b/test/tosca_create.yml @@ -36,9 +36,17 @@ topology_template: web_server: type: tosca.nodes.indigo.Compute - properties: - public_ip: yes capabilities: + endpoint: + properties: + network_name: PUBLIC + ports: + ssh_port: + protocol: tcp + source: 22 + http_port: + protocol: tcp + source: 80 # Host container properties host: properties: diff --git a/test/tosca_remove.yml b/test/tosca_remove.yml index 07d16006f..997af4cef 100644 --- a/test/tosca_remove.yml +++ b/test/tosca_remove.yml @@ -37,8 +37,17 @@ topology_template: web_server: type: tosca.nodes.indigo.Compute - properties: - public_ip: yes + capabilities: + endpoint: + properties: + network_name: PUBLIC + ports: + ssh_port: + protocol: tcp + source: 22 + http_port: + protocol: tcp + source: 80 capabilities: scalable: properties: From baf94e7225e518d07e4bfbc777870804ebbba4a5 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 16 May 2016 09:53:24 +0200 Subject: [PATCH 286/509] Enable to overwrite system info in AddResources --- IM/InfrastructureInfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 4b6c2d969..32f06e68c 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -212,14 +212,14 @@ def update_radl(self, radl, deployed_vms): """ with self._lock: - # Add new systems and networks only + # Add new networks only for s in radl.systems + radl.networks + radl.ansible_hosts: if not self.radl.add(s.clone(), "ignore"): InfrastructureInfo.logger.warn( "Ignoring the redefinition of %s %s" % (type(s), s.getId())) # Add or update configures - for s in radl.configures: + for s in radl.configures + radl.systems: self.radl.add(s.clone(), "replace") InfrastructureInfo.logger.warn( "(Re)definition of %s %s" % (type(s), s.getId())) From 728d09be070940ca143aefaff1089cc50291184b Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 16 May 2016 09:54:51 +0200 Subject: [PATCH 287/509] Bugfix setting dns_name --- IM/tosca/Tosca.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 7e6bdce1a..204632a74 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -234,8 +234,6 @@ def _add_node_nets(node, radl, system, nodetemplates): public_net.setValue("outports", Tosca._format_outports(ports)) system.setValue('net_interface.%d.connection' % num_net, public_net.id) - if dns_name: - system.setValue('net_interface.%d.dns_name' % num_net, dns_name) # The private net is always added private_nets = [] @@ -265,6 +263,9 @@ def _add_node_nets(node, radl, system, nodetemplates): system.setValue('net_interface.' + str(num_net) + '.connection', private_net.id) + if dns_name: + system.setValue('net_interface.0.dns_name', dns_name) + @staticmethod def _get_scalable_properties(node): count = min_instances = max_instances = default_instances = None From 9eb7083e5af9678772a92c31a765b866b3568b22 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 17 May 2016 09:42:53 +0200 Subject: [PATCH 288/509] Enable to keygen in OpenNebula connector --- IM/connectors/OpenNebula.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index a6b506a6b..b51f43244 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -482,8 +482,13 @@ def getONETemplate(self, radl, auth_data): # include the SSH_KEYS # It is supported since 3.8 version, (the VM must be prepared with the # ONE contextualization script) + password = system.getValue('disk.0.os.credentials.password') private = system.getValue('disk.0.os.credentials.private_key') public = system.getValue('disk.0.os.credentials.public_key') + + if not password and (not private or not public): + (public, private) = self.keygen() + system.setValue('disk.0.os.credentials.private_key', private) if (private and public) or ConfigOpenNebula.TEMPLATE_CONTEXT: res += 'CONTEXT = [' From 29d22b79bff6457e4724c5888f6637fca29353b9 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 17 May 2016 17:50:28 +0200 Subject: [PATCH 289/509] Update dockerfiles to use libcloud from indigo repo --- docker-devel/Dockerfile | 6 ++++++ docker/Dockerfile | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index a652a6977..1dd7134db 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -10,6 +10,12 @@ RUN cd tmp \ && cd tosca-parser \ && pip install /tmp/tosca-parser +# Install libcloud +RUN cd tmp \ + && git clone https://github.com/indigo-dc/libcloud.git \ + && cd libcloud \ + && pip install /tmp/libcloud + # Install im indigo tosca fork branch 'devel' RUN cd tmp \ && git clone --branch devel --recursive https://github.com/indigo-dc/im.git \ diff --git a/docker/Dockerfile b/docker/Dockerfile index f6acdfc95..ebfce15ad 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,6 +29,12 @@ RUN cd tmp \ && cd tosca-parser \ && pip install /tmp/tosca-parser +# Install libcloud +RUN cd tmp \ + && git clone https://github.com/indigo-dc/libcloud.git \ + && cd libcloud \ + && pip install /tmp/libcloud + # Install im indigo tosca fork RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/im.git \ From be3ef46f382eb16c531a1000b60e5c4e7ee86840 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 17 May 2016 17:54:00 +0200 Subject: [PATCH 290/509] Style changes --- IM/connectors/OpenNebula.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index b51f43244..32378e998 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -485,7 +485,7 @@ def getONETemplate(self, radl, auth_data): password = system.getValue('disk.0.os.credentials.password') private = system.getValue('disk.0.os.credentials.private_key') public = system.getValue('disk.0.os.credentials.public_key') - + if not password and (not private or not public): (public, private) = self.keygen() system.setValue('disk.0.os.credentials.private_key', private) From 6b6f242436520204ae0eb1162516ee404e517bbd Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 19 May 2016 12:37:36 +0200 Subject: [PATCH 291/509] Improve unittests --- test/test_im_logic.py | 65 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index c1acc6171..296d689dc 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +import time import os import unittest import sys @@ -36,6 +36,7 @@ from radl.radl_parse import parse_radl from IM.CloudInfo import CloudInfo from IM.connectors.CloudConnector import CloudConnector +from IM.SSH import SSH from IM.tosca.Tosca import Tosca @@ -67,12 +68,24 @@ def register_cloudconnector(self, name, cloud_connector): sys.modules['IM.connectors.' + name] = type('MyConnector', (object,), {name + 'CloudConnector': cloud_connector}) + def get_dummy_ssh(self, retry=False): + ssh = SSH("", "", "") + ssh.test_connectivity = Mock(return_value=True) + ssh.execute = Mock(return_value=("10", "", 0)) + ssh.sftp_put_files = Mock(return_value=True) + ssh.sftp_mkdir = Mock(return_value=True) + ssh.sftp_put_dir = Mock(return_value=True) + ssh.sftp_put = Mock(return_value=True) + return ssh + def gen_launch_res(self, inf, radl, requested_radl, num_vm, auth_data): res = [] for _ in range(num_vm): cloud = CloudInfo() cloud.type = "Dummy" vm = VirtualMachine(inf, "1234", cloud, radl, requested_radl) + vm.get_ssh = Mock(side_effect=self.get_dummy_ssh) + vm.state = VirtualMachine.RUNNING res.append((True, vm)) return res @@ -411,6 +424,56 @@ def test_export_import(self): IM.DestroyInfrastructure(new_inf_id, auth0) + def test_contextualize(self): + """Test Contextualization process""" + radl = """" + network publica (outbound = 'yes') + + system front ( + cpu.arch='x86_64' and + cpu.count>=1 and + memory.size>=512m and + net_interface.0.connection = 'publica' and + net_interface.0.ip = '10.0.0.1' and + disk.0.image.url = 'mock0://linux.for.ev.er' and + disk.0.os.credentials.username = 'ubuntu' and + disk.0.os.credentials.password = 'yoyoyo' and + disk.0.os.name = 'linux' and + disk.1.size=1GB and + disk.1.device='hdb' and + disk.1.fstype='ext4' and + disk.1.mount_path='/mnt/disk' and + disk.0.applications contains (name = 'ansible.modules.micafer.hadoop') and + disk.0.applications contains (name='gmetad') and + disk.0.applications contains (name='wget') + ) + + deploy front 1 + """ + + auth0 = self.getAuth([0], [], [("Mock", 0)]) + IM._reinit() + Config.PLAYBOOK_RETRIES = 1 + cloud0 = self.get_cloud_connector_mock("MyMock") + self.register_cloudconnector("Mock", cloud0) + infId = IM.CreateInfrastructure(str(radl), auth0) + + time.sleep(20) + + state = IM.GetInfrastructureState(infId, auth0) + self.assertEqual(state["state"], "unconfigured") + + IM.infrastructure_list[infId].ansible_configured = True + + IM.Reconfigure(infId, "", auth0) + + time.sleep(20) + + state = IM.GetInfrastructureState(infId, auth0) + self.assertEqual(state["state"], "running") + + IM.DestroyInfrastructure(infId, auth0) + def test_tosca_to_radl(self): """Test TOSCA RADL translation""" TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) From 3470e77d3fb6ed48ed630dc32bb5d821995fa5fe Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 19 May 2016 16:04:32 +0200 Subject: [PATCH 292/509] Bugfix killing ansible proceses --- IM/VirtualMachine.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/IM/VirtualMachine.py b/IM/VirtualMachine.py index 5a489887b..c8aeec55b 100644 --- a/IM/VirtualMachine.py +++ b/IM/VirtualMachine.py @@ -605,7 +605,29 @@ def kill_check_ctxt_process(self): try: VirtualMachine.logger.debug( "Killing ctxt process with pid: " + str(self.ctxt_pid)) - ssh.execute("kill -9 " + str(self.ctxt_pid)) + + # Try to get PGID to kill all child processes + pgkill_success = False + (stdout, stderr, code) = ssh.execute('ps -o "%r" ' + str(int(self.ctxt_pid))) + if code == 0: + out_parts = stdout.split("\n") + if len(out_parts) == 3: + pgid = int(out_parts[1]) + (stdout, stderr, code) = ssh.execute("kill -9 -" + str(pgid)) + if code == 0: + pgkill_success = True + else: + VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + stderr + + ". Using only PID.") + else: + VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + stdout + + ". Using only PID.") + else: + VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + stderr + + ". Using only PID.") + + if not pgkill_success: + ssh.execute("kill -9 " + str(int(self.ctxt_pid))) except: VirtualMachine.logger.exception( "Error killing ctxt process with pid: " + str(self.ctxt_pid)) @@ -636,15 +658,7 @@ def check_ctxt_process(self): ssh = self.get_ssh_ansible_master() if self.state in VirtualMachine.NOT_RUNNING_STATES: - try: - ssh.execute("kill -9 " + str(ctxt_pid)) - except: - VirtualMachine.logger.exception( - "Error killing ctxt process with pid: " + str(self.ctxt_pid)) - pass - - self.configured = False - self.ctxt_pid = None + self.kill_check_ctxt_process() else: try: (_, _, exit_status) = ssh.execute("ps " + str(ctxt_pid)) From 3c03b85940cc70f3e7ec90a7919d3c68b93dfc59 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 19 May 2016 16:11:18 +0200 Subject: [PATCH 293/509] Style changes --- IM/VirtualMachine.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/IM/VirtualMachine.py b/IM/VirtualMachine.py index c8aeec55b..74e54fb7b 100644 --- a/IM/VirtualMachine.py +++ b/IM/VirtualMachine.py @@ -605,7 +605,7 @@ def kill_check_ctxt_process(self): try: VirtualMachine.logger.debug( "Killing ctxt process with pid: " + str(self.ctxt_pid)) - + # Try to get PGID to kill all child processes pgkill_success = False (stdout, stderr, code) = ssh.execute('ps -o "%r" ' + str(int(self.ctxt_pid))) @@ -617,15 +617,15 @@ def kill_check_ctxt_process(self): if code == 0: pgkill_success = True else: - VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + stderr + - ". Using only PID.") + VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + + ": " + stderr + ". Using only PID.") else: - VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + stdout + - ". Using only PID.") + VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + + stdout + ". Using only PID.") else: - VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + stderr + - ". Using only PID.") - + VirtualMachine.logger.error("Error getting PGID of pid: " + str(self.ctxt_pid) + ": " + + stderr + ". Using only PID.") + if not pgkill_success: ssh.execute("kill -9 " + str(int(self.ctxt_pid))) except: From 7d453c88f81c77470dccdabd3d5b76abd06cb118 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 20 May 2016 10:16:03 +0200 Subject: [PATCH 294/509] Set CONTEXTUALIZATION_DIR path relative to tests dir --- test/test_im_logic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 296d689dc..cf2796845 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -454,6 +454,7 @@ def test_contextualize(self): auth0 = self.getAuth([0], [], [("Mock", 0)]) IM._reinit() Config.PLAYBOOK_RETRIES = 1 + Config.CONTEXTUALIZATION_DIR = os.path.dirname(os.path.realpath(__file__)) + "/../contextualization" cloud0 = self.get_cloud_connector_mock("MyMock") self.register_cloudconnector("Mock", cloud0) infId = IM.CreateInfrastructure(str(radl), auth0) From 86b1c432ed6584202faaf46ecd53da21035a0446 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 23 May 2016 17:08:24 +0200 Subject: [PATCH 295/509] Add DATA_DB env to config --- IM/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IM/config.py b/IM/config.py index f01065930..babe8e8dd 100644 --- a/IM/config.py +++ b/IM/config.py @@ -99,6 +99,9 @@ class Config: if config.has_section(section_name): parse_options(config, section_name, Config) +# Get some vars from env variables to make easy docker container configuration +if 'DATA_DB' in os.environ: + Config.DATA_DB = os.environ['DATA_DB'] class ConfigOpenNebula: TEMPLATE_CONTEXT = '' From 9e580e7ed84ea88a5185eb59e6e8788c089b5a98 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 23 May 2016 17:28:55 +0200 Subject: [PATCH 296/509] Add DATA_DB env to config --- IM/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/config.py b/IM/config.py index babe8e8dd..e67d2b6ae 100644 --- a/IM/config.py +++ b/IM/config.py @@ -99,7 +99,7 @@ class Config: if config.has_section(section_name): parse_options(config, section_name, Config) -# Get some vars from env variables to make easy docker container configuration +# Get some vars from environment variables to make easy docker container configuration if 'DATA_DB' in os.environ: Config.DATA_DB = os.environ['DATA_DB'] From 1f635a6be140035e83d500940190e78c484511f0 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 23 May 2016 17:31:04 +0200 Subject: [PATCH 297/509] Style change --- IM/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/config.py b/IM/config.py index e67d2b6ae..931fff465 100644 --- a/IM/config.py +++ b/IM/config.py @@ -103,6 +103,7 @@ class Config: if 'DATA_DB' in os.environ: Config.DATA_DB = os.environ['DATA_DB'] + class ConfigOpenNebula: TEMPLATE_CONTEXT = '' TEMPLATE_OTHER = 'GRAPHICS = [type="vnc",listen="0.0.0.0"]' From c368c766c625b40b370357f41223e27ce09cc65c Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 26 May 2016 16:34:49 +0200 Subject: [PATCH 298/509] Change var DATA_DB to IM_DATA_DB --- IM/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/config.py b/IM/config.py index 931fff465..eee5c4084 100644 --- a/IM/config.py +++ b/IM/config.py @@ -100,8 +100,8 @@ class Config: parse_options(config, section_name, Config) # Get some vars from environment variables to make easy docker container configuration -if 'DATA_DB' in os.environ: - Config.DATA_DB = os.environ['DATA_DB'] +if 'IM_DATA_DB' in os.environ: + Config.DATA_DB = os.environ['IM_DATA_DB'] class ConfigOpenNebula: From 74a2e4758af0a2b5edb88cb4a0b79f0f06a48662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Molt=C3=B3?= Date: Thu, 26 May 2016 16:55:53 +0200 Subject: [PATCH 299/509] Removed duplicate task in Ansible installation --- ansible_install.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index 192c7dc9c..c56f9b9eb 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -15,15 +15,12 @@ - name: pip upgrade setuptools pip: name=setuptools extra_args="-I" state=latest - + - name: pip install pbr,CherryPy and pyOpenSSL to enable HTTPS in REST API pip: name="pbr CherryPy pyOpenSSL" extra_args="-I" state=latest - name: pip install tosca-parser pip: name=git+http://github.com/indigo-dc/tosca-parser editable=false - - name: pip install tosca-parser - pip: name=git+http://github.com/indigo-dc/tosca-parser editable=false - - name: pip install IM pip: name=git+http://github.com/indigo-dc/im editable=false From ebf53a8539229faf55b6845ad13cc38fe5de5f18 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 27 May 2016 09:42:27 +0200 Subject: [PATCH 300/509] Update Dockerfile --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ebfce15ad..517060383 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ MAINTAINER Miguel Caballer LABEL version="1.4.4" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -# Update and install all the neccesary packages +# Update and install all the necessary packages RUN apt-get update && apt-get install -y \ gcc \ python-dev \ @@ -16,6 +16,7 @@ RUN apt-get update && apt-get install -y \ git \ libssl-dev \ libffi-dev \ + python-mysqldb \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* From 2da98739f55c3959bcad2a2c038824d87344c4db Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 27 May 2016 09:46:59 +0200 Subject: [PATCH 301/509] Update ansible install with mysqldb python --- ansible_install.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index c56f9b9eb..a39d600b3 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -6,11 +6,11 @@ when: ansible_os_family == "RedHat" - name: Yum install requisites - action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests,libffi-devel,openssl-devel state=installed + action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests,MySQL-python,libffi-devel,openssl-devel state=installed when: ansible_os_family == "RedHat" - name: Apt-get install requisites - apt: pkg=git,python-pip,python-dev,python-soappy,libssl-dev,libffi-dev state=installed update_cache=yes cache_valid_time=3600 + apt: pkg=git,python-pip,python-dev,python-soappy,python-mysqldb,libssl-dev,libffi-dev state=installed update_cache=yes cache_valid_time=3600 when: ansible_os_family == "Debian" - name: pip upgrade setuptools From b5f17a433ed034082ffeecfd7e45025415153fdc Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 27 May 2016 09:53:07 +0200 Subject: [PATCH 302/509] Update docker README --- docker/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/README.md b/docker/README.md index 1565b430b..d452e717c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -30,3 +30,9 @@ How to launch the IM service using docker: ```sh sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im ``` + +You can also specify an external MySQL server to store IM data using the IM_DATA_DB environment variable:: + +```sh +sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_DATA_DB=mysql://username:password@server/db_name --name im indigodatacloud/im +``` \ No newline at end of file From f58953bea0d7195d4172ba8b5a14b33c5004bd07 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 27 May 2016 10:46:38 +0200 Subject: [PATCH 303/509] Use upstream libcloud as the changes has been merged --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 517060383..d877c5696 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,9 +30,9 @@ RUN cd tmp \ && cd tosca-parser \ && pip install /tmp/tosca-parser -# Install libcloud +# Install libcloud from git untill the updates are released RUN cd tmp \ - && git clone https://github.com/indigo-dc/libcloud.git \ + && git clone https://github.com/apache/libcloud.git \ && cd libcloud \ && pip install /tmp/libcloud From 39d151dff47187fa2462bed67921fceb9e1477f8 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 27 May 2016 11:34:07 +0200 Subject: [PATCH 304/509] Increase test time --- test/test_im_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 1203521cc..e8d2f2f2e 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -470,7 +470,7 @@ def test_contextualize(self): self.register_cloudconnector("Mock", cloud0) infId = IM.CreateInfrastructure(str(radl), auth0) - time.sleep(20) + time.sleep(25) state = IM.GetInfrastructureState(infId, auth0) self.assertEqual(state["state"], "unconfigured") From 8d2060dce2d21be89a4b3f535c8909c6203a76db Mon Sep 17 00:00:00 2001 From: Alfonso Date: Mon, 30 May 2016 11:29:27 +0200 Subject: [PATCH 305/509] Update openstack connector to avoid certs validation From release version 2.7.9/3.4.3 on, Python by default attempts to perform certificate validation. More info in https://www.python.org/dev/peps/pep-0476/ This causes an error in the openstack connector --- IM/connectors/OpenStack.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/IM/connectors/OpenStack.py b/IM/connectors/OpenStack.py index 047bee54c..886ab0c72 100644 --- a/IM/connectors/OpenStack.py +++ b/IM/connectors/OpenStack.py @@ -87,6 +87,9 @@ def get_driver(self, auth_data): import libcloud.security libcloud.security.VERIFY_SSL_CERT = False + import ssl + ssl._create_default_https_context = ssl._create_unverified_context + cls = get_driver(Provider.OPENSTACK) driver = cls(auth['username'], auth['password'], ex_tenant_name=auth['tenant'], @@ -120,10 +123,8 @@ def concreteSystem(self, radl_system, auth_data): driver = self.get_driver(auth_data) res_system = radl_system.clone() - instance_type = self.get_instance_type( - driver.list_sizes(), res_system) - self.update_system_info_from_instance( - res_system, instance_type) + instance_type = self.get_instance_type(driver.list_sizes(), res_system) + self.update_system_info_from_instance(res_system, instance_type) res_system.addFeature( Feature("disk.0.image.url", "=", str_url), conflict="other", missing="other") From cb505a2109b0c55fdca4a0b9c9ac958f3b702900 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 3 Jun 2016 11:30:48 +0200 Subject: [PATCH 306/509] Force to reinstall galaxy roles --- IM/ConfManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index ad2d8bfa1..ff1eee697 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -1342,7 +1342,7 @@ def configure_ansible(self, ssh, tmp_dir): recipe_out.write( " - name: Install the % role with ansible-galaxy\n" % rolename) recipe_out.write( - " command: ansible-galaxy install %s\n" % url) + " command: ansible-galaxy install -f %s\n" % url) recipe_out.close() From 4dbea9f1c7e85d3bafecafe33ede04b019c5e98c Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 3 Jun 2016 11:31:22 +0200 Subject: [PATCH 307/509] Add domain as parameter --- IM/connectors/OpenStack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/connectors/OpenStack.py b/IM/connectors/OpenStack.py index 047bee54c..7ac8ef674 100644 --- a/IM/connectors/OpenStack.py +++ b/IM/connectors/OpenStack.py @@ -90,6 +90,7 @@ def get_driver(self, auth_data): cls = get_driver(Provider.OPENSTACK) driver = cls(auth['username'], auth['password'], ex_tenant_name=auth['tenant'], + ex_domain_name=parameters['domain'], ex_force_auth_url=parameters["auth_url"], ex_force_auth_version=parameters["auth_version"], ex_force_service_region=parameters["service_region"], From 84180b4091ed9e7f80b14d27a6abe4c71acb58b3 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 3 Jun 2016 11:33:10 +0200 Subject: [PATCH 308/509] Improvements in get_attribute function and minor changer --- IM/tosca/Tosca.py | 52 +++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 204632a74..9d7a62c6f 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -738,11 +738,17 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "private_address": if node.type == "tosca.nodes.indigo.Compute": # This only works with Ansible 2.1, wait for it to be released - # return "{{ groups['%s']|map('extract', hostvars, - # 'IM_NODE_PRIVATE_IP')|list }}" % node.name - return ("""|\n {%% set comma = joiner(",") %%}\n [ {%% for host in groups['%s'] %%}""" - """\n {{ comma() }}"{{ hostvars[host]['IM_NODE_PRIVATE_IP'] }}"\n """ - """{%% endfor %%} ]""" % node.name) + # return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PRIVATE_IP')|list }}" % node.name + if index is not None: + return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PRIVATE_IP'] }}" % (node.name, index) + else: + return ("""|\n""" + """ {%% if '%s' in groups %%}""" + """{%% set comma = joiner(",") %%}""" + """[{%% for host in groups['%s'] %%}""" + """{{ comma() }}"{{ hostvars[host]['IM_NODE_PRIVATE_IP'] }}" """ + """{%% endfor %%} ]""" + """{%% else %%}[]{%% endif %%}""" % (node.name, node.name)) else: if node_name in ["HOST", "SELF"]: return "{{ IM_NODE_PRIVATE_IP }}" @@ -751,11 +757,17 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "public_address": if node.type == "tosca.nodes.indigo.Compute": # This only works with Ansible 2.1, wait for it to be released - # return "{{ groups['%s']|map('extract', hostvars, - # 'IM_NODE_PUBLIC_IP')|list }}" % node.name - return ("""|\n {%% set comma = joiner(",") %%}\n [ {%% for host in groups['%s'] %%}""" - """\n {{ comma() }}"{{ hostvars[host]['IM_NODE_PUBLIC_IP'] }}"\n """ - """{%% endfor %%} ]""" % node.name) + # return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PUBLIC_IP')|list }}" % node.name + if index is not None: + return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PUBLIC_IP'] }}" % (node.name, index) + else: + return ("""|\n""" + """ {%% if '%s' in groups %%}""" + """{%% set comma = joiner(",") %%}""" + """[{%% for host in groups['%s'] %%}""" + """{{ comma() }}"{{ hostvars[host]['IM_NODE_PUBLIC_IP'] }}" """ + """{%% endfor %%} ]""" + """{%% else %%}[]{%% endif %%}""" % (node.name, node.name)) else: if node_name in ["HOST", "SELF"]: return "{{ IM_NODE_PUBLIC_IP }}" @@ -1024,7 +1036,7 @@ def _add_ansible_roles(node, nodetemplates, system): Find all the roles to be applied to this node and add them to the system as ansible.modules.* in 'disk.0.applications' """ - + roles = [] for other_node in nodetemplates: root_type = Tosca._get_root_parent_type(other_node).type if root_type == "tosca.nodes.Compute": @@ -1043,12 +1055,16 @@ def _add_ansible_roles(node, nodetemplates, system): name if ('type' in artifact and artifact['type'] == 'tosca.artifacts.AnsibleGalaxy.role' and 'file' in artifact and artifact['file']): - app_features = Features() - app_features.addFeature( - Feature('name', '=', 'ansible.modules.' + artifact['file'])) - feature = Feature( - 'disk.0.applications', 'contains', app_features) - system.addFeature(feature) + if artifact['file'] not in roles: + roles.append(artifact['file']) + + for role in roles: + app_features = Features() + app_features.addFeature( + Feature('name', '=', 'ansible.modules.' + role)) + feature = Feature( + 'disk.0.applications', 'contains', app_features) + system.addFeature(feature) @staticmethod def _gen_system(node, nodetemplates): @@ -1252,6 +1268,7 @@ def _merge_yaml(yaml1, yaml2): Tosca.logger.exception( "Error parsing YAML: " + yaml1 + "\n Ignore it") + yamlo2s = {} try: yamlo2s = yaml.load(yaml2) if not isinstance(yamlo2s, list) or any([not isinstance(d, dict) for d in yamlo2s]): @@ -1259,7 +1276,6 @@ def _merge_yaml(yaml1, yaml2): except Exception: Tosca.logger.exception( "Error parsing YAML: " + yaml2 + "\n Ignore it") - yamlo2s = {} if not yamlo2s and not yamlo1o: return "" From ed99378d6fd9cff778281c55ee50cbf14fed6327 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 3 Jun 2016 11:33:51 +0200 Subject: [PATCH 309/509] Change IM version in dockerfile --- docker-devel/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 1dd7134db..f4a74c3e3 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile to create a container with the IM service FROM grycapjenkins/im-base:latest MAINTAINER Miguel Caballer -LABEL version="1.4.4" +LABEL version="1.4.5" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" # Install tosca-parser From 8354d0d5ebfdd0b5fc84ede43fb414bde5398782 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 7 Jun 2016 11:52:08 +0200 Subject: [PATCH 310/509] Bugfix in ctxt_agent --- contextualization/ctxt_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contextualization/ctxt_agent.py b/contextualization/ctxt_agent.py index 5cf88bb09..d0747ef9e 100755 --- a/contextualization/ctxt_agent.py +++ b/contextualization/ctxt_agent.py @@ -237,7 +237,7 @@ def changeVMCredentials(vm, pk_file): logger.error( "Error changing password to Windows VM: " + r.std_out) return False - except winrm.exceptions.UnauthorizedError: + except winrm.exceptions.AuthenticationError: # if the password is correctly changed the command returns this # error try: From 4da0557b32958d1352d304bd55e5e26b28a2c94d Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 8 Jun 2016 15:02:24 +0200 Subject: [PATCH 311/509] Style changes --- test/test_im_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 75ad077c5..728ba2156 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -469,7 +469,7 @@ def test_contextualize(self): Config.CONFMAMAGER_CHECK_STATE_INTERVAL = 0.001 cloud0 = self.get_cloud_connector_mock("MyMock") self.register_cloudconnector("Mock", cloud0) - + infId = IM.CreateInfrastructure(str(radl), auth0) time.sleep(2) From 4e81c8d06657788dd53591b4b2ae4d8ca1032e13 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 9 Jun 2016 12:17:18 +0200 Subject: [PATCH 312/509] Bugfix --- IM/InfrastructureManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 4466f2fda..42fa77789 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1221,11 +1221,11 @@ def check_iam_token(im_auth): @staticmethod def check_auth_data(auth): # First check if it is configured to check the users from a list - im_auth = auth.getAuthInfo("InfrastructureManager")[0] + im_auth = auth.getAuthInfo("InfrastructureManager") # First check if the IAM token is included if "token" in im_auth: - InfrastructureManager.check_iam_token(im_auth) + InfrastructureManager.check_iam_token(im_auth[0]) else: # if not assume the basic user/password auth data if not InfrastructureManager.check_im_user(im_auth): From a63ab3e17f6f5da27c177db66026e2ce536e9250 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 9 Jun 2016 12:41:55 +0200 Subject: [PATCH 313/509] Improve tests --- test/TestREST.py | 6 +++--- test/{ => files}/tosca_add.yml | 0 test/{ => files}/tosca_create.yml | 0 test/{ => files}/tosca_remove.yml | 0 test/test_im_logic.py | 23 ++++++++++++++++++++++- 5 files changed, 25 insertions(+), 4 deletions(-) rename test/{ => files}/tosca_add.yml (100%) rename test/{ => files}/tosca_create.yml (100%) rename test/{ => files}/tosca_remove.yml (100%) diff --git a/test/TestREST.py b/test/TestREST.py index 114112de4..56ba21a06 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -479,7 +479,7 @@ def test_93_create_tosca(self): """ Test the CreateInfrastructure IM function with a TOSCA document """ - with open(TESTS_PATH + '/tosca_create.yml') as f: + with open(TESTS_PATH + '/files/tosca_create.yml') as f: tosca = f.read() self.server.request('POST', "/infrastructures", body=tosca, @@ -511,7 +511,7 @@ def test_95_add_tosca(self): """ Test the AddResource IM function with a TOSCA document """ - with open(TESTS_PATH + '/tosca_add.yml') as f: + with open(TESTS_PATH + '/files/tosca_add.yml') as f: tosca = f.read() self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, @@ -538,7 +538,7 @@ def test_96_remove_tosca(self): """ Test the RemoveResource IM function with a TOSCA document """ - with open(TESTS_PATH + '/tosca_remove.yml') as f: + with open(TESTS_PATH + '/files/tosca_remove.yml') as f: tosca = f.read() self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, diff --git a/test/tosca_add.yml b/test/files/tosca_add.yml similarity index 100% rename from test/tosca_add.yml rename to test/files/tosca_add.yml diff --git a/test/tosca_create.yml b/test/files/tosca_create.yml similarity index 100% rename from test/tosca_create.yml rename to test/files/tosca_create.yml diff --git a/test/tosca_remove.yml b/test/files/tosca_remove.yml similarity index 100% rename from test/tosca_remove.yml rename to test/files/tosca_remove.yml diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 9c76bb911..cc7d855cc 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -509,7 +509,7 @@ def test_contextualize(self): def test_tosca_to_radl(self): """Test TOSCA RADL translation""" TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) - with open(TESTS_PATH + '/tosca_create.yml') as f: + with open(TESTS_PATH + '/files/tosca_create.yml') as f: tosca_data = f.read() tosca = Tosca(tosca_data) _, radl = tosca.to_radl() @@ -532,6 +532,27 @@ def test_db(self, db): success = IM.save_data_to_db("mysql://username:password@server/db_name", {"1": inf}) self.assertTrue(success) + @patch('httplib.HTTPSConnection') + def test_0check_iam_token(self, connection): + im_auth = {"token": "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg5ODM1N2FiYSJ9.DwpZizVaYtvIj7fagQqDFpDh96szFupf6BNMIVLcopqQtZ9dBvwN9lgZ_w7Htvb3r-erho_hcme5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXUz5BzxBvANko"} + + TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) + with open(TESTS_PATH + '/files/iam_user_info.json') as f: + user_info = f.read() + + conn = MagicMock() + connection.return_value = conn + + resp = MagicMock() + resp.status = 200 + resp.read.return_value = user_info + conn.getresponse.return_value = resp + + IM.check_iam_token(im_auth) + + self.assertEqual(im_auth['username'], "micafer") + self.assertEqual(im_auth['password'], "https://iam-test.indigo-datacloud.eu/sub") + if __name__ == "__main__": unittest.main() From 3339e415ff4814e1f6bb1d89a524173d88ce0409 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 9 Jun 2016 12:55:33 +0200 Subject: [PATCH 314/509] Style changes --- test/files/iam_user_info.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test/files/iam_user_info.json diff --git a/test/files/iam_user_info.json b/test/files/iam_user_info.json new file mode 100644 index 000000000..bea6cf394 --- /dev/null +++ b/test/files/iam_user_info.json @@ -0,0 +1,20 @@ +{ + "sub": "sub", + "name": "Miguel", + "preferred_username": "micafer", + "family_name": "Caballer", + "email": "", + "email_verified": true, + "phone_number_verified": false, + "groups": [ + { + "id": "gid", + "name": "Users" + }, + { + "id": "gid", + "name": "Developers" + } + ], + "organisation_name": "indigo-dc" +} From 14f36e6b57d410552273929d8da595661c843808 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 9 Jun 2016 12:55:38 +0200 Subject: [PATCH 315/509] Style changes --- test/test_im_logic.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index cc7d855cc..059dd81df 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -43,6 +43,7 @@ from IM.tosca.Tosca import Tosca + class TestIM(unittest.TestCase): def __init__(self, *args): @@ -534,18 +535,23 @@ def test_db(self, db): @patch('httplib.HTTPSConnection') def test_0check_iam_token(self, connection): - im_auth = {"token": "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg5ODM1N2FiYSJ9.DwpZizVaYtvIj7fagQqDFpDh96szFupf6BNMIVLcopqQtZ9dBvwN9lgZ_w7Htvb3r-erho_hcme5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXUz5BzxBvANko"} + im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" + "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" + "joxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg" + "5ODM1N2FiYSJ9.DwpZizVaYtvIj7fagQqDFpDh96szFupf6BNMIVLcopqQtZ9dBvwN9lgZ_w7Htvb3r-erho_hc" + "me5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXU" + "z5BzxBvANko")} TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) with open(TESTS_PATH + '/files/iam_user_info.json') as f: user_info = f.read() - + conn = MagicMock() connection.return_value = conn resp = MagicMock() resp.status = 200 - resp.read.return_value = user_info + resp.read.return_value = user_info conn.getresponse.return_value = resp IM.check_iam_token(im_auth) From e4a97f771c42bd9cf42f1bf4756d14c30f5a4cb7 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 9 Jun 2016 13:06:48 +0200 Subject: [PATCH 316/509] Remove DB test --- test/test_im_logic.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 059dd81df..1c3613ae9 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -516,23 +516,6 @@ def test_tosca_to_radl(self): _, radl = tosca.to_radl() parse_radl(str(radl)) - @patch('IM.db.mdb') - def test_db(self, db): - conn = MagicMock() - db.connect.return_value = conn - - cursor = MagicMock() - conn.cursor.return_value = cursor - cursor.fetchall.return_value = [("1", "S'a'\np0\n.")] - - res = IM.get_data_from_db("mysql://username:password@server/db_name") - self.assertEqual(res, {}) - - inf = InfrastructureInfo() - inf.id = "1" - success = IM.save_data_to_db("mysql://username:password@server/db_name", {"1": inf}) - self.assertTrue(success) - @patch('httplib.HTTPSConnection') def test_0check_iam_token(self, connection): im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" From 0d5ebc1cbf8f08abe8f0002831fa3e3ac15cea7c Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 9 Jun 2016 13:18:05 +0200 Subject: [PATCH 317/509] Minor change --- test/test_im_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 1c3613ae9..a9ccfdc69 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -192,7 +192,7 @@ def test_inf_addresources0(self): def test_inf_addresources1(self): """Deploy n independent virtual machines.""" - n = 60 # Machines to deploy + n = 40 # Machines to deploy Config.MAX_SIMULTANEOUS_LAUNCHES = n / 2 # Test the pool radl = RADL() radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), From f313f9784e9428c353d73b2513aaa85a584eb349 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 9 Jun 2016 13:23:51 +0200 Subject: [PATCH 318/509] Minor change --- test/test_im_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index a9ccfdc69..6e90a8b46 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -517,7 +517,7 @@ def test_tosca_to_radl(self): parse_radl(str(radl)) @patch('httplib.HTTPSConnection') - def test_0check_iam_token(self, connection): + def test_check_iam_token(self, connection): im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" "joxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg" From 43e3d0d518577cc9f6fa38f3e0501da0657b0b03 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 10:32:54 +0200 Subject: [PATCH 319/509] Bugfixes --- IM/tosca/Tosca.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 9d7a62c6f..f5b6ce624 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -8,7 +8,7 @@ from IM.uriparse import uriparse from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef -from toscaparser.functions import Function, is_function, get_function, GetAttribute, Concat +from toscaparser.functions import Function, is_function, get_function, GetAttribute, Concat, Token from radl.radl import system, deploy, network, Feature, Features, configure, contextualize_item, RADL, contextualize @@ -111,8 +111,7 @@ def to_radl(self, inf_info=None): "Node %s has not compute node to host in." % node.name) interfaces = Tosca._get_interfaces(node) - interfaces.update( - Tosca._get_relationships_interfaces(relationships, node)) + interfaces.update(Tosca._get_relationships_interfaces(relationships, node)) conf = self._gen_configure_from_interfaces( radl, node, interfaces, compute) @@ -571,19 +570,22 @@ def _get_intrinsic_value(self, func, node, inf_info): res = "" for item in items: if is_function(item): - res += str(self._final_function_result(item, - node, inf_info)) + res += str(self._final_function_result(item, node, inf_info)) else: res += str(item) return res elif func_name == "token": + items = func["token"] if len(items) == 3: string_with_tokens = items[0] string_of_token_chars = items[1] substring_index = int(items[2]) + if is_function(string_with_tokens): + string_with_tokens = str(self._final_function_result(string_with_tokens, node, inf_info)) + parts = string_with_tokens.split(string_of_token_chars) - if len(parts) >= substring_index: + if len(parts) > substring_index: return parts[substring_index] else: Tosca.logger.error( @@ -807,6 +809,9 @@ def _final_function_result(self, func, node, inf_info=None): elif isinstance(func, Concat): func = self._get_intrinsic_value( {"concat": func.args}, node, inf_info) + elif isinstance(func, Token): + func = self._get_intrinsic_value( + {"token": func.args}, node, inf_info) else: func = func.result() From 3b47d7f42edb7acc19ddc48d48aa26033081ce84 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 10:33:21 +0200 Subject: [PATCH 320/509] Improve tosca tests --- test/files/tosca_long.yml | 127 ++++++++++++++++++++++++++++++++++++++ test/test_im_logic.py | 2 +- 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 test/files/tosca_long.yml diff --git a/test/files/tosca_long.yml b/test/files/tosca_long.yml new file mode 100644 index 000000000..44d482076 --- /dev/null +++ b/test/files/tosca_long.yml @@ -0,0 +1,127 @@ +tosca_definitions_version: tosca_simple_yaml_1_0 + +imports: + - indigo_custom_types: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/custom_types.yaml + +description: > + TOSCA test for launching a Virtual Elastic Cluster. It will launch + a single front-end that will be in change of managing the elasticity + using the specified LRMS (torque, sge, slurm and condor) workload. + +topology_template: + + node_templates: + + elastic_cluster_front_end: + type: tosca.nodes.indigo.ElasticCluster + properties: + # fake value to test token intrinsic functions + deployment_id: { token: [ get_attribute: [ lrms_server, public_address, 0 ], ':', 0 ] } + # fake value to test concat intrinsic functions + orchestrator_url: { concat: [ 'http://', get_attribute: [ lrms_server, public_address, 0 ], ':8080' ] } + requirements: + - lrms: lrms_front_end + - wn: wn_node + + lrms_front_end: + type: tosca.nodes.indigo.LRMS.FrontEnd.Slurm + properties: + wn_ips: { get_attribute: [ lrms_wn, private_address ] } + requirements: + - host: lrms_server + + lrms_server: + type: tosca.nodes.indigo.Compute + capabilities: + endpoint: + properties: + dns_name: slurmserver + network_name: PUBLIC + ports: + http_port: + protocol: tcp + source: 8080 + host: + properties: + num_cpus: 1 + mem_size: 1 GB + os: + properties: + # host Operating System image properties + type: linux + #distribution: scientific + #version: 6.6 + requirements: + - local_storage: + node: my_onedata_storage + relationship: + type: AttachesTo + properties: + location: /mnt/disk + interfaces: + Configure: + pre_configure_source: + implementation: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/onedata/oneclient_install.yml + inputs: + onedata_token: { get_property: [ TARGET, credential, token ] } + onedata_location: { get_property: [ SELF, location ] } + + my_onedata_storage: + type: tosca.nodes.indigo.OneDataStorage + properties: + oneprovider_host: ["oneprovider.com", "twoprovider.net"] + dataspace: ["space1","space2"] + onezone_endpoint: http://server.com + credential: + token: some_token + token_type: token + + wn_node: + type: tosca.nodes.indigo.LRMS.WorkerNode.Slurm + properties: + front_end_ip: { get_attribute: [ lrms_server, private_address, 0 ] } + capabilities: + wn: + properties: + max_instances: 5 + min_instances: 0 + requirements: + - host: lrms_wn + + lrms_wn: + type: tosca.nodes.indigo.Compute + capabilities: + scalable: + properties: + count: 0 + host: + properties: + num_cpus: 1 + mem_size: 2 GB + os: + properties: + # host Operating System image properties + type: linux + #distribution: scientific + #version: 6.6 + + mysql: + type: tosca.nodes.DBMS + requirements: + - host: + node_filter: + capabilities: + # Constraints for selecting “host” (Container Capability) + - host: + properties: + - num_cpus: { in_range: [ 1, 4 ] } + - mem_size: { greater_or_equal: 2 GB } + # Constraints for selecting “os” (OperatingSystem Capability) + - os: + properties: + - type: linux + + outputs: + galaxy_url: + value: { concat: [ 'http://', get_attribute: [ lrms_server, public_address, 0 ], ':8080' ] } + diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 6e90a8b46..969f21dca 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -510,7 +510,7 @@ def test_contextualize(self): def test_tosca_to_radl(self): """Test TOSCA RADL translation""" TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) - with open(TESTS_PATH + '/files/tosca_create.yml') as f: + with open(TESTS_PATH + '/files/tosca_long.yml') as f: tosca_data = f.read() tosca = Tosca(tosca_data) _, radl = tosca.to_radl() From 67deed16c14d126b10c43da9e5a9790a8b3712bf Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 10:33:50 +0200 Subject: [PATCH 321/509] Set root default user in ONE conn --- IM/connectors/OpenNebula.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index ed2b90eac..9b060959e 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -178,6 +178,10 @@ def concreteSystem(self, radl_system, auth_data): res_system.addFeature(Feature( "provider.port", "=", self.cloud.port), conflict="other", missing="other") + username = res_system.getValue('disk.0.os.credentials.username') + if not username: + res_system.setValue('disk.0.os.credentials.username', 'root') + res.append(res_system) return res From c71598186c506ec55a3a1798f2c88ea0e84805be Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 11:11:56 +0200 Subject: [PATCH 322/509] Add new tosca test --- test/test_im_logic.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 969f21dca..d9ec40a5b 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -516,6 +516,21 @@ def test_tosca_to_radl(self): _, radl = tosca.to_radl() parse_radl(str(radl)) + def test_tosca_get_outputs(self): + """Test TOSCA get_outputs function""" + TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) + with open(TESTS_PATH + '/files/tosca_create.yml') as f: + tosca_data = f.read() + tosca = Tosca(tosca_data) + _, radl = tosca.to_radl() + radl.systems[0].setValue("net_interface.0.ip", "158.42.1.1") + inf = InfrastructureInfo() + vm = VirtualMachine(inf, "1", None, radl, radl, None) + vm.requested_radl = radl + inf.vm_list = [vm] + outputs = tosca.get_outputs(inf) + print outputs + @patch('httplib.HTTPSConnection') def test_check_iam_token(self, connection): im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" From 9a50d27b21c1599e5e8114a26c0f2506ae9c5499 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 11:26:11 +0200 Subject: [PATCH 323/509] Comment get_outputs tests --- test/test_im_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index d9ec40a5b..005b306b7 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -528,8 +528,8 @@ def test_tosca_get_outputs(self): vm = VirtualMachine(inf, "1", None, radl, radl, None) vm.requested_radl = radl inf.vm_list = [vm] - outputs = tosca.get_outputs(inf) - print outputs + #outputs = tosca.get_outputs(inf) + #self.assertEqual(outputs, {'server_url': ['158.42.1.1']}) @patch('httplib.HTTPSConnection') def test_check_iam_token(self, connection): From 241f4cd3b0459383e66ed8358796af92f55e7a4a Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 11:28:34 +0200 Subject: [PATCH 324/509] Comment get_outputs tests --- test/test_im_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 005b306b7..82fb59fd8 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -528,8 +528,8 @@ def test_tosca_get_outputs(self): vm = VirtualMachine(inf, "1", None, radl, radl, None) vm.requested_radl = radl inf.vm_list = [vm] - #outputs = tosca.get_outputs(inf) - #self.assertEqual(outputs, {'server_url': ['158.42.1.1']}) + # outputs = tosca.get_outputs(inf) + # self.assertEqual(outputs, {'server_url': ['158.42.1.1']}) @patch('httplib.HTTPSConnection') def test_check_iam_token(self, connection): From ca97b5f00f9df63a24520ec06733b40df4097f9a Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 11:30:40 +0200 Subject: [PATCH 325/509] Add new tosca test --- test/test_im_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 82fb59fd8..2935103ab 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -528,8 +528,8 @@ def test_tosca_get_outputs(self): vm = VirtualMachine(inf, "1", None, radl, radl, None) vm.requested_radl = radl inf.vm_list = [vm] - # outputs = tosca.get_outputs(inf) - # self.assertEqual(outputs, {'server_url': ['158.42.1.1']}) + outputs = tosca.get_outputs(inf) + self.assertEqual(outputs, {'server_url': ['158.42.1.1']}) @patch('httplib.HTTPSConnection') def test_check_iam_token(self, connection): From b62fd367618068fecbace13d8a5d2d4904a3b5c0 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 11:39:44 +0200 Subject: [PATCH 326/509] Update ansible install recipe --- ansible_install.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ansible_install.yaml b/ansible_install.yaml index a39d600b3..e95d62867 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -24,3 +24,30 @@ - name: pip install IM pip: name=git+http://github.com/indigo-dc/im editable=false + +################################################ Configure Ansible ################################################### + + - name: Create /etc/ansible + file: path=/etc/ansible state=directory + + - name: Set host_key_checking to false in ansible.cfg + ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=host_key_checking value=False + + - name: Set transport to ssh in ansible.cfg + ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=transport value=ssh + when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version >= 10) + + - name: Set transport to smart in ansible.cfg + ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=transport value=smart + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version < 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version < 10) + + - name: Change ssh_args to set ControlPersist to 15 min in ansible.cfg + ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=ssh_args value="-o ControlMaster=auto -o ControlPersist=900s" + when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version >= 12) + + - name: Change ssh_args to remove ControlPersist in REL 6 and older in ansible.cfg + ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=ssh_args value="" + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version < 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version < 12) + + - name: Activate SSH pipelining in ansible.cfg + ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=pipelining value=True \ No newline at end of file From a431edfe783dc255d51a96ba7efe7360f9f9974d Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 12:42:58 +0200 Subject: [PATCH 327/509] Bugfix --- IM/InfrastructureManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 42fa77789..19e8ee4bb 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1224,7 +1224,7 @@ def check_auth_data(auth): im_auth = auth.getAuthInfo("InfrastructureManager") # First check if the IAM token is included - if "token" in im_auth: + if "token" in im_auth[0]: InfrastructureManager.check_iam_token(im_auth[0]) else: # if not assume the basic user/password auth data From 95348d16f5fbcaeb422476b794ec31ab5d6c9b5d Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 10 Jun 2016 12:58:59 +0200 Subject: [PATCH 328/509] Improve ansible recipe --- ansible_install.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index e95d62867..05645e2d0 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -6,11 +6,11 @@ when: ansible_os_family == "RedHat" - name: Yum install requisites - action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests,MySQL-python,libffi-devel,openssl-devel state=installed + action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests,MySQL-python,libffi-devel,openssl-devel,unzip state=installed when: ansible_os_family == "RedHat" - name: Apt-get install requisites - apt: pkg=git,python-pip,python-dev,python-soappy,python-mysqldb,libssl-dev,libffi-dev state=installed update_cache=yes cache_valid_time=3600 + apt: pkg=git,python-pip,python-dev,python-soappy,python-mysqldb,libssl-dev,libffi-dev,unzip state=installed update_cache=yes cache_valid_time=3600 when: ansible_os_family == "Debian" - name: pip upgrade setuptools @@ -19,11 +19,17 @@ - name: pip install pbr,CherryPy and pyOpenSSL to enable HTTPS in REST API pip: name="pbr CherryPy pyOpenSSL" extra_args="-I" state=latest + - name: Download tosca-parser + git: repo=https://github.com/indigo-dc/tosca-parser dest=/tmp/tosca-parser + - name: pip install tosca-parser - pip: name=git+http://github.com/indigo-dc/tosca-parser editable=false + pip: name=/tmp/tosca-parser + + - name: Download IM + git: repo=https://github.com/indigo-dc/im dest=/tmp/im - name: pip install IM - pip: name=git+http://github.com/indigo-dc/im editable=false + pip: name=/tmp/im ################################################ Configure Ansible ################################################### From 4bbf584b9f50242f86bfe5a5dc3d2a91a2e194d4 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 13 Jun 2016 09:53:18 +0200 Subject: [PATCH 329/509] Bugfix --- IM/connectors/OpenNebula.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 9b060959e..77f8d80e3 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -678,6 +678,10 @@ def getONENetworks(self, auth_data): self.logger.error("Unknown type of network") continue + if not ip: + self.logger.error("No IP found for network: %s. Ignoring network." % net.NAME) + continue + is_public = not (any([IPAddress(ip) in IPNetwork(mask) for mask in Config.PRIVATE_NET_MASKS])) From b9fee5a4af8c0f135236bdec79af26f5084eb22d Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 13 Jun 2016 11:10:55 +0200 Subject: [PATCH 330/509] Bugfix with IAM auth --- IM/REST.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IM/REST.py b/IM/REST.py index 4e579189c..3fcbbe773 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -320,6 +320,7 @@ def RESTGetInfrastructureProperty(id=None, prop=None): if accept and "application/json" not in accept and "*/*" not in accept and "application/*" not in accept: return return_error(415, "Unsupported Accept Media Types: %s" % accept) bottle.response.content_type = "application/json" + auth = InfrastructureManager.check_auth_data(auth) sel_inf = InfrastructureManager.get_infrastructure(id, auth) if "TOSCA" in sel_inf.extra_info: res = sel_inf.extra_info["TOSCA"].get_outputs(sel_inf) @@ -494,6 +495,7 @@ def RESTAddResource(id=None): radl_data = parse_radl_json(radl_data) elif "text/yaml" in content_type: tosca_data = Tosca(radl_data) + auth = InfrastructureManager.check_auth_data(auth) sel_inf = InfrastructureManager.get_infrastructure(id, auth) remove_list, radl_data = tosca_data.to_radl(sel_inf) elif "text/plain" in content_type or "*/*" in content_type or "text/*" in content_type: From 59302ab071d674b0f1a59809c7ed8bc66767bd26 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 13 Jun 2016 18:48:14 +0200 Subject: [PATCH 331/509] Remove LibVirt conn --- IM/connectors/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/connectors/__init__.py b/IM/connectors/__init__.py index 34d4687ed..c100f11b4 100644 --- a/IM/connectors/__init__.py +++ b/IM/connectors/__init__.py @@ -15,5 +15,5 @@ # along with this program. If not, see . -__all__ = ['CloudConnector', 'EC2', 'OCCI', 'OpenNebula', 'OpenStack', 'LibVirt', +__all__ = ['CloudConnector', 'EC2', 'OCCI', 'OpenNebula', 'OpenStack', 'LibCloud', 'Docker', 'GCE', 'FogBow', 'Azure', 'DeployedNode', 'Kubernetes', 'Dummy'] From f53fc3bf4f8f2247b52768b8a495570ca6658ad3 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 13 Jun 2016 18:48:49 +0200 Subject: [PATCH 332/509] Improve TOSCA tests --- test/files/tosca_create.yml | 4 +++- test/test_im_logic.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/test/files/tosca_create.yml b/test/files/tosca_create.yml index 320acd021..a4d5d9b8e 100644 --- a/test/files/tosca_create.yml +++ b/test/files/tosca_create.yml @@ -108,4 +108,6 @@ topology_template: outputs: server_url: - value: { get_attribute: [ web_server, public_address ] } \ No newline at end of file + value: { get_attribute: [ web_server, public_address ] } + server_creds: + value: { get_attribute: [ web_server, endpoint, credential, 0 ] } \ No newline at end of file diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 2afeaba89..207339ff9 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -530,12 +530,17 @@ def test_tosca_get_outputs(self): tosca = Tosca(tosca_data) _, radl = tosca.to_radl() radl.systems[0].setValue("net_interface.0.ip", "158.42.1.1") + radl.systems[0].setValue("disk.0.os.credentials.username", "ubuntu") + radl.systems[0].setValue("disk.0.os.credentials.password", "pass") inf = InfrastructureInfo() vm = VirtualMachine(inf, "1", None, radl, radl, None) vm.requested_radl = radl inf.vm_list = [vm] outputs = tosca.get_outputs(inf) - self.assertEqual(outputs, {'server_url': ['158.42.1.1']}) + self.assertEqual(outputs, {'server_url': ['158.42.1.1'], + 'server_creds': {'token_type': 'password', + 'token': 'pass', + 'user': 'ubuntu'}}) @patch('httplib.HTTPSConnection') def test_check_iam_token(self, connection): From 45cc2e18bdd5c2882a640ee8b09e2de50689956d Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 14 Jun 2016 12:30:59 +0200 Subject: [PATCH 333/509] Style changes --- test/test_im_logic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_im_logic.py b/test/test_im_logic.py index 207339ff9..2980a36ae 100755 --- a/test/test_im_logic.py +++ b/test/test_im_logic.py @@ -43,7 +43,6 @@ from IM.tosca.Tosca import Tosca - def read_file_as_string(file_name): tests_path = os.path.dirname(os.path.abspath(__file__)) abs_file_path = os.path.join(tests_path, file_name) From 98f9fc3b05e0003711ddcf68e1298de60b7e5f3c Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 16 Jun 2016 18:09:19 +0200 Subject: [PATCH 334/509] Add TTS suport in OpenNebula conn --- IM/config.py | 1 + IM/connectors/OpenNebula.py | 45 ++++++++++++++--- IM/tts/__init__.py | 0 IM/tts/tts.py | 98 +++++++++++++++++++++++++++++++++++++ etc/im.cfg | 3 +- 5 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 IM/tts/__init__.py create mode 100644 IM/tts/tts.py diff --git a/IM/config.py b/IM/config.py index eee5c4084..c5a0bc288 100644 --- a/IM/config.py +++ b/IM/config.py @@ -108,6 +108,7 @@ class ConfigOpenNebula: TEMPLATE_CONTEXT = '' TEMPLATE_OTHER = 'GRAPHICS = [type="vnc",listen="0.0.0.0"]' IMAGE_UNAME = '' + TTS_URL = 'http://localhost:8080' if config.has_section("OpenNebula"): parse_options(config, 'OpenNebula', ConfigOpenNebula) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 77f8d80e3..2252a2d47 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -26,6 +26,8 @@ from IM.config import ConfigOpenNebula from netaddr import IPNetwork, IPAddress from IM.config import Config +from IM.tts.tts import TTSClient +from IM.openid.JWT import JWT # Set of classes to parse the XML results of the ONE API @@ -186,6 +188,32 @@ def concreteSystem(self, radl_system, auth_data): return res + def get_auth_from_tts(self, token): + tts_uri = uriparse(ConfigOpenNebula.TTS_URL) + scheme = tts_uri[0] + host = tts_uri[1] + port = None + if host.find(":") != -1: + parts = host.split(":") + host = parts[0] + port = int(parts[1]) + + decoded_token = JWT.get_info(token) + ttsc = TTSClient(token, decoded_token['iss'], host, port, scheme) + + svc = ttsc.find_service_id("opennebula", self.cloud.server) + succes, cred = ttsc.request_credential(svc["id"]) + if succes: + username = password = None + for elem in cred: + if elem['name'] == 'Username': + username = elem['value'] + elif elem['name'] == 'Password': + password = elem['value'] + return username, password + else: + return None, None + def getSessionID(self, auth_data, hash_password=None): """ Get the ONE Session ID from the auth data @@ -198,9 +226,7 @@ def getSessionID(self, auth_data, hash_password=None): """ auths = auth_data.getAuthInfo(self.type, self.cloud.server) if not auths: - self.logger.error( - "No correct auth data has been specified to OpenNebula.") - return None + raise Exception("No auth data has been specified to OpenNebula.") else: auth = auths[0] @@ -214,10 +240,15 @@ def getSessionID(self, auth_data, hash_password=None): passwd = hashlib.sha1(passwd.strip()).hexdigest() return auth['username'] + ":" + passwd + elif 'token' in auth: + username, passwd = self.get_auth_from_tts(auth['token']) + if not username or not passwd: + raise Exception("Error getting ONE credentials using TTS.") + auth["username"] = username + auth["password"] = passwd + return username + ":" + passwd else: - self.logger.error( - "No correct auth data has been specified to OpenNebula: username and password") - return None + raise Exception("No correct auth data has been specified to OpenNebula: username and password") def setDisksFromTemplate(self, vm, template): """ @@ -359,7 +390,7 @@ def finalize(self, vm, auth_data): session_id = self.getSessionID(auth_data) if session_id is None: return (False, "Incorrect auth data, username and password must be specified for OpenNebula provider.") - func_res = server.one.vm.action(session_id, 'finalize', int(vm.id)) + func_res = server.one.vm.action(session_id, 'delete', int(vm.id)) if len(func_res) == 1: success = True diff --git a/IM/tts/__init__.py b/IM/tts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/IM/tts/tts.py b/IM/tts/tts.py new file mode 100644 index 000000000..b4b2880d3 --- /dev/null +++ b/IM/tts/tts.py @@ -0,0 +1,98 @@ +''' +Created on 16 de jun. de 2016 + +@author: micafer +''' + +import json +import httplib + + +class TTSClient: + + def __init__(self, token, iss, host, port=None, uri_scheme=None): + self.host = host + self.port = port + if not self.port: + self.port = 8080 + self.token = token + self.iss = iss + self.uri_scheme = uri_scheme + if not self.uri_scheme: + self.uri_scheme = "http" + + def _get_http_connection(self): + """ + Get the HTTP connection to contact the TTS server + """ + if self.uri_scheme == 'https': + conn = httplib.HTTPSConnection(self.host, self.port) + else: + conn = httplib.HTTPConnection(self.host, self.port) + + return conn + + def _perform_get(self, url): + headers = {} + headers['Authorization'] = 'Bearer %s' % self.token + headers['Content-Type'] = 'application/json' + #headers['Connection'] = 'close' + headers['X-OpenId-Connect-Issuer'] = self.iss + conn = self._get_http_connection() + conn.request('GET', url, headers=headers) + resp = conn.getresponse() + output = resp.read() + + if resp.status >= 200 and resp.status <= 299: + return True, output + else: + return False, "Error code %d. Msg: %s" % (resp.status, output) + + def _perform_post(self, url, body): + conn = self._get_http_connection() + + conn.putrequest('POST', url) + + conn.putheader('Authorization', 'Bearer %s' % self.token) + conn.putheader('Content-Type', 'application/json') + conn.putheader('X-OpenId-Connect-Issuer', self.iss) + #conn.putheader('Connection', 'close') + + conn.putheader('Content-Length', len(body)) + conn.endheaders(body) + + resp = conn.getresponse() + output = str(resp.read()) + + if resp.status == 303: + return self._perform_get(resp.msg['location']) + elif resp.status >= 200 and resp.status <= 299: + return True, output + else: + return False, "Error code %d. Msg: %s" % (resp.status, output) + + def request_credential(self, sid): + body = '{"service_id":"%s"}' % sid + url = "/api/credential/" + success, res = self._perform_post(url, body) + if success: + return True, json.loads(res) + else: + return False, res + + def list_endservices(self): + url = "/api/service" + success, output = self._perform_get(url) + if not success: + return False, output + else: + return True, json.loads(output) + + def find_service_id(self, stype, host): + success, services = self.list_endservices() + if success: + for service in services["service_list"]: + if service["type"] == stype and service["host"] == host: + return service + + return None \ No newline at end of file diff --git a/etc/im.cfg b/etc/im.cfg index 1aee87942..9bc2c949e 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -111,4 +111,5 @@ TEMPLATE_CONTEXT = TEMPLATE_OTHER = GRAPHICS = [type="vnc",listen="0.0.0.0", keymap="es"] # Set the IMAGE_UNAME value in case of using the name of the disk image in the Template IMAGE_UNAME = oneadmin - +# URL of the Indigo TTS +TTS_URL = http://localhost:8080 From 7f35cd5d9c30714e769def73f67500b14497acee Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 16 Jun 2016 18:09:45 +0200 Subject: [PATCH 335/509] Raise error in case of incorrect auth data --- IM/connectors/OCCI.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/IM/connectors/OCCI.py b/IM/connectors/OCCI.py index 30d5cdec2..d2c2b98cf 100644 --- a/IM/connectors/OCCI.py +++ b/IM/connectors/OCCI.py @@ -74,9 +74,7 @@ def get_http_connection(self, auth_data): """ auths = auth_data.getAuthInfo(self.type, self.cloud.server) if not auths: - self.logger.error( - "No correct auth data has been specified to OCCI.") - auth = None + raise Exception("No correct auth data has been specified to OCCI.") else: auth = auths[0] From 549e3ee88cee7338a070edc25c3fe5a7560e47c8 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 16 Jun 2016 18:22:08 +0200 Subject: [PATCH 336/509] Add comments and style changes --- IM/connectors/OpenNebula.py | 9 ++++++--- IM/tts/tts.py | 27 +++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 2252a2d47..fb40594df 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -189,6 +189,9 @@ def concreteSystem(self, radl_system, auth_data): return res def get_auth_from_tts(self, token): + """ + Get username and password from the TTS service + """ tts_uri = uriparse(ConfigOpenNebula.TTS_URL) scheme = tts_uri[0] host = tts_uri[1] @@ -197,11 +200,11 @@ def get_auth_from_tts(self, token): parts = host.split(":") host = parts[0] port = int(parts[1]) - + decoded_token = JWT.get_info(token) ttsc = TTSClient(token, decoded_token['iss'], host, port, scheme) - - svc = ttsc.find_service_id("opennebula", self.cloud.server) + + svc = ttsc.find_service("opennebula", self.cloud.server) succes, cred = ttsc.request_credential(svc["id"]) if succes: username = password = None diff --git a/IM/tts/tts.py b/IM/tts/tts.py index b4b2880d3..0c25eae12 100644 --- a/IM/tts/tts.py +++ b/IM/tts/tts.py @@ -9,6 +9,10 @@ class TTSClient: + """ + Class to interact with the TTS + https://github.com/indigo-dc/tts + """ def __init__(self, token, iss, host, port=None, uri_scheme=None): self.host = host @@ -33,10 +37,12 @@ def _get_http_connection(self): return conn def _perform_get(self, url): + """ + Perform the GET operation on the TTS with the specified URL + """ headers = {} headers['Authorization'] = 'Bearer %s' % self.token headers['Content-Type'] = 'application/json' - #headers['Connection'] = 'close' headers['X-OpenId-Connect-Issuer'] = self.iss conn = self._get_http_connection() conn.request('GET', url, headers=headers) @@ -49,6 +55,10 @@ def _perform_get(self, url): return False, "Error code %d. Msg: %s" % (resp.status, output) def _perform_post(self, url, body): + """ + Perform the POR operation on the TTS with the specified URL + and using the body specified + """ conn = self._get_http_connection() conn.putrequest('POST', url) @@ -56,7 +66,6 @@ def _perform_post(self, url, body): conn.putheader('Authorization', 'Bearer %s' % self.token) conn.putheader('Content-Type', 'application/json') conn.putheader('X-OpenId-Connect-Issuer', self.iss) - #conn.putheader('Connection', 'close') conn.putheader('Content-Length', len(body)) conn.endheaders(body) @@ -65,6 +74,7 @@ def _perform_post(self, url, body): output = str(resp.read()) if resp.status == 303: + # in case of redirection get the response from the new URL return self._perform_get(resp.msg['location']) elif resp.status >= 200 and resp.status <= 299: return True, output @@ -72,6 +82,9 @@ def _perform_post(self, url, body): return False, "Error code %d. Msg: %s" % (resp.status, output) def request_credential(self, sid): + """ + Request a credential for the specified service + """ body = '{"service_id":"%s"}' % sid url = "/api/credential/" success, res = self._perform_post(url, body) @@ -81,6 +94,9 @@ def request_credential(self, sid): return False, res def list_endservices(self): + """ + Get the list of services + """ url = "/api/service" success, output = self._perform_get(url) if not success: @@ -88,11 +104,14 @@ def list_endservices(self): else: return True, json.loads(output) - def find_service_id(self, stype, host): + def find_service(self, stype, host): + """ + Find a service with the specified type and host values + """ success, services = self.list_endservices() if success: for service in services["service_list"]: if service["type"] == stype and service["host"] == host: return service - return None \ No newline at end of file + return None From 4a3af3aa52aa928194358baa6cef3740b04bb6eb Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 09:09:29 +0200 Subject: [PATCH 337/509] Add TTSClient tests --- test/tts.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100755 test/tts.py diff --git a/test/tts.py b/test/tts.py new file mode 100755 index 000000000..4bea3cc05 --- /dev/null +++ b/test/tts.py @@ -0,0 +1,115 @@ +#! /usr/bin/env python +# +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest +import os + +from IM.tts.tts import TTSClient +from radl import radl_parse +from mock import patch, MagicMock + + +class TestTTSClient(unittest.TestCase): + """ + Class to test the TTCLient class + """ + @classmethod + def setUpClass(cls): + cls.last_op = None, None + + def get_response(self): + method, url = self.__class__.last_op + + resp = MagicMock() + + if method == "GET": + if "/api/credential/somecred" == url: + resp.status = 200 + resp.read.return_value = ('[{"name": "Username", "type": "text", "value": "username"},' + '{"name": "Password", "type": "text", "value": "password"}]') + if "/api/service" == url: + resp.status = 200 + resp.read.return_value = ('{"service_list": [{"id":"sid", "type":"stype", "host": "shost"}]}') + elif method == "POST": + if url == "/api/credential/": + resp.status = 303 + resp.msg = {'location': "/api/credential/somecred"} + + return resp + + def request(self, method, url, body=None, headers={}): + self.__class__.last_op = method, url + + @patch('httplib.HTTPConnection') + def test_list_endservices(self, connection): + conn = MagicMock() + connection.return_value = conn + + conn.request.side_effect = self.request + conn.putrequest.side_effect = self.request + conn.getresponse.side_effect = self.get_response + + token = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0" + iss = "https://iam-test.indigo-datacloud.eu/" + ttsc = TTSClient(token, iss, "localhost") + success, services = ttsc.list_endservices() + + expected_services = {"service_list": [{"id":"sid", "type":"stype", "host": "shost"}]} + + self.assertTrue(success, msg="ERROR: getting services: %s." % services) + self.assertEqual(services, expected_services, msg="ERROR: getting services: Unexpected services.") + + @patch('httplib.HTTPConnection') + def test_find_service(self, connection): + conn = MagicMock() + connection.return_value = conn + + conn.request.side_effect = self.request + conn.putrequest.side_effect = self.request + conn.getresponse.side_effect = self.get_response + + token = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0" + iss = "https://iam-test.indigo-datacloud.eu/" + ttsc = TTSClient(token, iss, "localhost") + service = ttsc.find_service("stype", "shost") + + expected_service = {"id":"sid", "type":"stype", "host": "shost"} + + self.assertEqual(service, expected_service, msg="ERROR: finding service: Unexpected service.") + + @patch('httplib.HTTPConnection') + def test_request_credential(self, connection): + conn = MagicMock() + connection.return_value = conn + + conn.request.side_effect = self.request + conn.putrequest.side_effect = self.request + conn.getresponse.side_effect = self.get_response + + token = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0" + iss = "https://iam-test.indigo-datacloud.eu/" + ttsc = TTSClient(token, iss, "localhost") + success, cred = ttsc.request_credential("sid") + + expected_cred = [{'name': 'Username', 'type': 'text', 'value': 'username'}, + {'name': 'Password', 'type': 'text', 'value': 'password'}] + + self.assertTrue(success, msg="ERROR: getting credentials: %s." % cred) + self.assertEqual(cred, expected_cred, msg="ERROR: getting credentials: Unexpected credetials.") +if __name__ == '__main__': + unittest.main() From 80c5e8cb131f860052e93ccefe9161617afbff88 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 09:10:05 +0200 Subject: [PATCH 338/509] Minor change --- test/VMRC.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/VMRC.py b/test/VMRC.py index 814ffadf3..2591fa306 100755 --- a/test/VMRC.py +++ b/test/VMRC.py @@ -26,7 +26,7 @@ class TestVMRC(unittest.TestCase): """ - Class to test the SSH class + Class to test the VMRC class """ @patch('SOAPpy.SOAPProxy') From 5033f0ece950f2ed3ae59dbad3a9ef71b0a060db Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 09:17:05 +0200 Subject: [PATCH 339/509] Minor change --- test/connectors/Azure.py | 1 + test/connectors/Docker.py | 1 + test/connectors/EC2.py | 1 + test/connectors/Fogbow.py | 1 + test/connectors/GCE.py | 1 + test/connectors/Kubernetes.py | 1 + test/connectors/LibCloud.py | 1 + test/connectors/OCCI.py | 1 + test/connectors/OpenNebula.py | 1 + test/connectors/OpenStack.py | 1 + 10 files changed, 10 insertions(+) diff --git a/test/connectors/Azure.py b/test/connectors/Azure.py index de83d2899..e38c7ca82 100755 --- a/test/connectors/Azure.py +++ b/test/connectors/Azure.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/Docker.py b/test/connectors/Docker.py index b10c18b63..a1cf71ba7 100755 --- a/test/connectors/Docker.py +++ b/test/connectors/Docker.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/EC2.py b/test/connectors/EC2.py index 07c486a6d..12cddc764 100755 --- a/test/connectors/EC2.py +++ b/test/connectors/EC2.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/Fogbow.py b/test/connectors/Fogbow.py index b5d69c8e2..1bec7c10a 100755 --- a/test/connectors/Fogbow.py +++ b/test/connectors/Fogbow.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/GCE.py b/test/connectors/GCE.py index 8961297c0..8dbe410c6 100755 --- a/test/connectors/GCE.py +++ b/test/connectors/GCE.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/Kubernetes.py b/test/connectors/Kubernetes.py index 4dcad401c..841c68a73 100755 --- a/test/connectors/Kubernetes.py +++ b/test/connectors/Kubernetes.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/LibCloud.py b/test/connectors/LibCloud.py index 7684627ac..d30da9ced 100755 --- a/test/connectors/LibCloud.py +++ b/test/connectors/LibCloud.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/OCCI.py b/test/connectors/OCCI.py index 07aea0975..90e287625 100755 --- a/test/connectors/OCCI.py +++ b/test/connectors/OCCI.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/OpenNebula.py b/test/connectors/OpenNebula.py index 8ef2148f7..5ba1104e1 100755 --- a/test/connectors/OpenNebula.py +++ b/test/connectors/OpenNebula.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication diff --git a/test/connectors/OpenStack.py b/test/connectors/OpenStack.py index f93f8790c..d6a0ad39a 100755 --- a/test/connectors/OpenStack.py +++ b/test/connectors/OpenStack.py @@ -23,6 +23,7 @@ import logging.config from StringIO import StringIO +sys.path.append(".") sys.path.append("..") from IM.CloudInfo import CloudInfo from IM.auth import Authentication From 0019b87d9704bfe3ec9846238b7d2f63494b1dec Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 09:19:58 +0200 Subject: [PATCH 340/509] Style changes --- test/tts.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/test/tts.py b/test/tts.py index 4bea3cc05..40053b84f 100755 --- a/test/tts.py +++ b/test/tts.py @@ -31,6 +31,13 @@ class TestTTSClient(unittest.TestCase): @classmethod def setUpClass(cls): cls.last_op = None, None + token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" + "LCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE" + "0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi" + "0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_" + "oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0") + iss = "https://iam-test.indigo-datacloud.eu/" + cls.ttsc = TTSClient(token, iss, "localhost") def get_response(self): method, url = self.__class__.last_op @@ -51,7 +58,7 @@ def get_response(self): resp.msg = {'location': "/api/credential/somecred"} return resp - + def request(self, method, url, body=None, headers={}): self.__class__.last_op = method, url @@ -64,12 +71,9 @@ def test_list_endservices(self, connection): conn.putrequest.side_effect = self.request conn.getresponse.side_effect = self.get_response - token = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0" - iss = "https://iam-test.indigo-datacloud.eu/" - ttsc = TTSClient(token, iss, "localhost") - success, services = ttsc.list_endservices() + success, services = self.ttsc.list_endservices() - expected_services = {"service_list": [{"id":"sid", "type":"stype", "host": "shost"}]} + expected_services = {"service_list": [{"id": "sid", "type": "stype", "host": "shost"}]} self.assertTrue(success, msg="ERROR: getting services: %s." % services) self.assertEqual(services, expected_services, msg="ERROR: getting services: Unexpected services.") @@ -83,12 +87,9 @@ def test_find_service(self, connection): conn.putrequest.side_effect = self.request conn.getresponse.side_effect = self.get_response - token = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0" - iss = "https://iam-test.indigo-datacloud.eu/" - ttsc = TTSClient(token, iss, "localhost") - service = ttsc.find_service("stype", "shost") + service = self.ttsc.find_service("stype", "shost") - expected_service = {"id":"sid", "type":"stype", "host": "shost"} + expected_service = {"id": "sid", "type": "stype", "host": "shost"} self.assertEqual(service, expected_service, msg="ERROR: finding service: Unexpected service.") @@ -101,11 +102,8 @@ def test_request_credential(self, connection): conn.putrequest.side_effect = self.request conn.getresponse.side_effect = self.get_response - token = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0" - iss = "https://iam-test.indigo-datacloud.eu/" - ttsc = TTSClient(token, iss, "localhost") - success, cred = ttsc.request_credential("sid") - + success, cred = self.ttsc.request_credential("sid") + expected_cred = [{'name': 'Username', 'type': 'text', 'value': 'username'}, {'name': 'Password', 'type': 'text', 'value': 'password'}] From f7d025e002f8d4120dc875bee6c89a98b9ecb226 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 11:49:42 +0200 Subject: [PATCH 341/509] set MAX_SIMULTANEOUS_LAUNCHES to 5 --- etc/im.cfg | 2 +- test/functional/test_im.py | 189 ++++++++++++++++++ test/{ => integration}/QuickTestIM.py | 19 +- test/{ => integration}/TestIM.py | 19 +- test/{ => integration}/TestREST.py | 26 +-- test/{ => integration}/TestREST_JSON.py | 22 +- test/{ => unit}/SSH.py | 30 +-- test/{ => unit}/VMRC.py | 0 test/{ => unit}/connectors/Azure.py | 0 test/{ => unit}/connectors/Docker.py | 0 test/{ => unit}/connectors/EC2.py | 0 test/{ => unit}/connectors/Fogbow.py | 0 test/{ => unit}/connectors/GCE.py | 0 test/{ => unit}/connectors/Kubernetes.py | 0 test/{ => unit}/connectors/LibCloud.py | 0 test/{ => unit}/connectors/OCCI.py | 0 test/{ => unit}/connectors/OpenNebula.py | 0 test/{ => unit}/connectors/OpenStack.py | 0 .../connectors/files/focci_instance.txt | 0 .../connectors/files/focci_resource.txt | 0 test/{ => unit}/connectors/files/nets.xml | 0 test/{ => unit}/connectors/files/occi.txt | 0 .../connectors/files/occi_vm_info.txt | 0 test/{ => unit}/connectors/files/vm_info.xml | 0 .../connectors/files/vm_info_off.xml | 0 test/{ => unit}/test_im_logic.py | 20 +- test/{ => unit}/tts.py | 0 27 files changed, 251 insertions(+), 76 deletions(-) create mode 100755 test/functional/test_im.py rename test/{ => integration}/QuickTestIM.py (97%) rename test/{ => integration}/TestIM.py (98%) rename test/{ => integration}/TestREST.py (98%) rename test/{ => integration}/TestREST_JSON.py (95%) rename test/{ => unit}/SSH.py (92%) rename test/{ => unit}/VMRC.py (100%) rename test/{ => unit}/connectors/Azure.py (100%) rename test/{ => unit}/connectors/Docker.py (100%) rename test/{ => unit}/connectors/EC2.py (100%) rename test/{ => unit}/connectors/Fogbow.py (100%) rename test/{ => unit}/connectors/GCE.py (100%) rename test/{ => unit}/connectors/Kubernetes.py (100%) rename test/{ => unit}/connectors/LibCloud.py (100%) rename test/{ => unit}/connectors/OCCI.py (100%) rename test/{ => unit}/connectors/OpenNebula.py (100%) rename test/{ => unit}/connectors/OpenStack.py (100%) rename test/{ => unit}/connectors/files/focci_instance.txt (100%) rename test/{ => unit}/connectors/files/focci_resource.txt (100%) rename test/{ => unit}/connectors/files/nets.xml (100%) rename test/{ => unit}/connectors/files/occi.txt (100%) rename test/{ => unit}/connectors/files/occi_vm_info.txt (100%) rename test/{ => unit}/connectors/files/vm_info.xml (100%) rename test/{ => unit}/connectors/files/vm_info_off.xml (100%) rename test/{ => unit}/test_im_logic.py (97%) rename test/{ => unit}/tts.py (100%) diff --git a/etc/im.cfg b/etc/im.cfg index 9bc2c949e..b7a6fbc70 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -33,7 +33,7 @@ USER_DB = # Maximum number of simultaneous VM launch/delete operations # In some old versions of python (prior to 2.7.5 or 3.3.2) it can produce an error # See https://bugs.python.org/issue10015. In this case set this value to 1 -MAX_SIMULTANEOUS_LAUNCHES = 1 +MAX_SIMULTANEOUS_LAUNCHES = 5 # Max number of retries launching a VM (always > 0) MAX_VM_FAILS = 1 diff --git a/test/functional/test_im.py b/test/functional/test_im.py new file mode 100755 index 000000000..4aec53f63 --- /dev/null +++ b/test/functional/test_im.py @@ -0,0 +1,189 @@ +#! /usr/bin/env python +# +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import time +import logging +import unittest +import sys + +from mock import Mock, patch, MagicMock + +sys.path.append("..") +sys.path.append(".") + +from IM.config import Config +# To load the ThreadPool class +Config.MAX_SIMULTANEOUS_LAUNCHES = 2 + +from IM.VirtualMachine import VirtualMachine +from IM.InfrastructureManager import InfrastructureManager as IM +from IM.auth import Authentication +from radl.radl import RADL, system, deploy, Feature, SoftFeatures +from radl.radl_parse import parse_radl +from IM.CloudInfo import CloudInfo +from IM.connectors.CloudConnector import CloudConnector +from IM.SSH import SSH +from IM.InfrastructureInfo import InfrastructureInfo +from IM.tosca.Tosca import Tosca + + +def read_file_as_string(file_name): + tests_path = os.path.dirname(os.path.abspath(__file__)) + abs_file_path = os.path.join(tests_path, file_name) + return open(abs_file_path, 'r').read() + + +class TestIM(unittest.TestCase): + + def __init__(self, *args): + unittest.TestCase.__init__(self, *args) + + def setUp(self): + + IM._reinit() + # Patch save_data + IM.save_data = staticmethod(lambda *args: None) + + ch = logging.StreamHandler(sys.stdout) + log = logging.getLogger('InfrastructureManager') + log.setLevel(logging.ERROR) + log.propagate = 0 + log.addHandler(ch) + log = logging.getLogger('ConfManager') + log.setLevel(logging.DEBUG) + log.propagate = 0 + log.addHandler(ch) + + def tearDown(self): + IM.stop() + + @staticmethod + def getAuth(im_users=[], vmrc_users=[], clouds=[]): + return Authentication([ + {'id': 'im%s' % i, 'type': 'InfrastructureManager', 'username': 'user%s' % i, + 'password': 'pass%s' % i} for i in im_users] + [ + {'id': 'vmrc%s' % i, 'type': 'VMRC', 'username': 'vmrcuser%s' % i, + 'password': 'pass%s' % i, 'host': 'hostname'} for i in vmrc_users] + [ + {'id': 'cloud%s' % i, 'type': c, 'username': 'user%s' % i, + 'password': 'pass%s' % i, 'host': 'http://server.com:80/path'} for c, i in clouds]) + + def register_cloudconnector(self, name, cloud_connector): + sys.modules['IM.connectors.' + name] = type('MyConnector', (object,), + {name + 'CloudConnector': cloud_connector}) + + def get_dummy_ssh(self, retry=False): + ssh = SSH("", "", "") + ssh.test_connectivity = Mock(return_value=True) + ssh.execute = Mock(return_value=("10", "", 0)) + ssh.sftp_put_files = Mock(return_value=True) + ssh.sftp_mkdir = Mock(return_value=True) + ssh.sftp_put_dir = Mock(return_value=True) + ssh.sftp_put = Mock(return_value=True) + return ssh + + def gen_launch_res(self, inf, radl, requested_radl, num_vm, auth_data): + res = [] + for _ in range(num_vm): + cloud = CloudInfo() + cloud.type = "Dummy" + vm = VirtualMachine(inf, "1234", cloud, radl, requested_radl) + vm.get_ssh = Mock(side_effect=self.get_dummy_ssh) + vm.state = VirtualMachine.RUNNING + res.append((True, vm)) + return res + + def get_cloud_connector_mock(self, name="MyMock0"): + cloud = type(name, (CloudConnector, object), {}) + cloud.launch = Mock(side_effect=self.gen_launch_res) + return cloud + + + def test_inf_lifecycle(self): + """Test Infrastructure lifecycle""" + radl = """" + network publica (outbound = 'yes') + + system front ( + cpu.arch='x86_64' and + cpu.count>=1 and + memory.size>=512m and + net_interface.0.connection = 'publica' and + net_interface.0.ip = '10.0.0.1' and + disk.0.image.url = 'mock0://linux.for.ev.er' and + disk.0.os.credentials.username = 'ubuntu' and + disk.0.os.credentials.password = 'yoyoyo' and + disk.0.os.name = 'linux' and + disk.1.size=1GB and + disk.1.device='hdb' and + disk.1.fstype='ext4' and + disk.1.mount_path='/mnt/disk' and + disk.0.applications contains (name = 'ansible.modules.micafer.hadoop') and + disk.0.applications contains (name='gmetad') and + disk.0.applications contains (name='wget') + ) + + deploy front 1 + """ + + auth0 = self.getAuth([0], [], [("Mock", 0)]) + IM._reinit() + Config.PLAYBOOK_RETRIES = 1 + Config.CONTEXTUALIZATION_DIR = os.path.dirname(os.path.realpath(__file__)) + "/../../contextualization" + Config.CONFMAMAGER_CHECK_STATE_INTERVAL = 0.001 + cloud0 = self.get_cloud_connector_mock("MyMock") + self.register_cloudconnector("Mock", cloud0) + + infId = IM.CreateInfrastructure(str(radl), auth0) + + time.sleep(5) + + state = IM.GetInfrastructureState(infId, auth0) + self.assertEqual(state["state"], "unconfigured") + + IM.infrastructure_list[infId].ansible_configured = True + + IM.Reconfigure(infId, "", auth0) + + time.sleep(2) + + state = IM.GetInfrastructureState(infId, auth0) + self.assertEqual(state["state"], "running") + + add_radl = RADL() + add_radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) + add_radl.add(deploy("s0", 1)) + + vms = IM.AddResource(infId, str(add_radl), auth0) + self.assertEqual(vms, [1]) + + state = IM.GetVMProperty(infId, "1", "state", auth0) + self.assertEqual(state, "running") + + contmsg = IM.GetVMContMsg(infId, "1", auth0) + self.assertEqual(contmsg, "") + + cont = IM.RemoveResource(infId, ['1'], auth0) + self.assertEqual(cont, 1) + + IM.DestroyInfrastructure(infId, auth0) + +if __name__ == "__main__": + unittest.main() diff --git a/test/QuickTestIM.py b/test/integration/QuickTestIM.py similarity index 97% rename from test/QuickTestIM.py rename to test/integration/QuickTestIM.py index 671b0fee1..ad28de81e 100755 --- a/test/QuickTestIM.py +++ b/test/integration/QuickTestIM.py @@ -30,13 +30,16 @@ from radl import radl_parse from IM import __version__ as version -TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) -RADL_FILE = TESTS_PATH + '/files/quick-test.radl' -AUTH_FILE = TESTS_PATH + '/auth.dat' HOSTNAME = "localhost" TEST_PORT = 8899 +def read_file_as_string(file_name): + tests_path = os.path.dirname(os.path.abspath(__file__)) + abs_file_path = os.path.join(tests_path, file_name) + return open(abs_file_path, 'r').read() + + class QuickTestIM(unittest.TestCase): server = None @@ -47,7 +50,9 @@ class QuickTestIM(unittest.TestCase): def setUpClass(cls): cls.server = xmlrpclib.ServerProxy( "http://" + HOSTNAME + ":" + str(TEST_PORT), allow_none=True) - cls.auth_data = Authentication.read_auth_data(AUTH_FILE) + tests_path = os.path.dirname(os.path.realpath(__file__)) + auth_file = tests_path + '/../files/auth.dat' + cls.auth_data = Authentication.read_auth_data(auth_file) cls.inf_id = 0 @classmethod @@ -122,11 +127,7 @@ def test_11_create(self): """ Test the CreateInfrastructure IM function """ - f = open(RADL_FILE) - radl = "" - for line in f.readlines(): - radl += line - f.close() + radl = read_file_as_string("../files/quick-test.radl") (success, inf_id) = self.server.CreateInfrastructure(radl, self.auth_data) self.assertTrue( diff --git a/test/TestIM.py b/test/integration/TestIM.py similarity index 98% rename from test/TestIM.py rename to test/integration/TestIM.py index da47315d6..baa69188a 100755 --- a/test/TestIM.py +++ b/test/integration/TestIM.py @@ -33,13 +33,16 @@ RADL_ADD_WIN = "network publica\nnetwork privada\nsystem windows\ndeploy windows 1 one" RADL_ADD = "network publica\nnetwork privada\nsystem wn\ndeploy wn 1 one" RADL_ADD_ERROR = "system wnno deploy wnno 1" -TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) -RADL_FILE = TESTS_PATH + '/files/test.radl' -AUTH_FILE = TESTS_PATH + '/auth.dat' HOSTNAME = "localhost" TEST_PORT = 8899 +def read_file_as_string(file_name): + tests_path = os.path.dirname(os.path.abspath(__file__)) + abs_file_path = os.path.join(tests_path, file_name) + return open(abs_file_path, 'r').read() + + class TestIM(unittest.TestCase): server = None @@ -50,7 +53,9 @@ class TestIM(unittest.TestCase): def setUpClass(cls): cls.server = xmlrpclib.ServerProxy( "http://" + HOSTNAME + ":" + str(TEST_PORT), allow_none=True) - cls.auth_data = Authentication.read_auth_data(AUTH_FILE) + tests_path = os.path.dirname(os.path.realpath(__file__)) + auth_file = tests_path + '/../files/auth.dat' + cls.auth_data = Authentication.read_auth_data(auth_file) @classmethod def tearDownClass(cls): @@ -129,11 +134,7 @@ def test_11_create(self): """ Test the CreateInfrastructure IM function """ - f = open(RADL_FILE) - radl = "" - for line in f.readlines(): - radl += line - f.close() + radl = read_file_as_string("../files/test.radl") (success, inf_id) = self.server.CreateInfrastructure(radl, self.auth_data) self.assertTrue( diff --git a/test/TestREST.py b/test/integration/TestREST.py similarity index 98% rename from test/TestREST.py rename to test/integration/TestREST.py index 56ba21a06..2c3d14fe0 100755 --- a/test/TestREST.py +++ b/test/integration/TestREST.py @@ -34,12 +34,14 @@ PID = None RADL_ADD = "network publica\nsystem front\ndeploy front 1" RADL_ADD_ERROR = "system wnno deploy wnno 1" -TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) -RADL_FILE = TESTS_PATH + '/files/test_simple.radl' -AUTH_FILE = TESTS_PATH + '/auth.dat' - HOSTNAME = "localhost" -TEST_PORT = 8800 +TEST_PORT = 8811 + + +def read_file_as_string(file_name): + tests_path = os.path.dirname(os.path.abspath(__file__)) + abs_file_path = os.path.join(tests_path, file_name) + return open(abs_file_path, 'r').read() class TestIM(unittest.TestCase): @@ -51,11 +53,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - f = open(AUTH_FILE) - cls.auth_data = "" - for line in f.readlines(): - cls.auth_data += line.strip() + "\\n" - f.close() + cls.auth_data = read_file_as_string('../files/auth.dat').replace("\n","\\n") cls.inf_id = "0" @classmethod @@ -142,17 +140,16 @@ def test_10_list(self): msg="ERROR listing user infrastructures:" + output) def test_12_list_with_incorrect_token(self): - f = open(AUTH_FILE) + auth_data_lines = read_file_as_string('../files/auth.dat').split("\n") token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" "LCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDYyODY5MjgxLCJpYXQiOjE" "0NjI4NjU2ODEsImp0aSI6Ijc1M2M4ZTI1LWU3MGMtNGI5MS05YWJhLTcxNDI5NTg3MzUzOSJ9.iA9nv7QdkmfgJPSQ_77_eKrvh" "P1xwZ1Z91xzrZ0Bzue0ark4qRMlHCdZvad1tunURaSsHHMsFYQ3H7oQj-ZSYWOfr1KxMaIo4pWaVHrW8qsCMLmqdNfubR54GmTh" "M4cA2ZdNZa8neVT8jUvzR1YX-5cz7sp2gWbW9LAwejoXDtk") auth_data = "type = InfrastructureManager; token = %s\\n" % token - for line in f.readlines(): + for line in auth_data_lines: if line.find("type = InfrastructureManager") == -1: auth_data += line.strip() + "\\n" - f.close() self.server.request('GET', "/infrastructures", headers={'AUTHORIZATION': auth_data}) @@ -188,8 +185,7 @@ def test_18_get_info_without_auth_data(self): msg="Incorrect error message: " + str(resp.status)) def test_20_create(self): - with open(RADL_FILE) as f: - radl = f.read() + radl = read_file_as_string('../files/test_simple.radl') self.server.request('POST', "/infrastructures", body=radl, headers={'AUTHORIZATION': self.auth_data}) diff --git a/test/TestREST_JSON.py b/test/integration/TestREST_JSON.py similarity index 95% rename from test/TestREST_JSON.py rename to test/integration/TestREST_JSON.py index 73d303288..f204da12c 100755 --- a/test/TestREST_JSON.py +++ b/test/integration/TestREST_JSON.py @@ -33,14 +33,16 @@ PID = None RADL_ADD = """[{"class":"network","reference":true,"id":"publica"}, {"class":"system","reference":true,"id":"front"},{"vm_number":1,"class":"deploy","system":"front"}]""" -TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) -RADL_FILE = TESTS_PATH + '/files/test_simple.json' -AUTH_FILE = TESTS_PATH + '/auth.dat' - HOSTNAME = "localhost" TEST_PORT = 8800 +def read_file_as_string(file_name): + tests_path = os.path.dirname(os.path.abspath(__file__)) + abs_file_path = os.path.join(tests_path, file_name) + return open(abs_file_path, 'r').read() + + class TestIM(unittest.TestCase): server = None @@ -50,11 +52,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - f = open(AUTH_FILE) - cls.auth_data = "" - for line in f.readlines(): - cls.auth_data += line.strip() + "\\n" - f.close() + cls.auth_data = read_file_as_string('../files/auth.dat').replace("\n","\\n") cls.inf_id = "0" @classmethod @@ -117,11 +115,7 @@ def wait_inf_state(self, state, timeout, incorrect_states=[], vm_ids=None): return all_ok def test_20_create(self): - f = open(RADL_FILE) - radl = "" - for line in f.readlines(): - radl += line - f.close() + radl = read_file_as_string('../files/test_simple.json') self.server.request('POST', "/infrastructures", body=radl, headers={ 'AUTHORIZATION': self.auth_data, 'Content-Type': 'application/json'}) diff --git a/test/SSH.py b/test/unit/SSH.py similarity index 92% rename from test/SSH.py rename to test/unit/SSH.py index 6010ac9b2..e9ec4c560 100755 --- a/test/SSH.py +++ b/test/unit/SSH.py @@ -36,13 +36,13 @@ class TestSSH(unittest.TestCase): @patch('paramiko.SSHClient') def test_test_connectivity(self, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) success = ssh.test_connectivity(5) self.assertTrue(success) @patch('paramiko.SSHClient') def test_execute(self, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) client = MagicMock() ssh_client.return_value = client @@ -62,56 +62,56 @@ def test_execute(self, ssh_client): @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient') def test_sftp_get(self, sftp_client, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) ssh.sftp_get("some_file", "some_file") @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient') def test_sftp_get_files(self, sftp_client, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) ssh.sftp_get(["some_file"], ["some_file"]) @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient') def test_sftp_put(self, sftp_client, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) ssh.sftp_put("some_file", "some_file") @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient') def test_sftp_put_files(self, sftp_client, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) ssh.sftp_put_files([("some_file", "some_file")]) @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient') def test_sftp_put_dir(self, sftp_client, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) ssh.sftp_put_dir("/tmp", "/tmp") @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient') def test_sftp_put_content(self, sftp_client, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) ssh.sftp_put_content("some_file", "some_content") @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient') def test_sftp_mkdir(self, sftp_client, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) ssh.sftp_mkdir("/some_dir") @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient.from_transport') def test_sftp_list(self, from_transport, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) client = MagicMock() from_transport.return_value = client @@ -123,7 +123,7 @@ def test_sftp_list(self, from_transport, ssh_client): @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient.from_transport') def test_sftp_list_attr(self, from_transport, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) client = MagicMock() from_transport.return_value = client @@ -135,7 +135,7 @@ def test_sftp_list_attr(self, from_transport, ssh_client): @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient.from_transport') def test_getcwd(self, from_transport, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) client = MagicMock() from_transport.return_value = client @@ -146,7 +146,7 @@ def test_getcwd(self, from_transport, ssh_client): @patch('paramiko.SSHClient') def test_execute_timeout(self, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) client = MagicMock() ssh_client.return_value = client @@ -159,7 +159,7 @@ def test_execute_timeout(self, ssh_client): @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient.from_transport') def test_sftp_remove(self, from_transport, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) client = MagicMock() from_transport.return_value = client @@ -171,7 +171,7 @@ def test_sftp_remove(self, from_transport, ssh_client): @patch('paramiko.SSHClient') @patch('paramiko.SFTPClient.from_transport') def test_sftp_chmod(self, from_transport, ssh_client): - ssh = SSHRetry("host", "user", "passwd", read_file_as_string("files/privatekey.pem")) + ssh = SSHRetry("host", "user", "passwd", read_file_as_string("../files/privatekey.pem")) client = MagicMock() from_transport.return_value = client diff --git a/test/VMRC.py b/test/unit/VMRC.py similarity index 100% rename from test/VMRC.py rename to test/unit/VMRC.py diff --git a/test/connectors/Azure.py b/test/unit/connectors/Azure.py similarity index 100% rename from test/connectors/Azure.py rename to test/unit/connectors/Azure.py diff --git a/test/connectors/Docker.py b/test/unit/connectors/Docker.py similarity index 100% rename from test/connectors/Docker.py rename to test/unit/connectors/Docker.py diff --git a/test/connectors/EC2.py b/test/unit/connectors/EC2.py similarity index 100% rename from test/connectors/EC2.py rename to test/unit/connectors/EC2.py diff --git a/test/connectors/Fogbow.py b/test/unit/connectors/Fogbow.py similarity index 100% rename from test/connectors/Fogbow.py rename to test/unit/connectors/Fogbow.py diff --git a/test/connectors/GCE.py b/test/unit/connectors/GCE.py similarity index 100% rename from test/connectors/GCE.py rename to test/unit/connectors/GCE.py diff --git a/test/connectors/Kubernetes.py b/test/unit/connectors/Kubernetes.py similarity index 100% rename from test/connectors/Kubernetes.py rename to test/unit/connectors/Kubernetes.py diff --git a/test/connectors/LibCloud.py b/test/unit/connectors/LibCloud.py similarity index 100% rename from test/connectors/LibCloud.py rename to test/unit/connectors/LibCloud.py diff --git a/test/connectors/OCCI.py b/test/unit/connectors/OCCI.py similarity index 100% rename from test/connectors/OCCI.py rename to test/unit/connectors/OCCI.py diff --git a/test/connectors/OpenNebula.py b/test/unit/connectors/OpenNebula.py similarity index 100% rename from test/connectors/OpenNebula.py rename to test/unit/connectors/OpenNebula.py diff --git a/test/connectors/OpenStack.py b/test/unit/connectors/OpenStack.py similarity index 100% rename from test/connectors/OpenStack.py rename to test/unit/connectors/OpenStack.py diff --git a/test/connectors/files/focci_instance.txt b/test/unit/connectors/files/focci_instance.txt similarity index 100% rename from test/connectors/files/focci_instance.txt rename to test/unit/connectors/files/focci_instance.txt diff --git a/test/connectors/files/focci_resource.txt b/test/unit/connectors/files/focci_resource.txt similarity index 100% rename from test/connectors/files/focci_resource.txt rename to test/unit/connectors/files/focci_resource.txt diff --git a/test/connectors/files/nets.xml b/test/unit/connectors/files/nets.xml similarity index 100% rename from test/connectors/files/nets.xml rename to test/unit/connectors/files/nets.xml diff --git a/test/connectors/files/occi.txt b/test/unit/connectors/files/occi.txt similarity index 100% rename from test/connectors/files/occi.txt rename to test/unit/connectors/files/occi.txt diff --git a/test/connectors/files/occi_vm_info.txt b/test/unit/connectors/files/occi_vm_info.txt similarity index 100% rename from test/connectors/files/occi_vm_info.txt rename to test/unit/connectors/files/occi_vm_info.txt diff --git a/test/connectors/files/vm_info.xml b/test/unit/connectors/files/vm_info.xml similarity index 100% rename from test/connectors/files/vm_info.xml rename to test/unit/connectors/files/vm_info.xml diff --git a/test/connectors/files/vm_info_off.xml b/test/unit/connectors/files/vm_info_off.xml similarity index 100% rename from test/connectors/files/vm_info_off.xml rename to test/unit/connectors/files/vm_info_off.xml diff --git a/test/test_im_logic.py b/test/unit/test_im_logic.py similarity index 97% rename from test/test_im_logic.py rename to test/unit/test_im_logic.py index 2980a36ae..19b37b7b3 100755 --- a/test/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -158,7 +158,7 @@ def test_inf_addresources_without_credentials(self): def test_inf_auth_with_userdb(self): """Test access im with user db""" - Config.USER_DB = os.path.dirname(os.path.realpath(__file__)) + '/files/users.txt' + Config.USER_DB = os.path.dirname(os.path.realpath(__file__)) + '/../files/users.txt' auth0 = self.getAuth([0]) infId0 = IM.CreateInfrastructure("", auth0) @@ -489,14 +489,14 @@ def test_contextualize(self): auth0 = self.getAuth([0], [], [("Mock", 0)]) IM._reinit() Config.PLAYBOOK_RETRIES = 1 - Config.CONTEXTUALIZATION_DIR = os.path.dirname(os.path.realpath(__file__)) + "/../contextualization" + Config.CONTEXTUALIZATION_DIR = os.path.dirname(os.path.realpath(__file__)) + "/../../contextualization" Config.CONFMAMAGER_CHECK_STATE_INTERVAL = 0.001 cloud0 = self.get_cloud_connector_mock("MyMock") self.register_cloudconnector("Mock", cloud0) infId = IM.CreateInfrastructure(str(radl), auth0) - time.sleep(2) + time.sleep(5) state = IM.GetInfrastructureState(infId, auth0) self.assertEqual(state["state"], "unconfigured") @@ -514,18 +514,14 @@ def test_contextualize(self): def test_tosca_to_radl(self): """Test TOSCA RADL translation""" - TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) - with open(TESTS_PATH + '/files/tosca_long.yml') as f: - tosca_data = f.read() + tosca_data = read_file_as_string('../files/tosca_long.yml') tosca = Tosca(tosca_data) _, radl = tosca.to_radl() parse_radl(str(radl)) def test_tosca_get_outputs(self): """Test TOSCA get_outputs function""" - TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) - with open(TESTS_PATH + '/files/tosca_create.yml') as f: - tosca_data = f.read() + tosca_data = read_file_as_string('../files/tosca_create.yml') tosca = Tosca(tosca_data) _, radl = tosca.to_radl() radl.systems[0].setValue("net_interface.0.ip", "158.42.1.1") @@ -550,9 +546,7 @@ def test_check_iam_token(self, connection): "me5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXU" "z5BzxBvANko")} - TESTS_PATH = os.path.dirname(os.path.realpath(__file__)) - with open(TESTS_PATH + '/files/iam_user_info.json') as f: - user_info = f.read() + user_info = read_file_as_string('../files/iam_user_info.json') conn = MagicMock() connection.return_value = conn @@ -574,7 +568,7 @@ def test_check_iam_token(self, connection): def test_db(self, execute, select, table_exists, connect): table_exists.return_value = True - select.return_value = [["1", "", read_file_as_string("files/data.pkl")]] + select.return_value = [["1", "", read_file_as_string("../files/data.pkl")]] execute.return_value = True res = IM.get_data_from_db("mysql://username:password@server/db_name") diff --git a/test/tts.py b/test/unit/tts.py similarity index 100% rename from test/tts.py rename to test/unit/tts.py From fa8676b4dd33703707981937f1064fa2ff7d1934 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 11:54:11 +0200 Subject: [PATCH 342/509] Test fixes --- test/functional/test_im.py | 7 ++----- test/integration/QuickTestIM.py | 2 +- test/integration/TestIM.py | 2 +- test/integration/TestREST.py | 4 ++-- test/integration/TestREST_JSON.py | 2 +- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/test/functional/test_im.py b/test/functional/test_im.py index 4aec53f63..6278b7bb6 100755 --- a/test/functional/test_im.py +++ b/test/functional/test_im.py @@ -22,7 +22,7 @@ import unittest import sys -from mock import Mock, patch, MagicMock +from mock import Mock sys.path.append("..") sys.path.append(".") @@ -34,13 +34,10 @@ from IM.VirtualMachine import VirtualMachine from IM.InfrastructureManager import InfrastructureManager as IM from IM.auth import Authentication -from radl.radl import RADL, system, deploy, Feature, SoftFeatures -from radl.radl_parse import parse_radl +from radl.radl import RADL, system, deploy, Feature from IM.CloudInfo import CloudInfo from IM.connectors.CloudConnector import CloudConnector from IM.SSH import SSH -from IM.InfrastructureInfo import InfrastructureInfo -from IM.tosca.Tosca import Tosca def read_file_as_string(file_name): diff --git a/test/integration/QuickTestIM.py b/test/integration/QuickTestIM.py index ad28de81e..c782188ab 100755 --- a/test/integration/QuickTestIM.py +++ b/test/integration/QuickTestIM.py @@ -51,7 +51,7 @@ def setUpClass(cls): cls.server = xmlrpclib.ServerProxy( "http://" + HOSTNAME + ":" + str(TEST_PORT), allow_none=True) tests_path = os.path.dirname(os.path.realpath(__file__)) - auth_file = tests_path + '/../files/auth.dat' + auth_file = tests_path + '/../../files/auth.dat' cls.auth_data = Authentication.read_auth_data(auth_file) cls.inf_id = 0 diff --git a/test/integration/TestIM.py b/test/integration/TestIM.py index baa69188a..68d30be13 100755 --- a/test/integration/TestIM.py +++ b/test/integration/TestIM.py @@ -54,7 +54,7 @@ def setUpClass(cls): cls.server = xmlrpclib.ServerProxy( "http://" + HOSTNAME + ":" + str(TEST_PORT), allow_none=True) tests_path = os.path.dirname(os.path.realpath(__file__)) - auth_file = tests_path + '/../files/auth.dat' + auth_file = tests_path + '/../../files/auth.dat' cls.auth_data = Authentication.read_auth_data(auth_file) @classmethod diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 2c3d14fe0..a7e7ba3f5 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -53,7 +53,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - cls.auth_data = read_file_as_string('../files/auth.dat').replace("\n","\\n") + cls.auth_data = read_file_as_string('../../files/auth.dat').replace("\n","\\n") cls.inf_id = "0" @classmethod @@ -140,7 +140,7 @@ def test_10_list(self): msg="ERROR listing user infrastructures:" + output) def test_12_list_with_incorrect_token(self): - auth_data_lines = read_file_as_string('../files/auth.dat').split("\n") + auth_data_lines = read_file_as_string('../../files/auth.dat').split("\n") token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" "LCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDYyODY5MjgxLCJpYXQiOjE" "0NjI4NjU2ODEsImp0aSI6Ijc1M2M4ZTI1LWU3MGMtNGI5MS05YWJhLTcxNDI5NTg3MzUzOSJ9.iA9nv7QdkmfgJPSQ_77_eKrvh" diff --git a/test/integration/TestREST_JSON.py b/test/integration/TestREST_JSON.py index f204da12c..839ca2cf6 100755 --- a/test/integration/TestREST_JSON.py +++ b/test/integration/TestREST_JSON.py @@ -52,7 +52,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - cls.auth_data = read_file_as_string('../files/auth.dat').replace("\n","\\n") + cls.auth_data = read_file_as_string('../../files/auth.dat').replace("\n","\\n") cls.inf_id = "0" @classmethod From b827203a9a082b43b4cbecd7465d9c07cc7e8a1a Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 12:02:34 +0200 Subject: [PATCH 343/509] Test fixes --- test/integration/QuickTestIM.py | 2 +- test/integration/TestIM.py | 2 +- test/integration/TestREST.py | 4 ++-- test/integration/TestREST_JSON.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration/QuickTestIM.py b/test/integration/QuickTestIM.py index c782188ab..ecca50756 100755 --- a/test/integration/QuickTestIM.py +++ b/test/integration/QuickTestIM.py @@ -51,7 +51,7 @@ def setUpClass(cls): cls.server = xmlrpclib.ServerProxy( "http://" + HOSTNAME + ":" + str(TEST_PORT), allow_none=True) tests_path = os.path.dirname(os.path.realpath(__file__)) - auth_file = tests_path + '/../../files/auth.dat' + auth_file = tests_path + '/../auth.dat' cls.auth_data = Authentication.read_auth_data(auth_file) cls.inf_id = 0 diff --git a/test/integration/TestIM.py b/test/integration/TestIM.py index 68d30be13..15481b119 100755 --- a/test/integration/TestIM.py +++ b/test/integration/TestIM.py @@ -54,7 +54,7 @@ def setUpClass(cls): cls.server = xmlrpclib.ServerProxy( "http://" + HOSTNAME + ":" + str(TEST_PORT), allow_none=True) tests_path = os.path.dirname(os.path.realpath(__file__)) - auth_file = tests_path + '/../../files/auth.dat' + auth_file = tests_path + '/../auth.dat' cls.auth_data = Authentication.read_auth_data(auth_file) @classmethod diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index a7e7ba3f5..52b7d6d0c 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -53,7 +53,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - cls.auth_data = read_file_as_string('../../files/auth.dat').replace("\n","\\n") + cls.auth_data = read_file_as_string('../auth.dat').replace("\n","\\n") cls.inf_id = "0" @classmethod @@ -140,7 +140,7 @@ def test_10_list(self): msg="ERROR listing user infrastructures:" + output) def test_12_list_with_incorrect_token(self): - auth_data_lines = read_file_as_string('../../files/auth.dat').split("\n") + auth_data_lines = read_file_as_string('../auth.dat').split("\n") token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" "LCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDYyODY5MjgxLCJpYXQiOjE" "0NjI4NjU2ODEsImp0aSI6Ijc1M2M4ZTI1LWU3MGMtNGI5MS05YWJhLTcxNDI5NTg3MzUzOSJ9.iA9nv7QdkmfgJPSQ_77_eKrvh" diff --git a/test/integration/TestREST_JSON.py b/test/integration/TestREST_JSON.py index 839ca2cf6..b23306fe4 100755 --- a/test/integration/TestREST_JSON.py +++ b/test/integration/TestREST_JSON.py @@ -52,7 +52,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - cls.auth_data = read_file_as_string('../../files/auth.dat').replace("\n","\\n") + cls.auth_data = read_file_as_string('../auth.dat').replace("\n","\\n") cls.inf_id = "0" @classmethod From fabe49e5130b891e703d44f0e6dd53fdaa8c6fa7 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 12:34:17 +0200 Subject: [PATCH 344/509] Style changes --- test/functional/test_im.py | 13 ++++++------- test/integration/TestREST.py | 2 +- test/integration/TestREST_JSON.py | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/test/functional/test_im.py b/test/functional/test_im.py index 6278b7bb6..ea9efc77f 100755 --- a/test/functional/test_im.py +++ b/test/functional/test_im.py @@ -110,7 +110,6 @@ def get_cloud_connector_mock(self, name="MyMock0"): cloud.launch = Mock(side_effect=self.gen_launch_res) return cloud - def test_inf_lifecycle(self): """Test Infrastructure lifecycle""" radl = """" @@ -161,22 +160,22 @@ def test_inf_lifecycle(self): state = IM.GetInfrastructureState(infId, auth0) self.assertEqual(state["state"], "running") - + add_radl = RADL() add_radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), - Feature("disk.0.os.credentials.username", "=", "user"), - Feature("disk.0.os.credentials.password", "=", "pass")])) + Feature("disk.0.os.credentials.username", "=", "user"), + Feature("disk.0.os.credentials.password", "=", "pass")])) add_radl.add(deploy("s0", 1)) - + vms = IM.AddResource(infId, str(add_radl), auth0) self.assertEqual(vms, [1]) - + state = IM.GetVMProperty(infId, "1", "state", auth0) self.assertEqual(state, "running") contmsg = IM.GetVMContMsg(infId, "1", auth0) self.assertEqual(contmsg, "") - + cont = IM.RemoveResource(infId, ['1'], auth0) self.assertEqual(cont, 1) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 52b7d6d0c..e91e0fd2f 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -53,7 +53,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - cls.auth_data = read_file_as_string('../auth.dat').replace("\n","\\n") + cls.auth_data = read_file_as_string('../auth.dat').replace("\n", "\\n") cls.inf_id = "0" @classmethod diff --git a/test/integration/TestREST_JSON.py b/test/integration/TestREST_JSON.py index b23306fe4..d7d97d6de 100755 --- a/test/integration/TestREST_JSON.py +++ b/test/integration/TestREST_JSON.py @@ -52,7 +52,7 @@ class TestIM(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - cls.auth_data = read_file_as_string('../auth.dat').replace("\n","\\n") + cls.auth_data = read_file_as_string('../auth.dat').replace("\n", "\\n") cls.inf_id = "0" @classmethod From 1e1a6c933981a4ddb4fe590b8067d2bd9cb1d84c Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 13:04:26 +0200 Subject: [PATCH 345/509] Test fixes --- test/integration/TestREST.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index e91e0fd2f..83fa39e7f 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -35,7 +35,7 @@ RADL_ADD = "network publica\nsystem front\ndeploy front 1" RADL_ADD_ERROR = "system wnno deploy wnno 1" HOSTNAME = "localhost" -TEST_PORT = 8811 +TEST_PORT = 8800 def read_file_as_string(file_name): @@ -475,8 +475,7 @@ def test_93_create_tosca(self): """ Test the CreateInfrastructure IM function with a TOSCA document """ - with open(TESTS_PATH + '/files/tosca_create.yml') as f: - tosca = f.read() + tosca = read_file_as_string('../files/tosca_create.radl') self.server.request('POST', "/infrastructures", body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) @@ -507,8 +506,7 @@ def test_95_add_tosca(self): """ Test the AddResource IM function with a TOSCA document """ - with open(TESTS_PATH + '/files/tosca_add.yml') as f: - tosca = f.read() + tosca = read_file_as_string('../files/tosca_add.radl') self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) @@ -534,8 +532,7 @@ def test_96_remove_tosca(self): """ Test the RemoveResource IM function with a TOSCA document """ - with open(TESTS_PATH + '/files/tosca_remove.yml') as f: - tosca = f.read() + tosca = read_file_as_string('../files/tosca_remove.radl') self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) From 2887de67b942b0d0f1a3075ac2ffa0dcea3003f6 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Jun 2016 13:47:29 +0200 Subject: [PATCH 346/509] Add IM.tts module to setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e5c92ef63..4586295ae 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ author_email='micafer1@upv.es', url='http://www.grycap.upv.es/im', include_package_data=True, - packages=['IM', 'IM.ansible', 'IM.connectors', 'IM.tosca', 'IM.openid'], + packages=['IM', 'IM.ansible', 'IM.connectors', 'IM.tosca', 'IM.openid', 'IM.tts'], scripts=["im_service.py"], data_files=datafiles, license="GPL version 3, http://www.gnu.org/licenses/gpl-3.0.txt", From ed9a0ed524694c812071c2dbdd079772e406d70a Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 20 Jun 2016 08:43:41 +0200 Subject: [PATCH 347/509] Bugfix with yml files extension --- test/integration/TestREST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 83fa39e7f..3182a92b2 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -475,7 +475,7 @@ def test_93_create_tosca(self): """ Test the CreateInfrastructure IM function with a TOSCA document """ - tosca = read_file_as_string('../files/tosca_create.radl') + tosca = read_file_as_string('../files/tosca_create.yml') self.server.request('POST', "/infrastructures", body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) @@ -506,7 +506,7 @@ def test_95_add_tosca(self): """ Test the AddResource IM function with a TOSCA document """ - tosca = read_file_as_string('../files/tosca_add.radl') + tosca = read_file_as_string('../files/tosca_add.yml') self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) @@ -532,7 +532,7 @@ def test_96_remove_tosca(self): """ Test the RemoveResource IM function with a TOSCA document """ - tosca = read_file_as_string('../files/tosca_remove.radl') + tosca = read_file_as_string('../files/tosca_remove.yml') self.server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) From 7093ec033363ad3fb6a9d00a630fcb2f4719e92b Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 20 Jun 2016 09:01:50 +0200 Subject: [PATCH 348/509] Minor change --- test/unit/test_im_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 19b37b7b3..5098d55c0 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -197,7 +197,7 @@ def test_inf_addresources0(self): def test_inf_addresources1(self): """Deploy n independent virtual machines.""" - n = 40 # Machines to deploy + n = 20 # Machines to deploy Config.MAX_SIMULTANEOUS_LAUNCHES = n / 2 # Test the pool radl = RADL() radl.add(system("s0", [Feature("disk.0.image.url", "=", "mock0://linux.for.ev.er"), From 57b307a4fc38d3c2ae4318f5e1e40c3acd06e009 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 23 Jun 2016 10:34:59 +0200 Subject: [PATCH 349/509] Use INDIGO repos to install Ansible --- contextualization/conf-ansible.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index d6d99a205..145768b56 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -6,30 +6,41 @@ - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" - - - name: Apt-get update - apt: update_cache=yes - when: ansible_os_family == "Debian" - name: EPEL - yum: name=epel-release + yum: name=epel-release,yum-priorities when: ansible_os_family == "RedHat" and ansible_distribution != "Fedora" ####################### Install Ansible in Ubuntu and RHEL systems with apt and yum ################################### ################### because they have recent versions of ansible in system repositories ############################### + - name: Ubuntu install indigo list + get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list dest=/etc/apt/sources.list.d/indigo1-ubuntu14_04.list + when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" + - name: Ubuntu install requirements apt: name=software-properties-common when: ansible_distribution == "Ubuntu" - + - name: Ubuntu install Ansible PPA repo apt_repository: repo='ppa:ansible/ansible' - when: ansible_distribution == "Ubuntu" + when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") + + - name: Apt-get update + apt: update_cache=yes + when: ansible_os_family == "Debian" - name: Ubuntu install Ansible with apt apt: name=ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip when: ansible_distribution == "Ubuntu" + - name: RH indigo repos + get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/yum.repos.d/{{item}} + with_items: + - indigo1-testing-updates.repo + - indigo1-testing-base.repo + when: ansible_os_family == "RedHat" and ansible_distribution_major_version >= 7 and ansible_distribution != "Fedora" + - name: Yum install Ansible RH yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,wget when: ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6 and ansible_distribution != "Fedora" From 5dc15436dc21c4ed0a5412168633b0578331396d Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 24 Jun 2016 10:53:56 +0200 Subject: [PATCH 350/509] Improvements for VMs that reqs a priv IP but gets a public one --- IM/ConfManager.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index e2ecc17b0..8ca819086 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -443,6 +443,9 @@ def generate_inventory(self, tmp_dir): if vm.getPublicIP(): node_line += ' IM_NODE_PUBLIC_IP=' + vm.getPublicIP() + if not vm.getPrivateIP(): + # If the node only has a public IP set this variable to the public one + node_line += ' IM_NODE_PRIVATE_IP=' + vm.getPublicIP() if vm.getPrivateIP(): node_line += ' IM_NODE_PRIVATE_IP=' + vm.getPrivateIP() node_line += ' IM_NODE_HOSTNAME=' + nodename @@ -513,14 +516,20 @@ def generate_etc_hosts(self, tmp_dir): for i in range(vm.getNumNetworkIfaces()): if vm.getRequestedNameIface(i): + (nodename, nodedom) = vm.getRequestedNameIface(i, default_domain=Config.DEFAULT_DOMAIN) if vm.getIfaceIP(i): - (nodename, nodedom) = vm.getRequestedNameIface( - i, default_domain=Config.DEFAULT_DOMAIN) hosts_out.write(vm.getIfaceIP( i) + " " + nodename + "." + nodedom + " " + nodename + "\r\n") else: - ConfManager.logger.warn("Inf ID: " + str(self.inf.id) + ": Net interface " + str( - i) + " request a name, but it does not have an IP.") + ConfManager.logger.warn("Inf ID: %s: Net interface %d" + " request a name, but it does not have an IP." % (self.inf.id, i)) + + for j in range(vm.getNumNetworkIfaces()): + if vm.getIfaceIP(j): + ConfManager.logger.warn("Setting the IP of the iface %d." % j) + hosts_out.write(vm.getIfaceIP( + i) + " " + nodename + "." + nodedom + " " + nodename + "\r\n") + break # the master node # TODO: Known issue: the master VM must set the public From 4f2aa2c5da2b2d695cbc189042796b5cc7151866 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 24 Jun 2016 11:52:22 +0200 Subject: [PATCH 351/509] Do not use INDIGO repos to install Ansible as they are incorrect --- contextualization/conf-ansible.yml | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 145768b56..d6d99a205 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -6,41 +6,30 @@ - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" + + - name: Apt-get update + apt: update_cache=yes + when: ansible_os_family == "Debian" - name: EPEL - yum: name=epel-release,yum-priorities + yum: name=epel-release when: ansible_os_family == "RedHat" and ansible_distribution != "Fedora" ####################### Install Ansible in Ubuntu and RHEL systems with apt and yum ################################### ################### because they have recent versions of ansible in system repositories ############################### - - name: Ubuntu install indigo list - get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list dest=/etc/apt/sources.list.d/indigo1-ubuntu14_04.list - when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" - - name: Ubuntu install requirements apt: name=software-properties-common when: ansible_distribution == "Ubuntu" - + - name: Ubuntu install Ansible PPA repo apt_repository: repo='ppa:ansible/ansible' - when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") - - - name: Apt-get update - apt: update_cache=yes - when: ansible_os_family == "Debian" + when: ansible_distribution == "Ubuntu" - name: Ubuntu install Ansible with apt apt: name=ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip when: ansible_distribution == "Ubuntu" - - name: RH indigo repos - get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/yum.repos.d/{{item}} - with_items: - - indigo1-testing-updates.repo - - indigo1-testing-base.repo - when: ansible_os_family == "RedHat" and ansible_distribution_major_version >= 7 and ansible_distribution != "Fedora" - - name: Yum install Ansible RH yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,wget when: ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6 and ansible_distribution != "Fedora" From 982eecf3c10551b690992440db4a910fe0d6644e Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 24 Jun 2016 11:55:30 +0200 Subject: [PATCH 352/509] Style changes --- IM/ConfManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 8ca819086..20b1bbab6 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -521,9 +521,9 @@ def generate_etc_hosts(self, tmp_dir): hosts_out.write(vm.getIfaceIP( i) + " " + nodename + "." + nodedom + " " + nodename + "\r\n") else: - ConfManager.logger.warn("Inf ID: %s: Net interface %d" - " request a name, but it does not have an IP." % (self.inf.id, i)) - + ConfManager.logger.warn("Inf ID: %s: Net interface %d request a name, " + "but it does not have an IP." % (self.inf.id, i)) + for j in range(vm.getNumNetworkIfaces()): if vm.getIfaceIP(j): ConfManager.logger.warn("Setting the IP of the iface %d." % j) From 74c155701131abb6d6bca0584dbaf6a31e507ce1 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 24 Jun 2016 13:21:24 +0200 Subject: [PATCH 353/509] Bugfix --- IM/ConfManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 20b1bbab6..aafb60da1 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -528,7 +528,7 @@ def generate_etc_hosts(self, tmp_dir): if vm.getIfaceIP(j): ConfManager.logger.warn("Setting the IP of the iface %d." % j) hosts_out.write(vm.getIfaceIP( - i) + " " + nodename + "." + nodedom + " " + nodename + "\r\n") + j) + " " + nodename + "." + nodedom + " " + nodename + "\r\n") break # the master node From 7f38d8412f0516c725a9c6e1dc1ecb588f57b6a2 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 27 Jun 2016 08:55:39 +0200 Subject: [PATCH 354/509] Raise error in case of site not in TTS --- IM/connectors/OpenNebula.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index fb40594df..018bedd0d 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -205,6 +205,8 @@ def get_auth_from_tts(self, token): ttsc = TTSClient(token, decoded_token['iss'], host, port, scheme) svc = ttsc.find_service("opennebula", self.cloud.server) + if not svc: + raise Exception("Cloud site %s nto found in TTS." % self.cloud.server) succes, cred = ttsc.request_credential(svc["id"]) if succes: username = password = None From b36765d9c39878d3ca26627cc3bc174058b3eb6b Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 27 Jun 2016 12:10:33 +0200 Subject: [PATCH 355/509] Improve error messages in TTS ops --- IM/connectors/OpenNebula.py | 8 ++++---- IM/tts/tts.py | 18 ++++++++++++++---- test/unit/tts.py | 5 +++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 018bedd0d..6de805e12 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -204,9 +204,9 @@ def get_auth_from_tts(self, token): decoded_token = JWT.get_info(token) ttsc = TTSClient(token, decoded_token['iss'], host, port, scheme) - svc = ttsc.find_service("opennebula", self.cloud.server) - if not svc: - raise Exception("Cloud site %s nto found in TTS." % self.cloud.server) + success, svc = ttsc.find_service("opennebula", self.cloud.server) + if not success: + raise Exception("Error getting credentials from TTS: %s" % svc) succes, cred = ttsc.request_credential(svc["id"]) if succes: username = password = None @@ -217,7 +217,7 @@ def get_auth_from_tts(self, token): password = elem['value'] return username, password else: - return None, None + raise Exception("Error getting credentials from TTS: %s" % cred) def getSessionID(self, auth_data, hash_password=None): """ diff --git a/IM/tts/tts.py b/IM/tts/tts.py index 0c25eae12..adcb774a6 100644 --- a/IM/tts/tts.py +++ b/IM/tts/tts.py @@ -87,7 +87,11 @@ def request_credential(self, sid): """ body = '{"service_id":"%s"}' % sid url = "/api/credential/" - success, res = self._perform_post(url, body) + try: + success, res = self._perform_post(url, body) + except Exception, ex: + success = False + res = str(ex) if success: return True, json.loads(res) else: @@ -98,7 +102,11 @@ def list_endservices(self): Get the list of services """ url = "/api/service" - success, output = self._perform_get(url) + try: + success, output = self._perform_get(url) + except Exception, ex: + success = False + output = str(ex) if not success: return False, output else: @@ -112,6 +120,8 @@ def find_service(self, stype, host): if success: for service in services["service_list"]: if service["type"] == stype and service["host"] == host: - return service + return True, service + else: + return False, services - return None + return False, "Cloud site %s not found in TTS" % host diff --git a/test/unit/tts.py b/test/unit/tts.py index 40053b84f..bf32e0b4f 100755 --- a/test/unit/tts.py +++ b/test/unit/tts.py @@ -87,11 +87,12 @@ def test_find_service(self, connection): conn.putrequest.side_effect = self.request conn.getresponse.side_effect = self.get_response - service = self.ttsc.find_service("stype", "shost") + success, service = self.ttsc.find_service("stype", "shost") expected_service = {"id": "sid", "type": "stype", "host": "shost"} - self.assertEqual(service, expected_service, msg="ERROR: finding service: Unexpected service.") + self.assertTrue(success) + self.assertEqual(service, expected_service) @patch('httplib.HTTPConnection') def test_request_credential(self, connection): From 61719b7d3608f46ab358c1eab40ff6a7603f2719 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Jun 2016 10:38:07 +0200 Subject: [PATCH 356/509] Add .gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4c8e2f6d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +.vscode/ +parsetab.py +test/auth.dat +.coverage +dist +cover From 712f084fcd190b54d69c544ee9b316071135972e Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Jun 2016 10:38:40 +0200 Subject: [PATCH 357/509] Bugfix in vars with lists or dicts --- IM/tosca/Tosca.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index f5b6ce624..4b1992357 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -430,11 +430,12 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: for var_name, var_value in env.iteritems(): - if var_value.startswith("|"): - variables += ' %s: %s ' % ( + # use " with ansible vars to avoid errors + if var_value.startswith("{{"): + variables += ' %s: "%s" ' % ( var_name, var_value) + "\n" else: - variables += ' %s: "%s" ' % ( + variables += ' %s: %s ' % ( var_name, var_value) + "\n" variables += "\n" From d48651c6d62832e363b510cc39d1412e5ffac6f7 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Jun 2016 11:55:07 +0200 Subject: [PATCH 358/509] Bugfix in vars with lists or dicts --- IM/tosca/Tosca.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 4b1992357..900be8d67 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -379,7 +379,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): param_value, node) if val: - env[param_name] = str(val) + env[param_name] = val else: raise Exception("input value for %s in interface %s of node %s not valid" % ( param_name, name, node.name)) @@ -430,13 +430,12 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: for var_name, var_value in env.iteritems(): - # use " with ansible vars to avoid errors - if var_value.startswith("{{"): + if isinstance(var_value, str): variables += ' %s: "%s" ' % ( var_name, var_value) + "\n" else: variables += ' %s: %s ' % ( - var_name, var_value) + "\n" + var_name, str(var_value)) + "\n" variables += "\n" script_content = self._remove_recipe_header(script_content) From d9da99422b32edb84b04c7aa9fff7ac206e4bafd Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Jun 2016 12:05:57 +0200 Subject: [PATCH 359/509] Raise exception en case of YAML parser error --- IM/tosca/Tosca.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 900be8d67..bb61dd036 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -1269,18 +1269,16 @@ def _merge_yaml(yaml1, yaml2): yamlo1o = yaml.load(yaml1)[0] if not isinstance(yamlo1o, dict): yamlo1o = {} - except Exception: - Tosca.logger.exception( - "Error parsing YAML: " + yaml1 + "\n Ignore it") + except Exception, ex: + raise Exception("Error parsing YAML: " + yaml1 + "\n. Error: %s" % str(ex)) yamlo2s = {} try: yamlo2s = yaml.load(yaml2) if not isinstance(yamlo2s, list) or any([not isinstance(d, dict) for d in yamlo2s]): yamlo2s = {} - except Exception: - Tosca.logger.exception( - "Error parsing YAML: " + yaml2 + "\n Ignore it") + except Exception, ex: + raise Exception("Error parsing YAML: " + yaml2 + "\n. Error: %s" % str(ex)) if not yamlo2s and not yamlo1o: return "" From c2c3834bc1e6241f2339306688ed6d05afcce770 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Jun 2016 12:15:53 +0200 Subject: [PATCH 360/509] Bugfix in vars with lists or dicts --- IM/tosca/Tosca.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bb61dd036..e89fc96ff 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -430,12 +430,11 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: for var_name, var_value in env.iteritems(): - if isinstance(var_value, str): - variables += ' %s: "%s" ' % ( - var_name, var_value) + "\n" + if isinstance(var_value, str) and not var_value.startswith("|"): + var_value = '"%s"' % var_value else: - variables += ' %s: %s ' % ( - var_name, str(var_value)) + "\n" + var_value = str(var_value) + variables += ' %s: %s ' % (var_name, var_value) + "\n" variables += "\n" script_content = self._remove_recipe_header(script_content) From b61978cbc1a33ef6a1d101221c9d2bc84b25a4df Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Jun 2016 13:08:28 +0200 Subject: [PATCH 361/509] Remove port 5099 in sgs --- IM/connectors/EC2.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/IM/connectors/EC2.py b/IM/connectors/EC2.py index 87525b35d..6855dadfe 100644 --- a/IM/connectors/EC2.py +++ b/IM/connectors/EC2.py @@ -356,7 +356,7 @@ def create_security_group(self, conn, inf, radl, vpc=None): outports = public_net.getOutPorts() if outports: for remote_port, remote_protocol, local_port, local_protocol in outports: - if local_port != 22 and local_port != 5099: + if local_port != 22: protocol = remote_protocol if remote_protocol != local_protocol: self.logger.warn( @@ -367,7 +367,6 @@ def create_security_group(self, conn, inf, radl, vpc=None): try: sg.authorize('tcp', 22, 22, '0.0.0.0/0') - sg.authorize('tcp', 5099, 5099, '0.0.0.0/0') # open all the ports for the VMs in the security group sg.authorize('tcp', 0, 65535, src_group=sg) From a890051e68f2f06ec854132f5e1defe201670ce8 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 30 Jun 2016 15:19:50 +0200 Subject: [PATCH 362/509] Add scripts to generate rpm and deb packages --- packages/generate_deb.sh | 7 +++++++ packages/generate_rpm.sh | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100755 packages/generate_deb.sh create mode 100755 packages/generate_rpm.sh diff --git a/packages/generate_deb.sh b/packages/generate_deb.sh new file mode 100755 index 000000000..7b6bc9098 --- /dev/null +++ b/packages/generate_deb.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +apt update +apt install -y dh-make python-stdeb +python setup.py --command-packages=stdeb.command sdist_dsc --depends "python-tosca-parser, python-radl, ansible, python-paramiko, python-yaml, python-soappy, python-boto, python-libcloud, python-bottle, python-netaddr, python-scp" bdist_deb +mkdir dist_pkg +cp deb_dist/*.deb dist_pkg \ No newline at end of file diff --git a/packages/generate_rpm.sh b/packages/generate_rpm.sh new file mode 100755 index 000000000..a605172b4 --- /dev/null +++ b/packages/generate_rpm.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +yum -y install rpm-build python-setuptools +echo "%_unpackaged_files_terminate_build 0" > ~/.rpmmacros +python setup.py bdist_rpm --requires="tosca-parser, RADL, ansible, python-paramiko, PyYAML, SOAPpy, python-boto >= 2.29, python-libcloud, python-bottle, python-netaddr, python-scp" +mkdir dist_pkg +cp dist/*.noarch.rpm dist_pkg \ No newline at end of file From 8e281dbee82453d9bd7907ef8bedbe1c274c600d Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 30 Jun 2016 15:23:59 +0200 Subject: [PATCH 363/509] Remove unnecesary packages --- packages/generate_deb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/generate_deb.sh b/packages/generate_deb.sh index 7b6bc9098..cc369ad5e 100755 --- a/packages/generate_deb.sh +++ b/packages/generate_deb.sh @@ -1,7 +1,7 @@ #!/bin/bash apt update -apt install -y dh-make python-stdeb +apt install -y python-stdeb python setup.py --command-packages=stdeb.command sdist_dsc --depends "python-tosca-parser, python-radl, ansible, python-paramiko, python-yaml, python-soappy, python-boto, python-libcloud, python-bottle, python-netaddr, python-scp" bdist_deb mkdir dist_pkg cp deb_dist/*.deb dist_pkg \ No newline at end of file From 761cce74fca26e84f3353002aa1864217ce4f5c0 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 1 Jul 2016 12:03:12 +0200 Subject: [PATCH 364/509] Set release as parameter --- packages/generate_rpm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/generate_rpm.sh b/packages/generate_rpm.sh index a605172b4..679056a12 100755 --- a/packages/generate_rpm.sh +++ b/packages/generate_rpm.sh @@ -2,6 +2,6 @@ yum -y install rpm-build python-setuptools echo "%_unpackaged_files_terminate_build 0" > ~/.rpmmacros -python setup.py bdist_rpm --requires="tosca-parser, RADL, ansible, python-paramiko, PyYAML, SOAPpy, python-boto >= 2.29, python-libcloud, python-bottle, python-netaddr, python-scp" +python setup.py bdist_rpm --release="$1" --requires="tosca-parser, RADL, ansible, python-paramiko, PyYAML, SOAPpy, python-boto >= 2.29, python-libcloud, python-bottle, python-netaddr, python-scp" mkdir dist_pkg cp dist/*.noarch.rpm dist_pkg \ No newline at end of file From 6cc09298296c253f0064d24524796b8a09e3562d Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 1 Jul 2016 12:04:34 +0200 Subject: [PATCH 365/509] Improve conf-ansible recipe --- contextualization/conf-ansible.yml | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index d6d99a205..c18c6542d 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -32,21 +32,21 @@ - name: Yum install Ansible RH yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,wget - when: ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6 and ansible_distribution != "Fedora" + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" ############################################ In other systems use pip ################################################# - name: Apt install requirements apt: name=unzip,gcc,python-dev,openssh-client,sshpass,python-pip,libffi-dev,libssl-dev when: ansible_os_family == "Debian" and ansible_distribution != "Ubuntu" - - - name: Yum install requirements Fedora - yum: name=python-distribute,gcc,python-devel,wget,openssh-clients,sshpass,python-pip - when: ansible_distribution == "Fedora" + + - name: Yum install requirements RH or Fedora + yum: name=python-distribute,gcc,python-devel,wget,openssh-clients,sshpass,python-pip,libffi-devel,openssl-devel + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6 - name: Yum install requirements RH5 yum: name=python26,python26-simplejson,python26-distribute,gcc,python26-devel,openssh-clients,sshpass,libffi-devel,openssl-devel - when: ansible_os_family == "RedHat" and ansible_distribution_major_version < 6 + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6 - name: Zypper install requirements Suse zypper: name=python,python-pip,gcc,python-devel,wget,libffi-devel,openssl-devel state=present @@ -54,36 +54,36 @@ - name: Install Pip 2.6 easy_install: name=pip executable=easy_install-2.6 - when: ansible_os_family == "RedHat" and ansible_distribution_major_version < 6 + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6 # - name: Install Pip (alternative) # shell: wget https://bootstrap.pypa.io/get-pip.py && python get-pip.py - name: Link python file: src=/usr/bin/python dest=/usr/bin/python_ansible state=link - when: ansible_os_family == "Suse" or ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6) + when: ansible_os_family == "Suse" or ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6) - name: Link python 2.6 file: src=/usr/bin/python2.6 dest=/usr/bin/python_ansible state=link - when: ansible_os_family == "RedHat" and ansible_distribution_major_version < 6 + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6 - name: Install ansible with Pip pip: name=ansible extra_args="-I" - when: ansible_os_family == "Suse" or (ansible_os_family == "Debian" and ansible_distribution != "Ubuntu") or ansible_distribution == "Fedora" + when: ansible_os_family == "Suse" or (ansible_os_family == "Debian" and ansible_distribution != "Ubuntu") or ansible_distribution == "Fedora" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 7) - name: Install ansible with Pip 2.6 pip: name=ansible executable=pip-2.6 - when: ansible_os_family == "RedHat" and ansible_distribution_major_version < 6 + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6 #################################### Now install and scp and pywinrm with pip ######################################## - name: Install scp and pywinrm with Pip pip: name="scp pywinrm" - when: ansible_os_family != "RedHat" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6) + when: ansible_os_family != "RedHat" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6) - name: Install scp and pywinrm with Pip 2.6 pip: name="scp pywinrm" executable=pip-2.6 - when: ansible_os_family == "RedHat" and ansible_distribution_major_version < 6 + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6 - name: Disable SELinux selinux: state=disabled @@ -100,19 +100,19 @@ - name: Set transport to ssh in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=transport value=ssh - when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version >= 10) + when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int >= 10) - name: Set transport to smart in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=transport value=smart - when: (ansible_os_family == "RedHat" and ansible_distribution_major_version < 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version < 10) + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int < 10) - name: Change ssh_args to set ControlPersist to 15 min in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=ssh_args value="-o ControlMaster=auto -o ControlPersist=900s" - when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version >= 12) + when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int >= 12) - name: Change ssh_args to remove ControlPersist in REL 6 and older in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=ssh_args value="" - when: (ansible_os_family == "RedHat" and ansible_distribution_major_version < 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version < 12) + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int < 12) - name: Activate SSH pipelining in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=pipelining value=True From 5558315be4b6b2feaf05e50a69036b9842c38aa7 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 4 Jul 2016 12:14:55 +0200 Subject: [PATCH 366/509] Update README --- README | 44 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/README b/README index d5fa35a15..93c4d7ac4 100644 --- a/README +++ b/README @@ -92,6 +92,50 @@ framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- +1.3.1 From RPM packages (RH6 and RH7) +------------------------------------- + +Download the RPM package from GitHub (https://github.com/grycap/im/releases/latest). +Also remember to download the RPM of the RADL package also from GitHub (https://github.com/grycap/radl/releases/latest) +and the tosca-parser RPM file from GitHub (https://github.com/indigo-dc/tosca-parser/releases/latest). + +You must have the epel repository enabled:: + + $ yum install epel-release + +Then install the downloaded RPMs:: + + $ yum localinstall IM-*.rpm RADL-*.rpm tosca-parser-*.rpm + +1.3.2 From Deb package (Tested with Ubuntu 14.04 and 16.04) +----------------------------------------------------------- + +Download the Deb package from GitHub (https://github.com/grycap/im/releases/latest). +Also remember to download the Deb of the RADL package also from GitHub (https://github.com/grycap/radl/releases/latest) +and the tosca-parser Deb file from GitHub (https://github.com/indigo-dc/tosca-parser/releases/latest). + +In Ubuntu 14.04 there are some requisites not available for the "trusty" version or are too old, so you have to manually install them manually. +You can download it from their corresponding PPAs. But here you have some links: + + * python-backports.ssl-match-hostname: https://launchpad.net/ubuntu/+source/backports.ssl-match-hostname/3.4.0.2-1/+build/6206773/+files/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb + * python-scp: http://launchpadlibrarian.net/210648810/python-scp_0.10.2-1_all.deb + * python-libcloud: https://launchpad.net/ubuntu/+source/libcloud/0.20.0-1/+build/8869143/+files/python-libcloud_0.20.0-1_all.deb + +It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see Ansible installation - http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu): + + $ sudo apt-get install software-properties-common + $ sudo apt-add-repository ppa:ansible/ansible + $ sudo apt-get update + +Put all the .deb files in the same directory and do: + + $ sudo dpkg -i *.deb + $ sudo apt install -f -y + + +1.3.3 From Source +----------------- + First install the requirements: On Debian Systems: diff --git a/README.md b/README.md index f11b3b11f..d2a413e75 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- +### 1.3.1 FROM PIP + First install the requirements: On Debian Systems: @@ -123,6 +125,48 @@ Finally install the IM service: $ pip install git+http://github.com/indigo-dc/im ``` +### 1.3.2 FROM RPM + +Download the RPM package from [GitHub](https://github.com/indigo-dc/im/releases/latest). +Also remember to download the RPM of the RADL package also from [GitHub](https://github.com/grycap/radl/releases/latest) and the tosca-parser RPM file from [GitHub](https://github.com/indigo-dc/tosca-parser/releases/latest). +You must have the epel repository enabled: + +``` +$ yum install epel-release +``` + +Then install the downloaded RPMs: + +``` +$ yum localinstall IM-*.rpm RADL-*.rpm tosca-parser-*.rpm +``` + +### 1.3.3 FROM DEB + +Download the Deb package from [GitHub](https://github.com/indigo-dc/im/releases/latest) +Also remember to download the Deb of the RADL package also from [GitHub](https://github.com/grycap/radl/releases/latest) and the tosca-parser Deb file from [GitHub](https://github.com/indigo-dc/tosca-parser/releases/latest). + +In Ubuntu 14.04 there are some requisites not available for the "trusty" version or are too old, so you have to manually install them manually. +You can download it from their corresponding PPAs. But here you have some links: + + * python-backports.ssl-match-hostname: [download](https://launchpad.net/ubuntu/+source/backports.ssl-match-hostname/3.4.0.2-1/+build/6206773/+files/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb) + * python-scp: [download](http://launchpadlibrarian.net/210648810/python-scp_0.10.2-1_all.deb>) + * python-libcloud: [download](https://launchpad.net/ubuntu/+source/libcloud/0.20.0-1/+build/8869143/+files/python-libcloud_0.20.0-1_all.deb) + +It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see [Ansible installation](http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu)): + +``` +$ sudo apt-get install software-properties-common +$ sudo apt-add-repository ppa:ansible/ansible +$ sudo apt-get update +``` + +Put all the .deb files in the same directory and do: + +``` +$ sudo dpkg -i *.deb +$ sudo apt install -f -y +``` 1.4 CONFIGURATION ----------------- From 1bb200694b1646d72fe19854498696424737fc6e Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 4 Jul 2016 12:24:48 +0200 Subject: [PATCH 367/509] Update README --- README.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d2a413e75..2045741fe 100644 --- a/README.md +++ b/README.md @@ -103,25 +103,25 @@ framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. First install the requirements: On Debian Systems: -``` +```sh $ apt-get -y install git python-pip python-dev python-soappy ``` On RedHat Systems: -``` +```sh $ yum -y install epel-release $ yum -y install git gcc python-devel python-pip SOAPpy python-importlib python-requests ``` Then install the TOSCA parser: -``` +```sh $ pip install git+http://github.com/indigo-dc/tosca-parser ``` Finally install the IM service: -``` +```sh $ pip install git+http://github.com/indigo-dc/im ``` @@ -131,13 +131,13 @@ Download the RPM package from [GitHub](https://github.com/indigo-dc/im/releases/ Also remember to download the RPM of the RADL package also from [GitHub](https://github.com/grycap/radl/releases/latest) and the tosca-parser RPM file from [GitHub](https://github.com/indigo-dc/tosca-parser/releases/latest). You must have the epel repository enabled: -``` +```sh $ yum install epel-release ``` Then install the downloaded RPMs: -``` +```sh $ yum localinstall IM-*.rpm RADL-*.rpm tosca-parser-*.rpm ``` @@ -155,7 +155,7 @@ You can download it from their corresponding PPAs. But here you have some links: It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see [Ansible installation](http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu)): -``` +```sh $ sudo apt-get install software-properties-common $ sudo apt-add-repository ppa:ansible/ansible $ sudo apt-get update @@ -163,7 +163,7 @@ $ sudo apt-get update Put all the .deb files in the same directory and do: -``` +```sh $ sudo dpkg -i *.deb $ sudo apt install -f -y ``` @@ -176,25 +176,25 @@ execute the next set of commands: On Debian Systems: -``` +```sh $ chkconfig im on ``` Or for newer systems like ubuntu 14.04: -``` +```sh $ sysv-rc-conf im on ``` On RedHat Systems: -``` +```sh $ update-rc.d im start 99 2 3 4 5 . stop 05 0 1 6 . ``` Or you can do it manually: -``` +```sh $ ln -s /etc/init.d/im /etc/rc2.d/S99im $ ln -s /etc/init.d/im /etc/rc3.d/S99im $ ln -s /etc/init.d/im /etc/rc5.d/S99im @@ -240,8 +240,13 @@ And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates path A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. -How to launch the IM service using docker: +How to launch the IM service using docker:: ```sh -sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im +$ sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im ``` +You can also specify an external MySQL server to store IM data using the IM_DATA_DB environment variable:: + +```sh +$ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_DATA_DB=mysql://username:password@server/db_name --name im indigodatacloud/im +``` \ No newline at end of file From 683aaeb0a7a037924419eb2b7baf93153aeb3c6c Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 4 Jul 2016 13:09:09 +0200 Subject: [PATCH 368/509] Update README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2045741fe..c4b0175d6 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,10 @@ framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- -### 1.3.1 FROM PIP +### 1.3.1 FROM SOURCE + +**WARNING: In some GNU/Linux distributions (RHEL 6 or equivalents) you must uninstall +the packages 'python-paramiko' and 'python-crypto' before installing the IM with pip.** First install the requirements: From 659992ce185855fede68eabe4eef04833d4abe6a Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 6 Jul 2016 08:57:52 +0200 Subject: [PATCH 369/509] Remove libcloud installation to use the current release 1.0 --- docker-devel/Dockerfile | 6 ------ docker/Dockerfile | 6 ------ 2 files changed, 12 deletions(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index f4a74c3e3..bcf2a13ac 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -10,12 +10,6 @@ RUN cd tmp \ && cd tosca-parser \ && pip install /tmp/tosca-parser -# Install libcloud -RUN cd tmp \ - && git clone https://github.com/indigo-dc/libcloud.git \ - && cd libcloud \ - && pip install /tmp/libcloud - # Install im indigo tosca fork branch 'devel' RUN cd tmp \ && git clone --branch devel --recursive https://github.com/indigo-dc/im.git \ diff --git a/docker/Dockerfile b/docker/Dockerfile index 81f4750f0..f06eff40a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,12 +30,6 @@ RUN cd tmp \ && cd tosca-parser \ && pip install /tmp/tosca-parser -# Install libcloud from git untill the updates are released -RUN cd tmp \ - && git clone https://github.com/apache/libcloud.git \ - && cd libcloud \ - && pip install /tmp/libcloud - # Install im indigo tosca fork RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/im.git \ From d95d7a3c42ea39fdc7708c2823bd891b5dd1c4d8 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 6 Jul 2016 08:58:30 +0200 Subject: [PATCH 370/509] Remove the ansible requirement as it makes to generate an incorrect dependency python-ansible --- packages/generate_deb.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/generate_deb.sh b/packages/generate_deb.sh index cc369ad5e..3b1a36ef2 100755 --- a/packages/generate_deb.sh +++ b/packages/generate_deb.sh @@ -2,6 +2,8 @@ apt update apt install -y python-stdeb +# remove the ansible requirement as it makes to generate an incorrect dependency python-ansible +sed -i '/install_requires/c\ install_requires=["paramiko >= 1.14", "PyYAML", "SOAPpy",' setup.py python setup.py --command-packages=stdeb.command sdist_dsc --depends "python-tosca-parser, python-radl, ansible, python-paramiko, python-yaml, python-soappy, python-boto, python-libcloud, python-bottle, python-netaddr, python-scp" bdist_deb mkdir dist_pkg cp deb_dist/*.deb dist_pkg \ No newline at end of file From b96e58121d5b17a4bca330ca1062f80c7afc7c50 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 6 Jul 2016 08:58:50 +0200 Subject: [PATCH 371/509] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4b0175d6..bb762cb34 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ be installed in the system. + The boto library version 2.29 or later must be installed (http://boto.readthedocs.org/en/latest/). - + The apache-libcloud library version 0.18 or later + + The apache-libcloud library version 1.0.0 or later must be installed (http://libcloud.apache.org/). + The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at From feace5a046aa2886369cf73ad49b38d11be22075 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 6 Jul 2016 09:09:40 +0200 Subject: [PATCH 372/509] Update README --- README | 6 +++--- README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README b/README index 93c4d7ac4..33e3a7034 100644 --- a/README +++ b/README @@ -117,9 +117,9 @@ and the tosca-parser Deb file from GitHub (https://github.com/indigo-dc/tosca-pa In Ubuntu 14.04 there are some requisites not available for the "trusty" version or are too old, so you have to manually install them manually. You can download it from their corresponding PPAs. But here you have some links: - * python-backports.ssl-match-hostname: https://launchpad.net/ubuntu/+source/backports.ssl-match-hostname/3.4.0.2-1/+build/6206773/+files/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb - * python-scp: http://launchpadlibrarian.net/210648810/python-scp_0.10.2-1_all.deb - * python-libcloud: https://launchpad.net/ubuntu/+source/libcloud/0.20.0-1/+build/8869143/+files/python-libcloud_0.20.0-1_all.deb + * python-backports.ssl-match-hostname: http://archive.ubuntu.com/ubuntu/pool/universe/b/backports.ssl-match-hostname/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb + * python-scp: http://archive.ubuntu.com/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb + * python-libcloud: http://archive.ubuntu.com/ubuntu/pool/universe/libc/libcloud/python-libcloud_0.20.0-1_all.deb It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see Ansible installation - http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu): diff --git a/README.md b/README.md index bb762cb34..50b5ffed0 100644 --- a/README.md +++ b/README.md @@ -152,9 +152,9 @@ Also remember to download the Deb of the RADL package also from [GitHub](https:/ In Ubuntu 14.04 there are some requisites not available for the "trusty" version or are too old, so you have to manually install them manually. You can download it from their corresponding PPAs. But here you have some links: - * python-backports.ssl-match-hostname: [download](https://launchpad.net/ubuntu/+source/backports.ssl-match-hostname/3.4.0.2-1/+build/6206773/+files/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb) - * python-scp: [download](http://launchpadlibrarian.net/210648810/python-scp_0.10.2-1_all.deb>) - * python-libcloud: [download](https://launchpad.net/ubuntu/+source/libcloud/0.20.0-1/+build/8869143/+files/python-libcloud_0.20.0-1_all.deb) + * python-backports.ssl-match-hostname: [download](http://archive.ubuntu.com/ubuntu/pool/universe/b/backports.ssl-match-hostname/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb) + * python-scp: [download](http://archive.ubuntu.com/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb) + * python-libcloud: [download](http://archive.ubuntu.com/ubuntu/pool/universe/libc/libcloud/python-libcloud_0.20.0-1_all.deb) It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see [Ansible installation](http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu)): From 230e7840b38c9e4024e2f6b6c05419c3190dba72 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 6 Jul 2016 09:13:34 +0200 Subject: [PATCH 373/509] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 50b5ffed0..5075c84cc 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ be installed in the system. must be installed (http://boto.readthedocs.org/en/latest/). + The apache-libcloud library version 1.0.0 or later - must be installed (http://libcloud.apache.org/). + must be installed (http://libcloud.apache.org/). To support OpenStack sites with IAM authentication, + version 1.0.0 or later must be installed. + The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at https://github.com/indigo-dc/tosca-parser but we are working to improve the mainstream version From 3d8d221a1d67b4f8dadfe6e5c82de5ef1fefd95d Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 6 Jul 2016 09:13:58 +0200 Subject: [PATCH 374/509] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5075c84cc..a9d719d11 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ be installed in the system. + The boto library version 2.29 or later must be installed (http://boto.readthedocs.org/en/latest/). - + The apache-libcloud library version 1.0.0 or later + + The apache-libcloud library version 0.18 or later must be installed (http://libcloud.apache.org/). To support OpenStack sites with IAM authentication, version 1.0.0 or later must be installed. From cdbb2c100aba82318ace08ddc70ef965416713f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 6 Jul 2016 12:56:00 +0200 Subject: [PATCH 375/509] Update image used for the devel build --- docker-devel/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index bcf2a13ac..103b7cae4 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile to create a container with the IM service -FROM grycapjenkins/im-base:latest +FROM grycap/jenkins:ubuntu14.04-im-base MAINTAINER Miguel Caballer LABEL version="1.4.5" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" From 119e5d7259c83eeb120a8632c7a830846c8a651e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 6 Jul 2016 13:22:30 +0200 Subject: [PATCH 376/509] Update Dockerfile --- docker-devel/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 103b7cae4..5da8b4401 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -4,6 +4,9 @@ MAINTAINER Miguel Caballer LABEL version="1.4.5" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" +# Add unresolved LibCloud dependency +RUN pip install backports.ssl_match_hostname + # Install tosca-parser RUN cd tmp \ && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ From d18341bcf62017ae787d53761d923b83a62acefa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Wed, 6 Jul 2016 13:23:07 +0200 Subject: [PATCH 377/509] Update Dockerfile --- docker/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index f06eff40a..5e0a7723b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,6 +19,9 @@ RUN apt-get update && apt-get install -y \ python-mysqldb \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* + +# Add unresolved LibCloud dependency +RUN pip install backports.ssl_match_hostname # Install CherryPy to enable HTTPS in REST API RUN pip install setuptools --upgrade -I From bf42ca999f9873ddc000ba95fe4e34e43092e202 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 7 Jul 2016 10:31:41 +0200 Subject: [PATCH 378/509] Use INDIGO repos in conf-ansible --- contextualization/conf-ansible.yml | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index c18c6542d..f313c2f82 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -6,32 +6,44 @@ - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" - - - name: Apt-get update - apt: update_cache=yes - when: ansible_os_family == "Debian" - name: EPEL - yum: name=epel-release + yum: name=epel-release,yum-priorities when: ansible_os_family == "RedHat" and ansible_distribution != "Fedora" ####################### Install Ansible in Ubuntu and RHEL systems with apt and yum ################################### ################### because they have recent versions of ansible in system repositories ############################### +################# Use INDIGO repos from Ubuntu 14 and CentOS 7 to assure a stable version ############################ + - name: Ubuntu install indigo list + get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list dest=/etc/apt/sources.list.d/indigo1-ubuntu14_04.list + when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" + - name: Ubuntu install requirements apt: name=software-properties-common - when: ansible_distribution == "Ubuntu" - + when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") + - name: Ubuntu install Ansible PPA repo apt_repository: repo='ppa:ansible/ansible' - when: ansible_distribution == "Ubuntu" + when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") + + - name: Apt-get update + apt: update_cache=yes + when: ansible_os_family == "Debian" - name: Ubuntu install Ansible with apt - apt: name=ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip + apt: name=ansible,python-dev,python-pip,python-jinja2,sshpass,openssh-client,unzip,libffi-dev,libssl-dev when: ansible_distribution == "Ubuntu" - - name: Yum install Ansible RH - yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,wget + - name: RH7 indigo repos + get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/yum.repos.d/{{item}} + with_items: + - indigo1-testing-updates.repo + - indigo1-testing-base.repo + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" + + - name: RH7 install ansible with yum + yum: name=ansible,python-devel,wget,openssh-clients,sshpass,python-pip,libffi-devel,openssl-devel when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" ############################################ In other systems use pip ################################################# @@ -39,10 +51,10 @@ - name: Apt install requirements apt: name=unzip,gcc,python-dev,openssh-client,sshpass,python-pip,libffi-dev,libssl-dev when: ansible_os_family == "Debian" and ansible_distribution != "Ubuntu" - - - name: Yum install requirements RH or Fedora + + - name: Yum install requirements RH6 or Fedora yum: name=python-distribute,gcc,python-devel,wget,openssh-clients,sshpass,python-pip,libffi-devel,openssl-devel - when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6 + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6 and ansible_distribution_major_version|int < 7 - name: Yum install requirements RH5 yum: name=python26,python26-simplejson,python26-distribute,gcc,python26-devel,openssh-clients,sshpass,libffi-devel,openssl-devel From 4c3928343923f33ef39b33cb8d0347fe64cf4c6f Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 7 Jul 2016 13:01:35 +0200 Subject: [PATCH 379/509] Remove INDIGO repos in conf-ansible till they work --- contextualization/conf-ansible.yml | 51 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index f313c2f82..b25ce44b7 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -6,44 +6,43 @@ - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" + + # Disable IPv6 + - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" + with_items: + - 'net.ipv6.conf.all.disable_ipv6' + - 'net.ipv6.conf.default.disable_ipv6' + - 'net.ipv6.conf.lo.disable_ipv6' + ignore_errors: yes + - shell: sysctl -p + ignore_errors: yes + + + - name: Apt-get update + apt: update_cache=yes + when: ansible_os_family == "Debian" - name: EPEL - yum: name=epel-release,yum-priorities + yum: name=epel-release when: ansible_os_family == "RedHat" and ansible_distribution != "Fedora" ####################### Install Ansible in Ubuntu and RHEL systems with apt and yum ################################### ################### because they have recent versions of ansible in system repositories ############################### -################# Use INDIGO repos from Ubuntu 14 and CentOS 7 to assure a stable version ############################ - - name: Ubuntu install indigo list - get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list dest=/etc/apt/sources.list.d/indigo1-ubuntu14_04.list - when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" - - name: Ubuntu install requirements apt: name=software-properties-common - when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") - + when: ansible_distribution == "Ubuntu" + - name: Ubuntu install Ansible PPA repo apt_repository: repo='ppa:ansible/ansible' - when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") - - - name: Apt-get update - apt: update_cache=yes - when: ansible_os_family == "Debian" + when: ansible_distribution == "Ubuntu" - name: Ubuntu install Ansible with apt - apt: name=ansible,python-dev,python-pip,python-jinja2,sshpass,openssh-client,unzip,libffi-dev,libssl-dev + apt: name=ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip when: ansible_distribution == "Ubuntu" - - name: RH7 indigo repos - get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/yum.repos.d/{{item}} - with_items: - - indigo1-testing-updates.repo - - indigo1-testing-base.repo - when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" - - - name: RH7 install ansible with yum - yum: name=ansible,python-devel,wget,openssh-clients,sshpass,python-pip,libffi-devel,openssl-devel + - name: Yum install Ansible RH + yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,wget when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" ############################################ In other systems use pip ################################################# @@ -51,10 +50,10 @@ - name: Apt install requirements apt: name=unzip,gcc,python-dev,openssh-client,sshpass,python-pip,libffi-dev,libssl-dev when: ansible_os_family == "Debian" and ansible_distribution != "Ubuntu" - - - name: Yum install requirements RH6 or Fedora + + - name: Yum install requirements RH or Fedora yum: name=python-distribute,gcc,python-devel,wget,openssh-clients,sshpass,python-pip,libffi-devel,openssl-devel - when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6 and ansible_distribution_major_version|int < 7 + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6 - name: Yum install requirements RH5 yum: name=python26,python26-simplejson,python26-distribute,gcc,python26-devel,openssh-clients,sshpass,libffi-devel,openssl-devel From 1f6be203478b911690953cb81938326146750a17 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 8 Jul 2016 09:21:13 +0200 Subject: [PATCH 380/509] Minor bugfix --- IM/connectors/Docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/connectors/Docker.py b/IM/connectors/Docker.py index df6addd03..88f83f6ad 100644 --- a/IM/connectors/Docker.py +++ b/IM/connectors/Docker.py @@ -83,7 +83,7 @@ def get_http_connection(self, auth_data): else: conn = httplib.HTTPSConnection( self.cloud.server, self.cloud.port) - elif self.cloud.protocol == 'http': + elif self.cloud.protocol == 'http' or not self.cloud.protocol: self.logger.warn("Using a unsecure connection to docker API!") conn = httplib.HTTPConnection(self.cloud.server, self.cloud.port) From 57a36ce17729f1fc8b85eeb8378e3ed111b45d70 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 8 Jul 2016 11:35:38 +0200 Subject: [PATCH 381/509] Improve REST tests --- test/unit/REST.py | 77 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/test/unit/REST.py b/test/unit/REST.py index 80cdb6325..2f41ba03d 100755 --- a/test/unit/REST.py +++ b/test/unit/REST.py @@ -21,6 +21,7 @@ import unittest import sys from mock import patch, MagicMock +from IM.InfrastructureInfo import InfrastructureInfo sys.path.append("..") sys.path.append(".") @@ -100,8 +101,9 @@ def test_GetInfrastructureInfo(self, bottle_request, GetInfrastructureInfo): @patch("IM.InfrastructureManager.InfrastructureManager.GetInfrastructureContMsg") @patch("IM.InfrastructureManager.InfrastructureManager.GetInfrastructureRADL") @patch("IM.InfrastructureManager.InfrastructureManager.GetInfrastructureState") + @patch("IM.InfrastructureManager.InfrastructureManager.get_infrastructure") @patch("bottle.request") - def test_GetInfrastructureProperty(self, bottle_request, GetInfrastructureState, + def test_GetInfrastructureProperty(self, bottle_request, get_infrastructure, GetInfrastructureState, GetInfrastructureRADL, GetInfrastructureContMsg): """Test REST GetInfrastructureProperty.""" bottle_request.return_value = MagicMock() @@ -112,12 +114,21 @@ def test_GetInfrastructureProperty(self, bottle_request, GetInfrastructureState, GetInfrastructureState.return_value = {'state': "running", 'vm_states': {"vm1": "running", "vm2": "running"}} GetInfrastructureRADL.return_value = "radl" GetInfrastructureContMsg.return_value = "contmsg" + + inf = MagicMock() + get_infrastructure.return_value = inf + tosca = MagicMock() + inf.extra_info = {"TOSCA": tosca} + tosca.get_outputs.return_value = "outputs" res = RESTGetInfrastructureProperty("1", "state") self.assertEqual(json.loads(res)["state"]["state"], "running") res = RESTGetInfrastructureProperty("1", "contmsg") self.assertEqual(res, "contmsg") + + res = RESTGetInfrastructureProperty("1", "outputs") + self.assertEqual(res, '{"outputs": "outputs"}') res = RESTGetInfrastructureProperty("1", "radl") self.assertEqual(res, "radl") @@ -135,8 +146,9 @@ def test_DestroyInfrastructure(self, bottle_request, DestroyInfrastructure): self.assertEqual(res, "") @patch("IM.InfrastructureManager.InfrastructureManager.CreateInfrastructure") + @patch("IM.InfrastructureManager.InfrastructureManager.get_infrastructure") @patch("bottle.request") - def test_CreateInfrastructure(self, bottle_request, CreateInfrastructure): + def test_CreateInfrastructure(self, bottle_request, get_infrastructure, CreateInfrastructure): """Test REST CreateInfrastructure.""" bottle_request.environ = {'HTTP_HOST': 'imserver.com'} bottle_request.return_value = MagicMock() @@ -149,6 +161,28 @@ def test_CreateInfrastructure(self, bottle_request, CreateInfrastructure): res = RESTCreateInfrastructure() self.assertEqual(res, "http://imserver.com/infrastructures/1") + + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" + "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " + "username = user; password = pass"), + "Content-Type": "application/json"} + bottle_request.body.read.return_value = read_file_as_string("../files/test_simple.json") + + CreateInfrastructure.return_value = "1" + + res = RESTCreateInfrastructure() + self.assertEqual(res, "http://imserver.com/infrastructures/1") + + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" + "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " + "username = user; password = pass"), + "Content-Type": "text/yaml"} + bottle_request.body.read.return_value = read_file_as_string("../files/tosca_create.yml") + + CreateInfrastructure.return_value = "1" + + res = RESTCreateInfrastructure() + self.assertEqual(res, "http://imserver.com/infrastructures/1") @patch("IM.InfrastructureManager.InfrastructureManager.GetVMInfo") @patch("bottle.request") @@ -189,8 +223,9 @@ def test_GetVMProperty(self, bottle_request, GetVMContMsg, GetVMProperty): self.assertEqual(res, "contmsg") @patch("IM.InfrastructureManager.InfrastructureManager.AddResource") + @patch("IM.InfrastructureManager.InfrastructureManager.get_infrastructure") @patch("bottle.request") - def test_AddResource(self, bottle_request, AddResource): + def test_AddResource(self, bottle_request, get_infrastructure, AddResource): """Test REST AddResource.""" bottle_request.environ = {'HTTP_HOST': 'imserver.com'} bottle_request.return_value = MagicMock() @@ -204,6 +239,24 @@ def test_AddResource(self, bottle_request, AddResource): res = RESTAddResource("1") self.assertEqual(res, "http://imserver.com/infrastructures/1/vms/1") + + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" + "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " + "username = user; password = pass"), + "Content-Type": "application/json"} + bottle_request.body.read.return_value = read_file_as_string("../files/test_simple.json") + + res = RESTAddResource("1") + self.assertEqual(res, "http://imserver.com/infrastructures/1/vms/1") + + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" + "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " + "username = user; password = pass"), + "Content-Type": "text/yaml"} + bottle_request.body.read.return_value = read_file_as_string("../files/tosca_create.yml") + + res = RESTAddResource("1") + self.assertEqual(res, "http://imserver.com/infrastructures/1/vms/1") @patch("IM.InfrastructureManager.InfrastructureManager.RemoveResource") @patch("bottle.request") @@ -235,6 +288,15 @@ def test_AlterVM(self, bottle_request, AlterVM): res = RESTAlterVM("1", "1") self.assertEqual(res, "vm_info") + + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" + "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " + "username = user; password = pass"), + "Content-Type": "application/json"} + bottle_request.body.read.return_value = read_file_as_string("../files/test_simple.json") + + res = RESTAlterVM("1", "1") + self.assertEqual(res, "vm_info") @patch("IM.InfrastructureManager.InfrastructureManager.Reconfigure") @patch("bottle.request") @@ -251,6 +313,15 @@ def test_Reconfigure(self, bottle_request, Reconfigure): res = RESTReconfigureInfrastructure("1") self.assertEqual(res, "") + + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" + "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " + "username = user; password = pass"), + "Content-Type": "application/json"} + bottle_request.body.read.return_value = read_file_as_string("../files/test_simple.json") + + res = RESTReconfigureInfrastructure("1") + self.assertEqual(res, "") @patch("IM.InfrastructureManager.InfrastructureManager.StartInfrastructure") @patch("bottle.request") From 2a4730359a2d2da28b7d7ec898a6670512fd1ac2 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 8 Jul 2016 11:59:58 +0200 Subject: [PATCH 382/509] Bugfix --- IM/connectors/OpenStack.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/IM/connectors/OpenStack.py b/IM/connectors/OpenStack.py index 412acb966..32057de31 100644 --- a/IM/connectors/OpenStack.py +++ b/IM/connectors/OpenStack.py @@ -290,6 +290,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): args['ex_userdata'] = cloud_init keypair = None + keypair_name = None public_key = system.getValue("disk.0.os.credentials.public_key") if public_key: keypair = driver.get_key_pair(public_key) @@ -299,8 +300,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): else: if "ssh_key" in driver.features.get("create_node", []): args["auth"] = NodeAuthSSHKey(public_key) - else: - args["ex_keyname"] = keypair.name + elif not system.getValue("disk.0.os.credentials.password"): keypair_name = "im-%d" % int(time.time() * 100.0) keypair = driver.create_key_pair(keypair_name) @@ -326,7 +326,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): vm.info.systems[0].setValue('instance_id', str(node.id)) vm.info.systems[0].setValue('instance_name', str(node.name)) # Add the keypair name to remove it later - vm.keypair = keypair_name + if keypair_name: + vm.keypair = keypair_name self.logger.debug("Node successfully created.") all_failed = False res.append((True, vm)) From 14dba5d05992d66483bd32269362a3473eea2a6a Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 8 Jul 2016 12:24:00 +0200 Subject: [PATCH 383/509] Add support for token_types private and public key --- IM/tosca/Tosca.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index e89fc96ff..55c4e2141 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -1105,14 +1105,28 @@ def _gen_system(node, nodetemplates): if value.find("://") == -1: value = "docker://%s" % value elif prop.name == "credential": - # Currently oly supports user/pass credentials + token_type = "password" + if 'token_type' in value and value['token_type']: + token_type = value['token_type'] + + token = None if 'token' in value and value['token']: - feature = Feature( - "disk.0.os.credentials.password", "=", value['token']) - res.addFeature(feature) + token = value['token'] + + if token: + if token_type == "password": + feature = Feature("disk.0.os.credentials.password", "=", token) + res.addFeature(feature) + elif token_type == "private_key": + feature = Feature("disk.0.os.credentials.private_key", "=", token) + res.addFeature(feature) + elif token_type == "public_key": + feature = Feature("disk.0.os.credentials.public_key", "=", token) + res.addFeature(feature) + else: + Tosca.logger.warn("Unknown tyoe of token %s. Ignoring." % token_type) if 'user' not in value or not value['user']: - raise Exception( - "User must be specified in the image credentials.") + raise Exception("User must be specified in the image credentials.") name = "disk.0.os.credentials.username" value = value['user'] From 83f65fa1c35d3e8967fa786a3e3250bdce725fd5 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 8 Jul 2016 12:24:12 +0200 Subject: [PATCH 384/509] Style changes --- IM/connectors/OpenStack.py | 2 +- test/unit/REST.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/IM/connectors/OpenStack.py b/IM/connectors/OpenStack.py index 32057de31..3c6946999 100644 --- a/IM/connectors/OpenStack.py +++ b/IM/connectors/OpenStack.py @@ -290,7 +290,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): args['ex_userdata'] = cloud_init keypair = None - keypair_name = None + keypair_name = None public_key = system.getValue("disk.0.os.credentials.public_key") if public_key: keypair = driver.get_key_pair(public_key) diff --git a/test/unit/REST.py b/test/unit/REST.py index 2f41ba03d..0f806c543 100755 --- a/test/unit/REST.py +++ b/test/unit/REST.py @@ -114,7 +114,7 @@ def test_GetInfrastructureProperty(self, bottle_request, get_infrastructure, Get GetInfrastructureState.return_value = {'state': "running", 'vm_states': {"vm1": "running", "vm2": "running"}} GetInfrastructureRADL.return_value = "radl" GetInfrastructureContMsg.return_value = "contmsg" - + inf = MagicMock() get_infrastructure.return_value = inf tosca = MagicMock() @@ -126,7 +126,7 @@ def test_GetInfrastructureProperty(self, bottle_request, get_infrastructure, Get res = RESTGetInfrastructureProperty("1", "contmsg") self.assertEqual(res, "contmsg") - + res = RESTGetInfrastructureProperty("1", "outputs") self.assertEqual(res, '{"outputs": "outputs"}') @@ -161,7 +161,7 @@ def test_CreateInfrastructure(self, bottle_request, get_infrastructure, CreateIn res = RESTCreateInfrastructure() self.assertEqual(res, "http://imserver.com/infrastructures/1") - + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " "username = user; password = pass"), @@ -172,7 +172,7 @@ def test_CreateInfrastructure(self, bottle_request, get_infrastructure, CreateIn res = RESTCreateInfrastructure() self.assertEqual(res, "http://imserver.com/infrastructures/1") - + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " "username = user; password = pass"), @@ -239,16 +239,16 @@ def test_AddResource(self, bottle_request, get_infrastructure, AddResource): res = RESTAddResource("1") self.assertEqual(res, "http://imserver.com/infrastructures/1/vms/1") - + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " "username = user; password = pass"), "Content-Type": "application/json"} bottle_request.body.read.return_value = read_file_as_string("../files/test_simple.json") - + res = RESTAddResource("1") self.assertEqual(res, "http://imserver.com/infrastructures/1/vms/1") - + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " "username = user; password = pass"), @@ -288,13 +288,13 @@ def test_AlterVM(self, bottle_request, AlterVM): res = RESTAlterVM("1", "1") self.assertEqual(res, "vm_info") - + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " "username = user; password = pass"), "Content-Type": "application/json"} bottle_request.body.read.return_value = read_file_as_string("../files/test_simple.json") - + res = RESTAlterVM("1", "1") self.assertEqual(res, "vm_info") @@ -313,13 +313,13 @@ def test_Reconfigure(self, bottle_request, Reconfigure): res = RESTReconfigureInfrastructure("1") self.assertEqual(res, "") - + bottle_request.headers = {"AUTHORIZATION": ("type = InfrastructureManager; username = user; password = pass\n" "id = one; type = OpenNebula; host = onedock.i3m.upv.es:2633; " "username = user; password = pass"), "Content-Type": "application/json"} bottle_request.body.read.return_value = read_file_as_string("../files/test_simple.json") - + res = RESTReconfigureInfrastructure("1") self.assertEqual(res, "") From 9f433042923971fc957dbda441f114d22a3a2511 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Jul 2016 09:08:31 +0200 Subject: [PATCH 385/509] Remove CpuShares --- IM/connectors/Docker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/connectors/Docker.py b/IM/connectors/Docker.py index 88f83f6ad..f022f50f1 100644 --- a/IM/connectors/Docker.py +++ b/IM/connectors/Docker.py @@ -194,7 +194,7 @@ def _generate_create_request_data(self, outports, system, vm, ssh_port): cont_data['Volumes'] = volumes HostConfig = {} - HostConfig['CpuShares'] = "%d" % cpu + #HostConfig['CpuShares'] = "%d" % cpu HostConfig['Memory'] = memory HostConfig['PortBindings'] = self._generate_port_bindings( outports, ssh_port) From 2d018db83f683913650e577cd0f507e4d0befb4a Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 13 Jul 2016 09:29:27 +0200 Subject: [PATCH 386/509] Set linux as os type if not defined --- IM/VirtualMachine.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IM/VirtualMachine.py b/IM/VirtualMachine.py index 74e54fb7b..e885981d7 100644 --- a/IM/VirtualMachine.py +++ b/IM/VirtualMachine.py @@ -211,9 +211,10 @@ def getIfaceIP(self, iface_num): def getOS(self): """ - Get O.S. of this VM + Get O.S. of this VM (if not specified assume linux) """ - return self.info.systems[0].getValue("disk.0.os.name") + os = self.info.systems[0].getValue("disk.0.os.name") + return os if os else "linux" def getCredentialValues(self, new=False): """ From 2ede76096510fcd91dd60b031319ce142f6033c7 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Jul 2016 10:01:52 +0200 Subject: [PATCH 387/509] Bugfix in doc with python-scp link --- README | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index 33e3a7034..4e6d7064c 100644 --- a/README +++ b/README @@ -118,7 +118,7 @@ In Ubuntu 14.04 there are some requisites not available for the "trusty" version You can download it from their corresponding PPAs. But here you have some links: * python-backports.ssl-match-hostname: http://archive.ubuntu.com/ubuntu/pool/universe/b/backports.ssl-match-hostname/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb - * python-scp: http://archive.ubuntu.com/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb + * python-scp: http://archive.ubuntu.com/ubuntu/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb * python-libcloud: http://archive.ubuntu.com/ubuntu/pool/universe/libc/libcloud/python-libcloud_0.20.0-1_all.deb It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see Ansible installation - http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu): diff --git a/README.md b/README.md index a9d719d11..0fef705fe 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ In Ubuntu 14.04 there are some requisites not available for the "trusty" version You can download it from their corresponding PPAs. But here you have some links: * python-backports.ssl-match-hostname: [download](http://archive.ubuntu.com/ubuntu/pool/universe/b/backports.ssl-match-hostname/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb) - * python-scp: [download](http://archive.ubuntu.com/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb) + * python-scp: [download](http://archive.ubuntu.com/ubuntu/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb) * python-libcloud: [download](http://archive.ubuntu.com/ubuntu/pool/universe/libc/libcloud/python-libcloud_0.20.0-1_all.deb) It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see [Ansible installation](http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu)): From 57036b2f6b3a4b74d1b3fe05f30762711fb4c8f4 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jul 2016 09:13:57 +0200 Subject: [PATCH 388/509] Minor changes --- test/unit/test_im_logic.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 593e79c2f..a188ad61f 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -138,7 +138,6 @@ def test_inf_creation1(self): disk.0.os.credentials.password = 'yoyoyo' and disk.0.os.name = 'linux' ) - system wn ( cpu.arch='x86_64' and cpu.count>=1 and @@ -149,7 +148,6 @@ def test_inf_creation1(self): disk.0.os.credentials.password = 'yoyoyo' and disk.0.os.name = 'linux' ) - deploy front 1 cloud0 deploy wn 1 cloud1 """ From 05008b9d4eed9416b6aa8ceb44c3797b6181af90 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 15 Jul 2016 09:44:48 +0200 Subject: [PATCH 389/509] Minor changes --- test/unit/test_im_logic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index a188ad61f..593e79c2f 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -138,6 +138,7 @@ def test_inf_creation1(self): disk.0.os.credentials.password = 'yoyoyo' and disk.0.os.name = 'linux' ) + system wn ( cpu.arch='x86_64' and cpu.count>=1 and @@ -148,6 +149,7 @@ def test_inf_creation1(self): disk.0.os.credentials.password = 'yoyoyo' and disk.0.os.name = 'linux' ) + deploy front 1 cloud0 deploy wn 1 cloud1 """ From e3a6c6d4dd898178a713cffb692d65a69920d769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20P=C3=A9rez?= Date: Mon, 18 Jul 2016 13:23:09 +0200 Subject: [PATCH 390/509] Update devel dockerfile Update base image --- docker-devel/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-devel/Dockerfile b/docker-devel/Dockerfile index 7b6761793..f52b8aba0 100644 --- a/docker-devel/Dockerfile +++ b/docker-devel/Dockerfile @@ -1,5 +1,5 @@ # Dockerfile to create a container with the IM service -FROM grycap/jenkins:ubuntu14.04-im-base +FROM grycap/jenkins:ubuntu14.04-im MAINTAINER Miguel Caballer LABEL version="1.4.6" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" From 7bc35c5b52eca14285aac1748a5d368d19251e91 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 19 Jul 2016 09:50:54 +0200 Subject: [PATCH 391/509] Increse sleep time --- test/functional/test_im.py | 2 +- test/unit/test_im_logic.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/test_im.py b/test/functional/test_im.py index b1c2577b2..ea689576d 100755 --- a/test/functional/test_im.py +++ b/test/functional/test_im.py @@ -147,7 +147,7 @@ def test_inf_lifecycle(self): infId = IM.CreateInfrastructure(str(radl), auth0) - time.sleep(10) + time.sleep(15) state = IM.GetInfrastructureState(infId, auth0) self.assertEqual(state["state"], "unconfigured") diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 593e79c2f..476ffef75 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -597,7 +597,7 @@ def test_contextualize(self): infId = IM.CreateInfrastructure(str(radl), auth0) - time.sleep(10) + time.sleep(15) state = IM.GetInfrastructureState(infId, auth0) self.assertEqual(state["state"], "unconfigured") From bc66eadeae4d81fbcf7c9c951c47641af287a56e Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 26 Jul 2016 08:47:07 +0200 Subject: [PATCH 392/509] Use INDIGO repos to install ansible --- contextualization/conf-ansible.yml | 53 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index b25ce44b7..07b9d7287 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -6,51 +6,56 @@ - name: Install libselinux-python in RH action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" - - # Disable IPv6 - - lineinfile: dest=/etc/sysctl.conf regexp="{{ item }}" line="{{ item }} = 1" - with_items: - - 'net.ipv6.conf.all.disable_ipv6' - - 'net.ipv6.conf.default.disable_ipv6' - - 'net.ipv6.conf.lo.disable_ipv6' - ignore_errors: yes - - shell: sysctl -p - ignore_errors: yes - - - - name: Apt-get update - apt: update_cache=yes - when: ansible_os_family == "Debian" - name: EPEL - yum: name=epel-release + yum: name=epel-release,yum-priorities when: ansible_os_family == "RedHat" and ansible_distribution != "Fedora" ####################### Install Ansible in Ubuntu and RHEL systems with apt and yum ################################### ################### because they have recent versions of ansible in system repositories ############################### +################# Use INDIGO repos from Ubuntu 14 and CentOS 7 to assure a stable version ############################ + - name: Ubuntu install indigo list + get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/apt/sources.list.d/{{item}} + when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" + with_items: + - indigo1-testing-ubuntu14_04.list + - indigo1-ubuntu14_04.list + - name: Ubuntu install requirements apt: name=software-properties-common - when: ansible_distribution == "Ubuntu" - + when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") + - name: Ubuntu install Ansible PPA repo apt_repository: repo='ppa:ansible/ansible' - when: ansible_distribution == "Ubuntu" + when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") + + - name: Apt-get update + apt: update_cache=yes + when: ansible_os_family == "Debian" - name: Ubuntu install Ansible with apt - apt: name=ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip + apt: name=ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip force=yes when: ansible_distribution == "Ubuntu" - - name: Yum install Ansible RH - yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,wget + - name: RH indigo repos + get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/yum.repos.d/{{item}} + with_items: + - indigo1-testing-updates.repo + - indigo1-testing-base.repo + - indigo1-testing-third-party.repo + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" + + - name: RH7 install Ansible with yum + yum: name=ansible,python-pip,python-jinja2,sshpass,openssh-clients,unzip when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" - + ############################################ In other systems use pip ################################################# - name: Apt install requirements apt: name=unzip,gcc,python-dev,openssh-client,sshpass,python-pip,libffi-dev,libssl-dev when: ansible_os_family == "Debian" and ansible_distribution != "Ubuntu" - + - name: Yum install requirements RH or Fedora yum: name=python-distribute,gcc,python-devel,wget,openssh-clients,sshpass,python-pip,libffi-devel,openssl-devel when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6 From 7327d389b202e7942ae0c8fddfefa168f6ab371d Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 26 Jul 2016 08:47:30 +0200 Subject: [PATCH 393/509] Mionr changes --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0fef705fe..842b7ecc4 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- -### 1.3.1 FROM SOURCE +### 1.3.1 FROM PIP **WARNING: In some GNU/Linux distributions (RHEL 6 or equivalents) you must uninstall the packages 'python-paramiko' and 'python-crypto' before installing the IM with pip.** @@ -108,13 +108,13 @@ First install the requirements: On Debian Systems: ```sh -$ apt-get -y install git python-pip python-dev python-soappy +$ apt -y install git gcc python-dev libffi-dev libssl-dev python-pip sshpass ``` On RedHat Systems: ```sh $ yum -y install epel-release -$ yum -y install git gcc python-devel python-pip SOAPpy python-importlib python-requests +$ yum -y install git which gcc python-devel libffi-devel openssl-devel python-pip sshpass ``` Then install the TOSCA parser: @@ -123,6 +123,13 @@ Then install the TOSCA parser: $ pip install git+http://github.com/indigo-dc/tosca-parser ``` +For some problems with the dependencies of the apache-libcloud package in some systems (as ubuntu 14.04 or CentOS 6) +this package has to be installed manually: + +```sh +$ pip install backports-ssl_match_hostname +``` + Finally install the IM service: ```sh From 5a29c886d2535c633ac58874d6c662eeaac57a7e Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 26 Jul 2016 08:47:50 +0200 Subject: [PATCH 394/509] Mionr changes --- IM/connectors/Azure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/connectors/Azure.py b/IM/connectors/Azure.py index fd8e08412..4e69f2dd7 100644 --- a/IM/connectors/Azure.py +++ b/IM/connectors/Azure.py @@ -725,6 +725,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): if success: res.append((True, vm)) else: + self.delete_service(service_name, auth_data) self.logger.exception("Error waiting the VM creation") res.append((False, "Error waiting the VM creation")) From 7403a0dc05ea7bb5268a6a2d6f2b697251143f9b Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 26 Jul 2016 08:47:59 +0200 Subject: [PATCH 395/509] Mionr changes --- IM/ConfManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/ConfManager.py b/IM/ConfManager.py index 9823ac43a..54e0ed269 100644 --- a/IM/ConfManager.py +++ b/IM/ConfManager.py @@ -1374,7 +1374,7 @@ def configure_ansible(self, ssh, tmp_dir): ' when: ansible_os_family == "Debian"\n') recipe_out.write( - " - name: Install the % role with ansible-galaxy\n" % rolename) + " - name: Install the %s role with ansible-galaxy\n" % rolename) recipe_out.write( " command: ansible-galaxy -f install %s\n" % url) From 2baafb69ce87f3ff7948ec9398ed8090961f7e26 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 26 Jul 2016 08:48:46 +0200 Subject: [PATCH 396/509] Bugfix --- etc/im.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/im.cfg b/etc/im.cfg index 305713d3a..047a67ecb 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -102,7 +102,7 @@ PLAYBOOK_RETRIES = 3 # List of networks assumed as private # It must be a coma separated string of the network definitions (without spaces) # This are the default values: -# PRIVATE_NET_MASKS = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,100.64.0.0/10,192.0.0.0/24,198.18.0.0/15" +# PRIVATE_NET_MASKS = 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,100.64.0.0/10,192.0.0.0/24,198.18.0.0/15 [OpenNebula] # OpenNebula connector configuration values From 8be5a64c623f4c27a22651d36be2a7a856f087b9 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 26 Jul 2016 08:56:41 +0200 Subject: [PATCH 397/509] Style changes --- test/unit/test_ansible.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_ansible.py b/test/unit/test_ansible.py index 853761456..2fcaf391f 100755 --- a/test/unit/test_ansible.py +++ b/test/unit/test_ansible.py @@ -37,7 +37,7 @@ def test_ansible_thread(self): play_file_path = os.path.join(tests_path, "../files/play.yaml") inventory = os.path.join(tests_path, "../files/inventory") ansible_process = AnsibleThread(result, StringIO(), play_file_path, None, 1, None, - "password", 1, inventory, "username") + "password", 1, inventory, "username") ansible_process.run() _, (return_code, _), output = result.get() From 05f02b778f98a648de2d6dfae7588eb8e8929299 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 1 Sep 2016 15:31:24 +0200 Subject: [PATCH 398/509] Update README --- README | 88 +++++++++++++++++++++---------------------------------- README.md | 81 ++++++++++++++++++-------------------------------- 2 files changed, 63 insertions(+), 106 deletions(-) diff --git a/README b/README index 4e6d7064c..b4eb6029f 100644 --- a/README +++ b/README @@ -12,6 +12,15 @@ contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/). It is used by the [INDIGO Orchestrator](https://github.com/indigo-dc/orchestrator) to contact Cloud sites to finally deploy the VMs/containers. + +New features added: + ++ Support for TOSCA 1.0 YAML specification with the custom node types described in https://github.com/indigo-dc/tosca-types/blob/master/custom_types.yaml ++ Support for the Identity and Access Management Service (IAM). ++ Support for the Token Translation Service (TTS) to support IAM authetication on OpenNebula Clouds. ++ Improvements to access OpenStack Clouds that support IAM. + Read the documentation and more at http://www.grycap.upv.es/im. There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos @@ -92,76 +101,47 @@ framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- -1.3.1 From RPM packages (RH6 and RH7) -------------------------------------- +1.3.1 From RPM package +---------------------- -Download the RPM package from GitHub (https://github.com/grycap/im/releases/latest). -Also remember to download the RPM of the RADL package also from GitHub (https://github.com/grycap/radl/releases/latest) -and the tosca-parser RPM file from GitHub (https://github.com/indigo-dc/tosca-parser/releases/latest). - -You must have the epel repository enabled:: +You must have the epel repository enabled: - $ yum install epel-release - -Then install the downloaded RPMs:: + $ yum install epel-release - $ yum localinstall IM-*.rpm RADL-*.rpm tosca-parser-*.rpm +Then you have to enable the INDIGO - DataCloud packages repositories. See full instructions +at https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4. +Briefly you have to download the repo file from http://repo.indigo-datacloud.eu/repos/1/indigo1.repo in your /etc/yum.repos.d folder. -1.3.2 From Deb package (Tested with Ubuntu 14.04 and 16.04) ------------------------------------------------------------ + $ cd /etc/yum.repos.d + $ wget http://repo.indigo-datacloud.eu/repos/1/indigo1.repo -Download the Deb package from GitHub (https://github.com/grycap/im/releases/latest). -Also remember to download the Deb of the RADL package also from GitHub (https://github.com/grycap/radl/releases/latest) -and the tosca-parser Deb file from GitHub (https://github.com/indigo-dc/tosca-parser/releases/latest). +And then install the GPG key for the INDIGO repository: -In Ubuntu 14.04 there are some requisites not available for the "trusty" version or are too old, so you have to manually install them manually. -You can download it from their corresponding PPAs. But here you have some links: - - * python-backports.ssl-match-hostname: http://archive.ubuntu.com/ubuntu/pool/universe/b/backports.ssl-match-hostname/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb - * python-scp: http://archive.ubuntu.com/ubuntu/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb - * python-libcloud: http://archive.ubuntu.com/ubuntu/pool/universe/libc/libcloud/python-libcloud_0.20.0-1_all.deb - -It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see Ansible installation - http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu): + $ rpm --import http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc - $ sudo apt-get install software-properties-common - $ sudo apt-add-repository ppa:ansible/ansible - $ sudo apt-get update +Finally install the IM package. -Put all the .deb files in the same directory and do: + $ yum install IM - $ sudo dpkg -i *.deb - $ sudo apt install -f -y +1.3.2 From Deb package +---------------------- -1.3.3 From Source ------------------ - -First install the requirements: - -On Debian Systems: - - $ apt-get -y install git python-setuptools python-dev gcc python-soappy python-pip python-pbr python-dateutil - -On RedHat Systems: +You have to enable the INDIGO - DataCloud packages repositories. See full instructions +at https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4. +Briefly you have to download the list file from http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list in your /etc/apt/sources.list.d folder. - $ yum remove python-paramiko python-crypto - $ yum -y install git python-setuptools python-devel gcc SOAPpy python-dateutil python-six python-requests - $ easy_install pip - $ pip install pbr + $ cd /etc/apt/sources.list.d + $ wget http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list -Then install the TOSCA parser: +And then install the GPG key for INDIGO the repository: - $ cd /tmp - $ git clone --recursive https://github.com/indigo-dc/tosca-parser.git - $ cd tosca-parser - $ python setup.py install + $ wget -q -O - http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc | sudo apt-key add - -Finally install the IM service: +Finally install the IM package. - $ cd /tmp - $ git clone --recursive https://github.com/indigo-dc/im.git - $ cd im - $ python setup.py install + $ apt update + $ apt install python-im 1.4 CONFIGURATION ----------------- diff --git a/README.md b/README.md index 842b7ecc4..1d29938a6 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,14 @@ contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. -This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/) has -added support to TOSCA documents as input for the infrastructure creation. +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/). It is used by the [INDIGO Orchestrator](https://github.com/indigo-dc/orchestrator) to contact Cloud sites to finally deploy the VMs/containers. + +New features added: + ++ Support for TOSCA 1.0 YAML specification with the custom node types described in https://github.com/indigo-dc/tosca-types/blob/master/custom_types.yaml ++ Support for the Identity and Access Management Service (IAM). ++ Support for the Token Translation Service (TTS) to support IAM authetication on OpenNebula Clouds. ++ Improvements to access OpenStack Clouds that support IAM. Read the documentation and more at http://www.grycap.upv.es/im. @@ -99,84 +105,55 @@ framework (http://www.cherrypy.org/) and pyOpenSSL must be installed. 1.3 INSTALLING -------------- -### 1.3.1 FROM PIP - -**WARNING: In some GNU/Linux distributions (RHEL 6 or equivalents) you must uninstall -the packages 'python-paramiko' and 'python-crypto' before installing the IM with pip.** +### 1.3.1 FROM RPM -First install the requirements: - -On Debian Systems: -```sh -$ apt -y install git gcc python-dev libffi-dev libssl-dev python-pip sshpass -``` +You must have the epel repository enabled: -On RedHat Systems: ```sh -$ yum -y install epel-release -$ yum -y install git which gcc python-devel libffi-devel openssl-devel python-pip sshpass +$ yum install epel-release ``` -Then install the TOSCA parser: +Then you have to enable the INDIGO - DataCloud packages repositories. See full instructions +[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the repo file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1.repo) in your /etc/yum.repos.d folder. ```sh -$ pip install git+http://github.com/indigo-dc/tosca-parser +$ cd /etc/yum.repos.d +$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1.repo ``` -For some problems with the dependencies of the apache-libcloud package in some systems (as ubuntu 14.04 or CentOS 6) -this package has to be installed manually: +And then install the GPG key for the INDIGO repository: ```sh -$ pip install backports-ssl_match_hostname +$ rpm --import http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc ``` -Finally install the IM service: +Finally install the IM package. ```sh -$ pip install git+http://github.com/indigo-dc/im +$ yum install IM ``` -### 1.3.2 FROM RPM +### 1.3.2 FROM DEB -Download the RPM package from [GitHub](https://github.com/indigo-dc/im/releases/latest). -Also remember to download the RPM of the RADL package also from [GitHub](https://github.com/grycap/radl/releases/latest) and the tosca-parser RPM file from [GitHub](https://github.com/indigo-dc/tosca-parser/releases/latest). -You must have the epel repository enabled: - -```sh -$ yum install epel-release -``` - -Then install the downloaded RPMs: +You have to enable the INDIGO - DataCloud packages repositories. See full instructions +[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the list file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list) in your /etc/apt/sources.list.d folder. ```sh -$ yum localinstall IM-*.rpm RADL-*.rpm tosca-parser-*.rpm +$ cd /etc/apt/sources.list.d +$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list ``` -### 1.3.3 FROM DEB - -Download the Deb package from [GitHub](https://github.com/indigo-dc/im/releases/latest) -Also remember to download the Deb of the RADL package also from [GitHub](https://github.com/grycap/radl/releases/latest) and the tosca-parser Deb file from [GitHub](https://github.com/indigo-dc/tosca-parser/releases/latest). - -In Ubuntu 14.04 there are some requisites not available for the "trusty" version or are too old, so you have to manually install them manually. -You can download it from their corresponding PPAs. But here you have some links: - - * python-backports.ssl-match-hostname: [download](http://archive.ubuntu.com/ubuntu/pool/universe/b/backports.ssl-match-hostname/python-backports.ssl-match-hostname_3.4.0.2-1_all.deb) - * python-scp: [download](http://archive.ubuntu.com/ubuntu/pool/universe/p/python-scp/python-scp_0.10.2-1_all.deb) - * python-libcloud: [download](http://archive.ubuntu.com/ubuntu/pool/universe/libc/libcloud/python-libcloud_0.20.0-1_all.deb) - -It is also recommended to configure the Ansible PPA to install the newest versions of Ansible (see [Ansible installation](http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-apt-ubuntu)): +And then install the GPG key for INDIGO the repository: ```sh -$ sudo apt-get install software-properties-common -$ sudo apt-add-repository ppa:ansible/ansible -$ sudo apt-get update +$ wget -q -O - http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc | sudo apt-key add - ``` -Put all the .deb files in the same directory and do: +Finally install the IM package. ```sh -$ sudo dpkg -i *.deb -$ sudo apt install -f -y +$ apt update +$ apt install python-im ``` 1.4 CONFIGURATION From 75b1e4ce6742871d95c6c809896dc8953f37e72a Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 6 Sep 2016 13:15:52 +0200 Subject: [PATCH 399/509] Bugfixes conf-ansible.yml downloading centos repo files --- contextualization/conf-ansible.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 07b9d7287..18f213020 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -41,9 +41,8 @@ - name: RH indigo repos get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/yum.repos.d/{{item}} with_items: - - indigo1-testing-updates.repo - - indigo1-testing-base.repo - - indigo1-testing-third-party.repo + - indigo1.repo + - indigo1-testing.repo when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" - name: RH7 install Ansible with yum From d4efd8a46280aee90ddcf0310717b1469c3b0e5c Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 12:55:48 +0200 Subject: [PATCH 400/509] Add single site support --- IM/InfrastructureManager.py | 16 ++++++++++++++++ IM/config.py | 4 ++++ etc/im.cfg | 9 +++++++++ 3 files changed, 29 insertions(+) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index c397b36dd..82a6286a9 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -444,6 +444,10 @@ def AddResource(inf_id, radl_data, auth, context=True, failed_clouds=[]): systems_with_vmrc = {} for system_id in set([d.id for d in radl.deploys if d.vm_number > 0]): s = radl.get_system_by_name(system_id) + + if Config.SINGLE_SITE: + image_id = os.path.basename(s.getValue("disk.0.image.url")) + s.setValue("disk.0.image.url",Config.SINGLE_SITE_IMAGE_URL_PREFIX + image_id) if not s.getValue("disk.0.image.url") and len(vmrc_list) == 0: raise Exception( @@ -1231,6 +1235,18 @@ def check_auth_data(auth): if not InfrastructureManager.check_im_user(im_auth): raise UnauthorizedUserException() + if Config.SINGLE_SITE: + vmrc_auth = auth.getAuthInfo("VMRC") + single_site_auth = auth.getAuthInfo(Config.SINGLE_SITE_TYPE) + + single_site_auth[0]["host"] = Config.SINGLE_SITE_AUTH_HOST + + auth_list = [] + auth_list.extend(im_auth) + auth_list.extend(vmrc_auth) + auth_list.extend(single_site_auth) + auth = Authentication(auth_list) + # We have to check if TTS is needed for other auth item return auth diff --git a/IM/config.py b/IM/config.py index 879c518c1..7a0e62ac3 100644 --- a/IM/config.py +++ b/IM/config.py @@ -91,6 +91,10 @@ class Config: CONFMAMAGER_CHECK_STATE_INTERVAL = 5 UPDATE_CTXT_LOG_INTERVAL = 20 ANSIBLE_INSTALL_TIMEOUT = 900 + SINGLE_SITE = False + SINGLE_SITE_TYPE = '' + SINGLE_SITE_AUTH_HOST = '' + SINGLE_SITE_IMAGE_URL_PREFIX = '' config = ConfigParser.ConfigParser() config.read([Config.IM_PATH + '/../im.cfg', Config.IM_PATH + diff --git a/etc/im.cfg b/etc/im.cfg index 047a67ecb..7fb8e00d7 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -104,6 +104,15 @@ PLAYBOOK_RETRIES = 3 # This are the default values: # PRIVATE_NET_MASKS = 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,100.64.0.0/10,192.0.0.0/24,198.18.0.0/15 +# Flag to use the IM as interface to a single site +SINGLE_SITE = True +# Set the type of the single site +SINGLE_SITE_TYPE = OpenNebula +# Set the host to be used in the auth line of the single site +SINGLE_SITE_AUTH_HOST = http://onedock.i3m.upv.es:2633 +# Set the url prefix of the images of the single site +SINGLE_SITE_IMAGE_URL_PREFIX = one://onedock.i3m.upv.es/ + [OpenNebula] # OpenNebula connector configuration values From 0dcb97f9b78244dd167cf64203644b5162249b10 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 13:20:19 +0200 Subject: [PATCH 401/509] Add IM_SINGLE_SITE_ONE_HOST env variable --- IM/config.py | 5 +++++ README | 5 +++++ README.md | 9 ++++++++- docker/README.md | 9 ++++++++- etc/im.cfg | 6 +++--- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/IM/config.py b/IM/config.py index 7a0e62ac3..7dc7459f8 100644 --- a/IM/config.py +++ b/IM/config.py @@ -108,6 +108,11 @@ class Config: if 'IM_DATA_DB' in os.environ: Config.DATA_DB = os.environ['IM_DATA_DB'] +if 'IM_SINGLE_SITE_ONE_HOST' in os.environ: + Config.SINGLE_SITE = True + Config.SINGLE_SITE_TYPE = 'OpenNebula' + Config.SINGLE_SITE_AUTH_HOST = 'http://%s:2633' % os.environ['IM_SINGLE_SITE_ONE_HOST'] + Config.SINGLE_SITE_IMAGE_URL_PREFIX = 'one://%s/' % os.environ['IM_SINGLE_SITE_ONE_HOST'] class ConfigOpenNebula: TEMPLATE_CONTEXT = '' diff --git a/README b/README index b4eb6029f..d2dd8d82b 100644 --- a/README +++ b/README @@ -207,3 +207,8 @@ default configuration. Information about this image can be found here: https://h How to launch the IM service using docker: $ sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im + +You can use the IM as an entry point of an OpenNebula cloud provider as a TOSCA compliant endpoint for your site: + + $ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_SINGLE_SITE_ONE_HOST=oneserver.com --name im indigodatacloud/im + \ No newline at end of file diff --git a/README.md b/README.md index 1d29938a6..a09fe6ee2 100644 --- a/README.md +++ b/README.md @@ -237,4 +237,11 @@ You can also specify an external MySQL server to store IM data using the IM_DATA ```sh $ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_DATA_DB=mysql://username:password@server/db_name --name im indigodatacloud/im -``` \ No newline at end of file +``` + +You can use the IM as an entry point of an OpenNebula cloud provider as a TOSCA compliant endpoint for your site:: + +```sh +$ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_SINGLE_SITE_ONE_HOST=oneserver.com --name im indigodatacloud/im +``` + \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index d452e717c..e8a067a43 100644 --- a/docker/README.md +++ b/docker/README.md @@ -35,4 +35,11 @@ You can also specify an external MySQL server to store IM data using the IM_DATA ```sh sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_DATA_DB=mysql://username:password@server/db_name --name im indigodatacloud/im -``` \ No newline at end of file +``` + +You can use the IM as an entry point of an OpenNebula cloud provider as a TOSCA compliant endpoint for your site:: + +```sh +$ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_SINGLE_SITE_ONE_HOST=oneserver.com --name im indigodatacloud/im +``` + \ No newline at end of file diff --git a/etc/im.cfg b/etc/im.cfg index 7fb8e00d7..76a50bdf1 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -105,13 +105,13 @@ PLAYBOOK_RETRIES = 3 # PRIVATE_NET_MASKS = 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,100.64.0.0/10,192.0.0.0/24,198.18.0.0/15 # Flag to use the IM as interface to a single site -SINGLE_SITE = True +SINGLE_SITE = False # Set the type of the single site SINGLE_SITE_TYPE = OpenNebula # Set the host to be used in the auth line of the single site -SINGLE_SITE_AUTH_HOST = http://onedock.i3m.upv.es:2633 +SINGLE_SITE_AUTH_HOST = http://server.com:2633 # Set the url prefix of the images of the single site -SINGLE_SITE_IMAGE_URL_PREFIX = one://onedock.i3m.upv.es/ +SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com/ [OpenNebula] # OpenNebula connector configuration values From b76e94dce25256e7e23778965dec2f94bb27ba44 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 15:08:21 +0200 Subject: [PATCH 402/509] Set the TTL_URL based on IM_SINGLE_SITE_ONE_HOST env variable --- IM/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IM/config.py b/IM/config.py index 7dc7459f8..2eff32e3d 100644 --- a/IM/config.py +++ b/IM/config.py @@ -120,5 +120,9 @@ class ConfigOpenNebula: IMAGE_UNAME = '' TTS_URL = 'http://localhost:8080' +# In this case set assume that the TTS server is in the same server +if 'IM_SINGLE_SITE_ONE_HOST' in os.environ: + ConfigOpenNebula.TTS_URL = 'http://%s:8080' % os.environ['IM_SINGLE_SITE_ONE_HOST'] + if config.has_section("OpenNebula"): parse_options(config, 'OpenNebula', ConfigOpenNebula) From 93dd8e1b66d2d9f911f8b0fd17bd57221294b67f Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 15:49:11 +0200 Subject: [PATCH 403/509] Enable to use the standard Bearer auth token in case of single site --- IM/REST.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 3fcbbe773..8b30c6136 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -171,8 +171,17 @@ def get_auth_header(): Get the Authentication object from the AUTHORIZATION header replacing the new line chars. """ - auth_data = bottle.request.headers[ - 'AUTHORIZATION'].replace(AUTH_NEW_LINE_SEPARATOR, "\n") + auth_header = bottle.request.headers['AUTHORIZATION'] + if Config.SINGLE_SITE and auth_header.startswith("Bearer "): + token = auth_header[7:] + im_auth = {"type": "InfrastructureManager", + "username": "user", + "token": token} + single_site_auth = {"type": Config.SINGLE_SITE_TYPE, + "host": Config.SINGLE_SITE_AUTH_HOST, + "token": token} + return Authentication([im_auth, single_site_auth]) + auth_data = auth_header.replace(AUTH_NEW_LINE_SEPARATOR, "\n") auth_data = auth_data.split(AUTH_LINE_SEPARATOR) return Authentication(Authentication.read_auth_data(auth_data)) From cf254daf4e1ee6afbfd89e195d4de32ecb4fae58 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 16:20:48 +0200 Subject: [PATCH 404/509] Bugfix settin the TTL_URL based on IM_SINGLE_SITE_ONE_HOST env variable --- IM/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IM/config.py b/IM/config.py index 2eff32e3d..30eee41f0 100644 --- a/IM/config.py +++ b/IM/config.py @@ -120,9 +120,9 @@ class ConfigOpenNebula: IMAGE_UNAME = '' TTS_URL = 'http://localhost:8080' -# In this case set assume that the TTS server is in the same server -if 'IM_SINGLE_SITE_ONE_HOST' in os.environ: - ConfigOpenNebula.TTS_URL = 'http://%s:8080' % os.environ['IM_SINGLE_SITE_ONE_HOST'] - if config.has_section("OpenNebula"): parse_options(config, 'OpenNebula', ConfigOpenNebula) + +# In this case set assume that the TTS server is in the same server +if 'IM_SINGLE_SITE_ONE_HOST' in os.environ: + ConfigOpenNebula.TTS_URL = 'http://%s:8080' % os.environ['IM_SINGLE_SITE_ONE_HOST'] \ No newline at end of file From 9b0effa58ac7d0534bbc74d4544a9ef0fd2a3f82 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 16:35:50 +0200 Subject: [PATCH 405/509] Style change --- IM/InfrastructureManager.py | 8 ++++---- IM/config.py | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 82a6286a9..42704acd5 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -444,10 +444,10 @@ def AddResource(inf_id, radl_data, auth, context=True, failed_clouds=[]): systems_with_vmrc = {} for system_id in set([d.id for d in radl.deploys if d.vm_number > 0]): s = radl.get_system_by_name(system_id) - + if Config.SINGLE_SITE: image_id = os.path.basename(s.getValue("disk.0.image.url")) - s.setValue("disk.0.image.url",Config.SINGLE_SITE_IMAGE_URL_PREFIX + image_id) + s.setValue("disk.0.image.url", Config.SINGLE_SITE_IMAGE_URL_PREFIX + image_id) if not s.getValue("disk.0.image.url") and len(vmrc_list) == 0: raise Exception( @@ -1238,9 +1238,9 @@ def check_auth_data(auth): if Config.SINGLE_SITE: vmrc_auth = auth.getAuthInfo("VMRC") single_site_auth = auth.getAuthInfo(Config.SINGLE_SITE_TYPE) - + single_site_auth[0]["host"] = Config.SINGLE_SITE_AUTH_HOST - + auth_list = [] auth_list.extend(im_auth) auth_list.extend(vmrc_auth) diff --git a/IM/config.py b/IM/config.py index 30eee41f0..372cb40d1 100644 --- a/IM/config.py +++ b/IM/config.py @@ -114,6 +114,7 @@ class Config: Config.SINGLE_SITE_AUTH_HOST = 'http://%s:2633' % os.environ['IM_SINGLE_SITE_ONE_HOST'] Config.SINGLE_SITE_IMAGE_URL_PREFIX = 'one://%s/' % os.environ['IM_SINGLE_SITE_ONE_HOST'] + class ConfigOpenNebula: TEMPLATE_CONTEXT = '' TEMPLATE_OTHER = 'GRAPHICS = [type="vnc",listen="0.0.0.0"]' @@ -123,6 +124,7 @@ class ConfigOpenNebula: if config.has_section("OpenNebula"): parse_options(config, 'OpenNebula', ConfigOpenNebula) + # In this case set assume that the TTS server is in the same server if 'IM_SINGLE_SITE_ONE_HOST' in os.environ: - ConfigOpenNebula.TTS_URL = 'http://%s:8080' % os.environ['IM_SINGLE_SITE_ONE_HOST'] \ No newline at end of file + ConfigOpenNebula.TTS_URL = 'http://%s:8080' % os.environ['IM_SINGLE_SITE_ONE_HOST'] From 7c15cf78fba516459b0e5bc935fd6e15db0ebebf Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 17:02:11 +0200 Subject: [PATCH 406/509] Return 403 error when the user cannot access the infrastructure --- IM/InfrastructureManager.py | 23 +++++++++++++--------- IM/REST.py | 38 ++++++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index c397b36dd..0d3f6ddf9 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -44,6 +44,13 @@ from multiprocessing.pool import ThreadPool +class UnauthorizedUserException(Exception): + """ Invalid InfrastructureManager credentials to access an infrastructure""" + + def __init__(self, msg="Access to this infrastructure not granted."): + Exception.__init__(self, msg) + + class IncorrectInfrastructureException(Exception): """ Invalid infrastructure ID or access not granted. """ @@ -58,7 +65,7 @@ def __init__(self, msg="Deleted infrastructure."): Exception.__init__(self, msg) -class UnauthorizedUserException(Exception): +class InvaliddUserException(Exception): """ Invalid InfrastructureManager credentials """ def __init__(self, msg="Invalid InfrastructureManager credentials"): @@ -248,16 +255,14 @@ def get_infrastructure(inf_id, auth): """Return infrastructure info with some id if valid authorization provided.""" if inf_id not in InfrastructureManager.infrastructure_list: - InfrastructureManager.logger.error( - "Error, incorrect infrastructure ID") + InfrastructureManager.logger.error("Error, incorrect infrastructure ID") raise IncorrectInfrastructureException() sel_inf = InfrastructureManager.infrastructure_list[inf_id] if not sel_inf.is_authorized(auth): InfrastructureManager.logger.error("Access Error") - raise IncorrectInfrastructureException() + raise UnauthorizedUserException() if sel_inf.deleted: - InfrastructureManager.logger.error( - "Access to a deleted infrastructure.") + InfrastructureManager.logger.error("Access to a deleted infrastructure.") raise DeletedInfrastructureException() return sel_inf @@ -1216,7 +1221,7 @@ def check_iam_token(im_auth): if not success: InfrastructureManager.logger.error( "Incorrect auth token: %s" % userinfo) - raise UnauthorizedUserException("Invalid InfrastructureManager credentials %s" % userinfo) + raise InvaliddUserException("Invalid InfrastructureManager credentials %s" % userinfo) @staticmethod def check_auth_data(auth): @@ -1229,7 +1234,7 @@ def check_auth_data(auth): else: # if not assume the basic user/password auth data if not InfrastructureManager.check_im_user(im_auth): - raise UnauthorizedUserException() + raise InvaliddUserException() # We have to check if TTS is needed for other auth item return auth @@ -1299,7 +1304,7 @@ def GetInfrastructureList(auth): if not auths: InfrastructureManager.logger.error( "No correct auth data has been specified.") - raise UnauthorizedUserException() + raise InvaliddUserException() res = [] for elem in InfrastructureManager.infrastructure_list.values(): diff --git a/IM/REST.py b/IM/REST.py index 3fcbbe773..112222cbc 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -21,7 +21,8 @@ from InfrastructureInfo import IncorrectVMException, DeletedVMException from InfrastructureManager import (InfrastructureManager, DeletedInfrastructureException, - IncorrectInfrastructureException, UnauthorizedUserException) + IncorrectInfrastructureException, UnauthorizedUserException, + InvaliddUserException) from auth import Authentication from config import Config from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json, featuresToSimple, radlToSimple @@ -263,6 +264,8 @@ def RESTDestroyInfrastructure(id=None): return return_error(404, "Error Destroying Inf: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error Destroying Inf: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error Destroying Inf: " + str(ex)) except Exception, ex: logger.exception("Error Destroying Inf") return return_error(400, "Error Destroying Inf: " + str(ex)) @@ -291,6 +294,8 @@ def RESTGetInfrastructureInfo(id=None): return return_error(404, "Error Getting Inf. info: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error Getting Inf. info: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error Getting Inf. info: " + str(ex)) except Exception, ex: logger.exception("Error Getting Inf. info") return return_error(400, "Error Getting Inf. info: " + str(ex)) @@ -336,6 +341,8 @@ def RESTGetInfrastructureProperty(id=None, prop=None): return return_error(404, "Error Getting Inf. prop: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error Getting Inf. prop: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error Getting Inf. prop: " + str(ex)) except Exception, ex: logger.exception("Error Getting Inf. prop") return return_error(400, "Error Getting Inf. prop: " + str(ex)) @@ -360,7 +367,7 @@ def RESTGetInfrastructureList(): protocol + bottle.request.environ['HTTP_HOST'] + "/infrastructures/" + str(inf_id)) return format_output(res, "text/uri-list", "uri-list", "uri") - except UnauthorizedUserException, ex: + except InvaliddUserException, ex: return return_error(401, "Error Getting Inf. List: " + str(ex)) except Exception, ex: logger.exception("Error Getting Inf. List") @@ -407,7 +414,7 @@ def RESTCreateInfrastructure(): "/infrastructures/" + str(inf_id) return format_output(res, "text/uri-list", "uri") - except UnauthorizedUserException, ex: + except InvaliddUserException, ex: return return_error(401, "Error Getting Inf. info: " + str(ex)) except Exception, ex: logger.exception("Error Creating Inf.") @@ -428,6 +435,8 @@ def RESTGetVMInfo(infid=None, vmid=None): return return_error(404, "Error Getting VM. info: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error Getting VM. info: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error Getting VM. info: " + str(ex)) except DeletedVMException, ex: return return_error(404, "Error Getting VM. info: " + str(ex)) except IncorrectVMException, ex: @@ -458,6 +467,8 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): return return_error(404, "Error Getting VM. property: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error Getting VM. property: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error Getting VM. property: " + str(ex)) except DeletedVMException, ex: return return_error(404, "Error Getting VM. property: " + str(ex)) except IncorrectVMException, ex: @@ -528,6 +539,8 @@ def RESTAddResource(id=None): return return_error(404, "Error Adding resources: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error Adding resources: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error Adding resources: " + str(ex)) except Exception, ex: logger.exception("Error Adding resources") return return_error(400, "Error Adding resources: " + str(ex)) @@ -558,6 +571,8 @@ def RESTRemoveResource(infid=None, vmid=None): return return_error(404, "Error Removing resources: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error Removing resources: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error Removing resources: " + str(ex)) except DeletedVMException, ex: return return_error(404, "Error Removing resources: " + str(ex)) except IncorrectVMException, ex: @@ -593,6 +608,8 @@ def RESTAlterVM(infid=None, vmid=None): return return_error(404, "Error modifying resources: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error modifying resources: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error modifying resources: " + str(ex)) except DeletedVMException, ex: return return_error(404, "Error modifying resources: " + str(ex)) except IncorrectVMException, ex: @@ -637,6 +654,8 @@ def RESTReconfigureInfrastructure(id=None): return return_error(404, "Error reconfiguring infrastructure: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error reconfiguring infrastructure: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error reconfiguring infrastructure: " + str(ex)) except Exception, ex: logger.exception("Error reconfiguring infrastructure") return return_error(400, "Error reconfiguring infrastructure: " + str(ex)) @@ -656,6 +675,8 @@ def RESTStartInfrastructure(id=None): return return_error(404, "Error starting infrastructure: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error starting infrastructure: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error starting infrastructure: " + str(ex)) except Exception, ex: logger.exception("Error starting infrastructure") return return_error(400, "Error starting infrastructure: " + str(ex)) @@ -675,6 +696,8 @@ def RESTStopInfrastructure(id=None): return return_error(404, "Error stopping infrastructure: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error stopping infrastructure: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error stopping infrastructure: " + str(ex)) except Exception, ex: logger.exception("Error stopping infrastructure") return return_error(400, "Error stopping infrastructure: " + str(ex)) @@ -694,6 +717,8 @@ def RESTStartVM(infid=None, vmid=None, prop=None): return return_error(404, "Error starting VM: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error starting VM: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error starting VM: " + str(ex)) except DeletedVMException, ex: return return_error(404, "Error starting VM: " + str(ex)) except IncorrectVMException, ex: @@ -717,6 +742,8 @@ def RESTStopVM(infid=None, vmid=None, prop=None): return return_error(404, "Error stopping VM: " + str(ex)) except IncorrectInfrastructureException, ex: return return_error(404, "Error stopping VM: " + str(ex)) + except UnauthorizedUserException, ex: + return return_error(403, "Error stopping VM: " + str(ex)) except DeletedVMException, ex: return return_error(404, "Error stopping VM: " + str(ex)) except IncorrectVMException, ex: @@ -735,6 +762,11 @@ def RESTGeVersion(): return return_error(400, "Error getting IM version: " + str(ex)) +@app.error(403) +def error_mesage_403(error): + return return_error(403, error.body) + + @app.error(404) def error_mesage_404(error): return return_error(404, error.body) From 8e4358b6be65768822466cdf06691ecdd2d5b877 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 8 Sep 2016 17:15:16 +0200 Subject: [PATCH 407/509] Add new test to check 403 error --- test/integration/TestREST.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 3182a92b2..de00d01f0 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -200,6 +200,15 @@ def test_20_create(self): self.assertTrue( all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") + def test_22_get_forbidden_info(self): + self.server.request('GET', "/infrastructures/" + self.inf_id, + headers={'AUTHORIZATION': ("type = InfrastructureManager; " + "username = some; password = other")}) + resp = self.server.getresponse() + resp.read() + self.assertEqual(resp.status, 403, + msg="Incorrect error message: " + str(resp.status)) + def test_30_get_vm_info(self): self.server.request('GET', "/infrastructures/" + self.inf_id, headers={'AUTHORIZATION': self.auth_data}) From 4f7f8f4c1987b85c52b3b3096c70e1b24d47ad38 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 9 Sep 2016 08:12:28 +0200 Subject: [PATCH 408/509] Fix tests with new 403 error --- test/unit/test_im_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index a53449bbd..d52473e57 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -170,11 +170,11 @@ def test_inf_auth(self): with self.assertRaises(Exception) as ex: IM.DestroyInfrastructure(infId0, auth1) self.assertEqual(str(ex.exception), - "Invalid infrastructure ID or access not granted.") + "Access to this infrastructure not granted.") with self.assertRaises(Exception) as ex: IM.DestroyInfrastructure(infId1, auth0) self.assertEqual(str(ex.exception), - "Invalid infrastructure ID or access not granted.") + "Access to this infrastructure not granted.") IM.DestroyInfrastructure(infId0, auth0) IM.DestroyInfrastructure(infId1, auth1) From a1cdce47374f008062a8e0d62a8a171648af26a7 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 12 Sep 2016 12:36:05 +0200 Subject: [PATCH 409/509] Add support for basic auth header --- IM/REST.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index 0d5e308d0..7b9bd065b 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -18,6 +18,7 @@ import threading import bottle import json +import base64 from InfrastructureInfo import IncorrectVMException, DeletedVMException from InfrastructureManager import (InfrastructureManager, DeletedInfrastructureException, @@ -173,7 +174,19 @@ def get_auth_header(): replacing the new line chars. """ auth_header = bottle.request.headers['AUTHORIZATION'] - if Config.SINGLE_SITE and auth_header.startswith("Bearer "): + if Config.SINGLE_SITE: + if auth_header.startswith("Basic "): + auth_data = base64.b64decode(auth_header[6:]) + user_pass = auth_data.split(":") + im_auth = {"type": "InfrastructureManager", + "username": user_pass[0], + "password": user_pass[1]} + single_site_auth = {"type": Config.SINGLE_SITE_TYPE, + "host": Config.SINGLE_SITE_AUTH_HOST, + "username": user_pass[0], + "password": user_pass[1]} + return Authentication([im_auth, single_site_auth]) + elif auth_header.startswith("Bearer "): token = auth_header[7:] im_auth = {"type": "InfrastructureManager", "username": "user", From 176db4376d395cfd311fbf3e05d09b2a0c5aad1a Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 12 Sep 2016 17:50:21 +0200 Subject: [PATCH 410/509] Improve install recipe --- ansible_install.yaml | 42 ++++++++++++++++-------------- contextualization/conf-ansible.yml | 18 ++++++------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index 05645e2d0..977b5b70d 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -5,31 +5,35 @@ action: yum pkg=epel-release state=installed when: ansible_os_family == "RedHat" - - name: Yum install requisites - action: yum pkg=git,gcc,python-devel,python-pip,SOAPpy,python-requests,MySQL-python,libffi-devel,openssl-devel,unzip state=installed + - name: Install libselinux-python in RH + action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" - - name: Apt-get install requisites - apt: pkg=git,python-pip,python-dev,python-soappy,python-mysqldb,libssl-dev,libffi-dev,unzip state=installed update_cache=yes cache_valid_time=3600 - when: ansible_os_family == "Debian" - - - name: pip upgrade setuptools - pip: name=setuptools extra_args="-I" state=latest - - - name: pip install pbr,CherryPy and pyOpenSSL to enable HTTPS in REST API - pip: name="pbr CherryPy pyOpenSSL" extra_args="-I" state=latest + - name: Ubuntu install indigo list + get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list dest=/etc/apt/sources.list.d/indigo1-ubuntu14_04.list + when: ansible_distribution == "Ubuntu" - - name: Download tosca-parser - git: repo=https://github.com/indigo-dc/tosca-parser dest=/tmp/tosca-parser + - apt_key: url=http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc state=present + when: ansible_distribution == "Ubuntu" + + - name: Apt-get update + apt: update_cache=yes + when: ansible_os_family == "Debian" - - name: pip install tosca-parser - pip: name=/tmp/tosca-parser + - name: Ubuntu install Ansible with apt + apt: name=python-im,ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip force=yes + when: ansible_distribution == "Ubuntu" + + - name: RH indigo repos + get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1.repo dest=/etc/yum.repos.d/indigo1.repo + when: ansible_os_family == "RedHat" - - name: Download IM - git: repo=https://github.com/indigo-dc/im dest=/tmp/im + - rpm_key: state=present key=http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc + when: ansible_os_family == "RedHat" - - name: pip install IM - pip: name=/tmp/im + - name: RH7 install Ansible with yum + yum: name=IM,ansible,python-pip,python-jinja2,sshpass,openssh-clients,unzip + when: ansible_os_family == "RedHat" ################################################ Configure Ansible ################################################### diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index 18f213020..e963a1a98 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -16,12 +16,12 @@ ################# Use INDIGO repos from Ubuntu 14 and CentOS 7 to assure a stable version ############################ - name: Ubuntu install indigo list - get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/apt/sources.list.d/{{item}} + get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list dest=/etc/apt/sources.list.d/indigo1-ubuntu14_04.list when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" - with_items: - - indigo1-testing-ubuntu14_04.list - - indigo1-ubuntu14_04.list - + + - apt_key: url=http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc state=present + when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" + - name: Ubuntu install requirements apt: name=software-properties-common when: ansible_os_family == "Debian" and (ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14") @@ -39,10 +39,10 @@ when: ansible_distribution == "Ubuntu" - name: RH indigo repos - get_url: url=http://repo.indigo-datacloud.eu/repos/1/{{item}} dest=/etc/yum.repos.d/{{item}} - with_items: - - indigo1.repo - - indigo1-testing.repo + get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1.repo dest=/etc/yum.repos.d/indigo1.repo + when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" + + - rpm_key: state=present key=http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7 and ansible_distribution != "Fedora" - name: RH7 install Ansible with yum From 851fd8dbeafbf815f3b1dddf781f932074ae0ec1 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 12 Sep 2016 18:29:01 +0200 Subject: [PATCH 411/509] Bugfix --- test/files/tosca_long.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/files/tosca_long.yml b/test/files/tosca_long.yml index 44d482076..b747bcaff 100644 --- a/test/files/tosca_long.yml +++ b/test/files/tosca_long.yml @@ -19,6 +19,7 @@ topology_template: deployment_id: { token: [ get_attribute: [ lrms_server, public_address, 0 ], ':', 0 ] } # fake value to test concat intrinsic functions orchestrator_url: { concat: [ 'http://', get_attribute: [ lrms_server, public_address, 0 ], ':8080' ] } + iam_access_token: iam_access_token requirements: - lrms: lrms_front_end - wn: wn_node From b3bf5cf1dc23ce2acbb9396b112023efcdb8b526 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 15 Sep 2016 10:05:16 +0200 Subject: [PATCH 412/509] Add support to recive a incomplete TOSCA doc in AddResource --- IM/REST.py | 2 ++ IM/tosca/Tosca.py | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 7b9bd065b..32acde190 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -531,6 +531,8 @@ def RESTAddResource(id=None): auth = InfrastructureManager.check_auth_data(auth) sel_inf = InfrastructureManager.get_infrastructure(id, auth) remove_list, radl_data = tosca_data.to_radl(sel_inf) + if sel_inf.extra_info['TOSCA']: + tosca_data = sel_inf.extra_info['TOSCA'].merge(tosca_data) elif "text/plain" in content_type or "*/*" in content_type or "text/*" in content_type: content_type = "text/plain" else: diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 55c4e2141..bd1f25c13 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -27,12 +27,8 @@ class Tosca: logger = logging.getLogger('InfrastructureManager') def __init__(self, yaml_str): - self.tosca = None - # write the contents to a file as ToscaTemplate needs - with tempfile.NamedTemporaryFile(suffix=".yaml") as f: - f.write(yaml_str) - f.flush() - self.tosca = ToscaTemplate(f.name) + self.yaml = yaml.load(yaml_str) + self.tosca = ToscaTemplate(yaml_dict_tpl=copy.deepcopy(self.yaml)) def to_radl(self, inf_info=None): """ @@ -469,7 +465,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): # Merge the main recipe with the other yaml files for recipe in recipe_list: - recipes = Tosca._merge_yaml(recipes, recipe) + recipes = Tosca._merge_recipes(recipes, recipe) return configure(name, recipes) else: @@ -1268,9 +1264,9 @@ def _get_interfaces(node): return interfaces @staticmethod - def _merge_yaml(yaml1, yaml2): + def _merge_recipes(yaml1, yaml2): """ - Merge two ansible yaml docs + Merge two ansible recipes yaml docs Arguments: - yaml1(str): string with the first YAML @@ -1335,3 +1331,26 @@ def get_outputs(self, inf_info): res[output.name] = val return res + + def merge(self, other_tosca): + Tosca._merge_yaml(self.yaml,other_tosca.yaml) + self.tosca = ToscaTemplate(yaml_dict_tpl=copy.deepcopy(self.yaml)) + + @staticmethod + def _merge_yaml(yaml1, yaml2): + if yaml2 is None: + return yaml1 + elif isinstance(yaml1, dict) and isinstance(yaml2, dict): + for k,v in yaml2.iteritems(): + if k not in yaml1: + yaml1[k] = v + else: + yaml1[k] = Tosca._merge_yaml(yaml1[k], v) + elif isinstance(yaml1, list) and isinstance(yaml2, (list, tuple)): + for v in yaml2: + if v not in yaml1: + yaml1.append(v) + else: + yaml1 = yaml2 + + return yaml1 \ No newline at end of file From 205b2092e1e7d91e11eb7e69318cd98bad7ec4cf Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 15 Sep 2016 10:55:37 +0200 Subject: [PATCH 413/509] Bugfixes --- IM/REST.py | 3 ++- IM/tosca/Tosca.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index 32acde190..e0a4a6772 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -530,9 +530,10 @@ def RESTAddResource(id=None): tosca_data = Tosca(radl_data) auth = InfrastructureManager.check_auth_data(auth) sel_inf = InfrastructureManager.get_infrastructure(id, auth) - remove_list, radl_data = tosca_data.to_radl(sel_inf) + # merge the current TOSCA with the new one if sel_inf.extra_info['TOSCA']: tosca_data = sel_inf.extra_info['TOSCA'].merge(tosca_data) + remove_list, radl_data = tosca_data.to_radl(sel_inf) elif "text/plain" in content_type or "*/*" in content_type or "text/*" in content_type: content_type = "text/plain" else: diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bd1f25c13..6b94cf643 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -1335,6 +1335,7 @@ def get_outputs(self, inf_info): def merge(self, other_tosca): Tosca._merge_yaml(self.yaml,other_tosca.yaml) self.tosca = ToscaTemplate(yaml_dict_tpl=copy.deepcopy(self.yaml)) + return self @staticmethod def _merge_yaml(yaml1, yaml2): From 5d68eb31cbce0e5750ae549c5774acef4b1654b8 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 15 Sep 2016 11:55:35 +0200 Subject: [PATCH 414/509] Bugfixes --- IM/REST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index e0a4a6772..f5bc590e0 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -531,7 +531,7 @@ def RESTAddResource(id=None): auth = InfrastructureManager.check_auth_data(auth) sel_inf = InfrastructureManager.get_infrastructure(id, auth) # merge the current TOSCA with the new one - if sel_inf.extra_info['TOSCA']: + if isinstance(sel_inf.extra_info['TOSCA'], Tosca): tosca_data = sel_inf.extra_info['TOSCA'].merge(tosca_data) remove_list, radl_data = tosca_data.to_radl(sel_inf) elif "text/plain" in content_type or "*/*" in content_type or "text/*" in content_type: From af714ab9e63a9f7f2dfbc865897de5cc5f73f692 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 15 Sep 2016 11:57:53 +0200 Subject: [PATCH 415/509] Style changes --- IM/tosca/Tosca.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 6b94cf643..36edf2d81 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -1333,7 +1333,7 @@ def get_outputs(self, inf_info): return res def merge(self, other_tosca): - Tosca._merge_yaml(self.yaml,other_tosca.yaml) + Tosca._merge_yaml(self.yaml, other_tosca.yaml) self.tosca = ToscaTemplate(yaml_dict_tpl=copy.deepcopy(self.yaml)) return self @@ -1342,7 +1342,7 @@ def _merge_yaml(yaml1, yaml2): if yaml2 is None: return yaml1 elif isinstance(yaml1, dict) and isinstance(yaml2, dict): - for k,v in yaml2.iteritems(): + for k, v in yaml2.iteritems(): if k not in yaml1: yaml1[k] = v else: @@ -1354,4 +1354,4 @@ def _merge_yaml(yaml1, yaml2): else: yaml1 = yaml2 - return yaml1 \ No newline at end of file + return yaml1 From e5b04fe07a0916b14dd63757c426c80b9b7f3d63 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 18 Oct 2016 08:42:33 +0200 Subject: [PATCH 416/509] Style changes --- test/integration/TestREST.py | 152 +++++++++++++++++------------------ 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 26e617fcd..d9fc9a6dd 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -60,8 +60,8 @@ def tearDownClass(cls): # Assure that the infrastructure is destroyed try: server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('DELETE', "/infrastructures/" + - cls.inf_id, headers={'Authorization': cls.auth_data}) + server.request('DELETE', "/infrastructures/" + cls.inf_id, + headers={'Authorization': cls.auth_data}) server.getresponse() server.close() except Exception: @@ -74,7 +74,7 @@ def wait_inf_state(self, state, timeout, incorrect_states=[], vm_ids=None): if not vm_ids: server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -107,7 +107,7 @@ def wait_inf_state(self, state, timeout, incorrect_states=[], vm_ids=None): if vm_state == VirtualMachine.UNCONFIGURED: server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id + "/contmsg", - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -143,7 +143,7 @@ def test_05_version(self): def test_10_list(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures", - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -172,7 +172,7 @@ def test_12_list_with_incorrect_token(self): def test_15_get_incorrect_info(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/999999", - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() resp.read() server.close() @@ -181,8 +181,8 @@ def test_15_get_incorrect_info(self): def test_16_get_incorrect_info_json(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', "/infrastructures/999999", headers={ - 'AUTHORIZATION': self.auth_data, 'Accept': 'application/json'}) + server.request('GET', "/infrastructures/999999", + headers={'AUTHORIZATION': self.auth_data, 'Accept': 'application/json'}) resp = server.getresponse() output = resp.read() server.close() @@ -206,7 +206,7 @@ def test_20_create(self): radl = read_file_as_string('../files/test_simple.radl') server.request('POST', "/infrastructures", body=radl, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -222,46 +222,46 @@ def test_20_create(self): def test_22_get_forbidden_info(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': ("type = InfrastructureManager; " - "username = some; password = other")}) + headers={'AUTHORIZATION': ("type = InfrastructureManager; " + "username = some; password = other")}) resp = server.getresponse() resp.read() server.close() self.assertEqual(resp.status, 403, msg="Incorrect error message: " + str(resp.status)) - + def test_30_get_vm_info(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure info:" + output) vm_ids = output.split("\n") - + vm_uri = uriparse(vm_ids[0]) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', vm_uri[2], headers={ - 'AUTHORIZATION': self.auth_data}) + server.request('GET', vm_uri[2], + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR getting VM info:" + output) - + def test_32_get_vm_contmsg(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure info:" + output) vm_ids = output.split("\n") - + vm_uri = uriparse(vm_ids[0]) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request( @@ -273,11 +273,11 @@ def test_32_get_vm_contmsg(self): msg="ERROR getting VM contmsg:" + output) self.assertEqual( len(output), 0, msg="Incorrect VM contextualization message: " + output) - + def test_33_get_contmsg(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', "/infrastructures/" + self.inf_id + - "/contmsg", headers={'AUTHORIZATION': self.auth_data}) + server.request('GET', "/infrastructures/" + self.inf_id + "/contmsg", + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -285,11 +285,11 @@ def test_33_get_contmsg(self): msg="ERROR getting the infrastructure info:" + output) self.assertGreater( len(output), 30, msg="Incorrect contextualization message: " + output) - + def test_34_get_radl(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', "/infrastructures/" + self.inf_id + - "/radl", headers={'AUTHORIZATION': self.auth_data}) + server.request('GET', "/infrastructures/" + self.inf_id + "/radl", + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -300,18 +300,18 @@ def test_34_get_radl(self): except Exception, ex: self.assertTrue( False, msg="ERROR parsing the RADL returned by GetInfrastructureRADL: " + str(ex)) - + def test_35_get_vm_property(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure info:" + output) vm_ids = output.split("\n") - + vm_uri = uriparse(vm_ids[0]) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request( @@ -321,20 +321,20 @@ def test_35_get_vm_property(self): server.close() self.assertEqual(resp.status, 200, msg="ERROR getting VM property:" + output) - + def test_40_addresource(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('POST', "/infrastructures/" + self.inf_id, - body=RADL_ADD, headers={'AUTHORIZATION': self.auth_data}) + body=RADL_ADD, headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) - + server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -346,11 +346,11 @@ def test_40_addresource(self): all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue( all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - + def test_45_getstate(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', "/infrastructures/" + self.inf_id + - "/state", headers={'AUTHORIZATION': self.auth_data}) + server.request('GET', "/infrastructures/" + self.inf_id + "/state", + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -364,31 +364,31 @@ def test_45_getstate(self): for vm_id, vm_state in vm_states.iteritems(): self.assertEqual(vm_state, "configured", msg="Unexpected vm state: " + vm_state + " in VM ID " + str(vm_id) + ". It must be 'configured'.") - + def test_46_removeresource(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure info:" + output) vm_ids = output.split("\n") - + vm_uri = uriparse(vm_ids[1]) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('DELETE', vm_uri[2], headers={ - 'AUTHORIZATION': self.auth_data}) + server.request('DELETE', vm_uri[2], + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR removing resources:" + output) - + server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -397,45 +397,45 @@ def test_46_removeresource(self): vm_ids = output.split("\n") self.assertEqual(len(vm_ids), 1, msg=("ERROR getting infrastructure info: Incorrect number of VMs(" + str(len(vm_ids)) + "). It must be 1")) - + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 300) self.assertTrue( all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - + def test_47_addresource_noconfig(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('POST', "/infrastructures/" + self.inf_id + "?context=0", - body=RADL_ADD, headers={'AUTHORIZATION': self.auth_data}) + body=RADL_ADD, headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) - + def test_50_removeresource_noconfig(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', "/infrastructures/" + self.inf_id + - "?context=0", headers={'AUTHORIZATION': self.auth_data}) + server.request('GET', "/infrastructures/" + self.inf_id + "?context=0", + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR getting the infrastructure info:" + output) vm_ids = output.split("\n") - + vm_uri = uriparse(vm_ids[1]) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('DELETE', vm_uri[2], headers={ - 'AUTHORIZATION': self.auth_data}) + server.request('DELETE', vm_uri[2], + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR removing resources:" + output) - + server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -444,29 +444,29 @@ def test_50_removeresource_noconfig(self): vm_ids = output.split("\n") self.assertEqual(len(vm_ids), 1, msg=("ERROR getting infrastructure info: Incorrect number of VMs(" + str(len(vm_ids)) + "). It must be 1")) - + def test_55_reconfigure(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('PUT', "/infrastructures/" + self.inf_id + - "/reconfigure", headers={'AUTHORIZATION': self.auth_data}) + server.request('PUT', "/infrastructures/" + self.inf_id + "/reconfigure", + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR reconfiguring:" + output) - + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 300) self.assertTrue( all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") - + def test_57_reconfigure_list(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('PUT', "/infrastructures/" + self.inf_id + - "/reconfigure?vm_list=0", headers={'AUTHORIZATION': self.auth_data}) + server.request('PUT', "/infrastructures/" + self.inf_id + "/reconfigure?vm_list=0", + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() self.assertEqual(resp.status, 200, msg="ERROR reconfiguring:" + output) - + all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 300) self.assertTrue( all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") @@ -475,7 +475,7 @@ def test_60_stop(self): time.sleep(10) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('PUT', "/infrastructures/" + self.inf_id + "/stop", - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -493,7 +493,7 @@ def test_70_start(self): time.sleep(10) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('PUT', "/infrastructures/" + self.inf_id + "/start", - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -510,7 +510,7 @@ def test_80_stop_vm(self): time.sleep(10) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('PUT', "/infrastructures/" + self.inf_id + "/vms/0/stop", - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -528,7 +528,7 @@ def test_90_start_vm(self): time.sleep(10) server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('PUT', "/infrastructures/" + self.inf_id + "/vms/0/start", - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -543,8 +543,8 @@ def test_90_start_vm(self): def test_92_destroy(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('DELETE', "/infrastructures/" + - self.inf_id, headers={'Authorization': self.auth_data}) + server.request('DELETE', "/infrastructures/" + self.inf_id, + headers={'Authorization': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -559,7 +559,7 @@ def test_93_create_tosca(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('POST', "/infrastructures", body=tosca, - headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) + headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -574,8 +574,8 @@ def test_93_create_tosca(self): def test_94_get_outputs(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', "/infrastructures/" + self.inf_id + - "/outputs", headers={'Authorization': self.auth_data}) + server.request('GET', "/infrastructures/" + self.inf_id + "/outputs", + headers={'Authorization': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -594,7 +594,7 @@ def test_95_add_tosca(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, - headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) + headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -603,7 +603,7 @@ def test_95_add_tosca(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -624,7 +624,7 @@ def test_96_remove_tosca(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('POST', "/infrastructures/" + self.inf_id, body=tosca, - headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) + headers={'AUTHORIZATION': self.auth_data, 'Content-Type': 'text/yaml'}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -633,7 +633,7 @@ def test_96_remove_tosca(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures/" + self.inf_id, - headers={'AUTHORIZATION': self.auth_data}) + headers={'AUTHORIZATION': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() @@ -648,8 +648,8 @@ def test_96_remove_tosca(self): def test_98_destroy(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('DELETE', "/infrastructures/" + - self.inf_id, headers={'Authorization': self.auth_data}) + server.request('DELETE', "/infrastructures/" + self.inf_id, + headers={'Authorization': self.auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() From 3a2bc293364934f3670080930024baca1256c826 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 18 Oct 2016 09:37:24 +0200 Subject: [PATCH 417/509] Remove AlterVM test --- test/integration/TestIM.py | 40 -------------------------------------- 1 file changed, 40 deletions(-) diff --git a/test/integration/TestIM.py b/test/integration/TestIM.py index 5a68b9106..95430f9b3 100755 --- a/test/integration/TestIM.py +++ b/test/integration/TestIM.py @@ -227,46 +227,6 @@ def test_18_error_addresource(self): self.assertGreater( pos, -1, msg="Incorrect RADL in AddResource not returned the expected error: " + res) - def test_18_altervm(self): - """ - Test AlterVM function - """ - - alter_radl = """ - system wn ( - cpu.count>=2 and - memory.size>=1024m and - disk.1.size=1GB and - disk.1.device='hdb' and - disk.1.fstype='ext4' and - disk.1.mount_path='/mnt/disk' and - disk.2.size=1GB and - disk.2.device='hdb' and - disk.2.fstype='ext4' and - disk.2.mount_path='/mnt/disk2' - ) - """ - (success, res) = self.server.AlterVM( - self.inf_id, 1, alter_radl, self.auth_data) - self.assertTrue(success, msg="ERROR calling AlterVM: " + str(res)) - - (success, info) = self.server.GetVMInfo( - self.inf_id, 1, self.auth_data) - self.assertTrue(success, msg="ERROR calling GetVMInfo: " + str(info)) - try: - radl_res = radl_parse.parse_radl(info) - except Exception, ex: - self.assertTrue( - False, msg="ERROR parsing the RADL returned by GetVMInfo: " + str(ex)) - - new_cpu = radl_res.systems[0].getValue('cpu.count') - new_mem = radl_res.systems[0].getValue('memory.size') - new_disk = radl_res.systems[0].getValue('disk.2.provider_id') - - self.assertEqual(new_cpu, 2, msg="Incorrect number of CPUs (%d) it must be 2." % new_cpu) - self.assertEqual(new_mem, 1073741824, msg="Incorrect ammount of memory (%d) it must be 1024." % new_mem) - self.assertNotEqual(new_disk, None, msg="Disk 2 without provider id") - def test_19_addresource(self): """ Test AddResource function From 1f1cb1c6622c12b890fc68f1aed2463352145706 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 18 Oct 2016 10:51:57 +0200 Subject: [PATCH 418/509] Bugfix in test --- test/integration/TestREST.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index d9fc9a6dd..6bb0bb18d 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -162,10 +162,12 @@ def test_12_list_with_incorrect_token(self): if line.find("type = InfrastructureManager") == -1: auth_data += line.strip() + "\\n" - self.server.request('GET', "/infrastructures", + server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) + server.request('GET', "/infrastructures", headers={'AUTHORIZATION': auth_data}) - resp = self.server.getresponse() + resp = server.getresponse() output = str(resp.read()) + server.close() self.assertEqual(resp.status, 401, msg="ERROR using an invalid token. A 401 error is expected:" + output) From a6da368f9b1e512902c09480621c1f19e81b00f3 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 18 Oct 2016 10:53:21 +0200 Subject: [PATCH 419/509] Style changes --- test/integration/TestREST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 6bb0bb18d..3a979098e 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -164,7 +164,7 @@ def test_12_list_with_incorrect_token(self): server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) server.request('GET', "/infrastructures", - headers={'AUTHORIZATION': auth_data}) + headers={'AUTHORIZATION': auth_data}) resp = server.getresponse() output = str(resp.read()) server.close() From 3d3d55df6c1b78334fc9bab5ba2afd695f4f0f01 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 19 Oct 2016 10:30:52 +0200 Subject: [PATCH 420/509] Add support to source_range --- IM/tosca/Tosca.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 36edf2d81..e7fe4de6a 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -143,20 +143,30 @@ def _get_num_instances(self, sys_name, inf_info): def _format_outports(ports_dict): res = "" for port in ports_dict.values(): - # TODO: format ranges protocol = "tcp" + source_range = None if "protocol" in port: protocol = port["protocol"] - if "source" in port: - remote_port = port["source"] - if "target" in port: - local_port = port["target"] + if "source_range" in port: + source_range = port["source_range"] else: - local_port = remote_port - - if res: - res += "," - res += "%s/%s-%s/%s" % (remote_port, protocol, local_port, protocol) + if "source" in port: + remote_port = port["source"] + if "target" in port: + local_port = port["target"] + else: + local_port = remote_port + + # In case of source_range do not use port mapping only direct ports + if source_range: + for port_in_range in range(source_range[0], source_range[1]): + if res: + res += "," + res += "%s" % (port_in_range) + else: + if res: + res += "," + res += "%s/%s-%s/%s" % (remote_port, protocol, local_port, protocol) return res From f019f73867c616fc078fde123eeb358536172506 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Thu, 20 Oct 2016 18:47:37 +0200 Subject: [PATCH 421/509] Solve pickle OpenStack volumes --- IM/VirtualMachine.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IM/VirtualMachine.py b/IM/VirtualMachine.py index 08572c91e..95d2c5021 100644 --- a/IM/VirtualMachine.py +++ b/IM/VirtualMachine.py @@ -90,6 +90,9 @@ def __getstate__(self): # Quit the lock to the data to be store by pickle del odict['_lock'] del odict['cloud_connector'] + # To avoid some problems with openstack volumes + if 'volumes' in odict: + del odict['volumes'] return odict def __setstate__(self, dic): From eaee0d08fdf6937bb82e433d4a34b4b1083ad338 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 24 Oct 2016 09:18:57 +0200 Subject: [PATCH 422/509] Order deploys --- IM/tosca/Tosca.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index e7fe4de6a..93e3593d5 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -124,8 +124,26 @@ def to_radl(self, inf_info=None): # If there are no configures, disable contextualization radl.contextualize = contextualize({}) + self._order_deploys(radl) + return all_removal_list, self._complete_radl_networks(radl) + def _order_deploys(self, radl): + """ + Order the RADL deploys to assure VMs with Public IPs a set a the beginning + (to avoid problems with cluster configuration) + """ + pub = [] + priv = [] + for d in radl.deploys: + if radl.hasPublicNet(d.id): + pub.append(d) + else: + priv.append(d) + + radl.deploys = pub + priv + + def _get_num_instances(self, sys_name, inf_info): """ Get the current number of instances of system type name sys_name From f6deb4a712e9317b815c767886592671dbe7f5ec Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 24 Oct 2016 09:33:21 +0200 Subject: [PATCH 423/509] use extract function --- IM/tosca/Tosca.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 93e3593d5..93bc5d9ce 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -763,17 +763,10 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "private_address": if node.type == "tosca.nodes.indigo.Compute": # This only works with Ansible 2.1, wait for it to be released - # return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PRIVATE_IP')|list }}" % node.name if index is not None: return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PRIVATE_IP'] }}" % (node.name, index) else: - return ("""|\n""" - """ {%% if '%s' in groups %%}""" - """{%% set comma = joiner(",") %%}""" - """[{%% for host in groups['%s'] %%}""" - """{{ comma() }}"{{ hostvars[host]['IM_NODE_PRIVATE_IP'] }}" """ - """{%% endfor %%} ]""" - """{%% else %%}[]{%% endif %%}""" % (node.name, node.name)) + return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PRIVATE_IP')|list }}" % node.name else: if node_name in ["HOST", "SELF"]: return "{{ IM_NODE_PRIVATE_IP }}" @@ -782,17 +775,10 @@ def _get_attribute_result(self, func, node, inf_info): elif attribute_name == "public_address": if node.type == "tosca.nodes.indigo.Compute": # This only works with Ansible 2.1, wait for it to be released - # return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PUBLIC_IP')|list }}" % node.name if index is not None: return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PUBLIC_IP'] }}" % (node.name, index) else: - return ("""|\n""" - """ {%% if '%s' in groups %%}""" - """{%% set comma = joiner(",") %%}""" - """[{%% for host in groups['%s'] %%}""" - """{{ comma() }}"{{ hostvars[host]['IM_NODE_PUBLIC_IP'] }}" """ - """{%% endfor %%} ]""" - """{%% else %%}[]{%% endif %%}""" % (node.name, node.name)) + return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PUBLIC_IP')|list }}" % node.name else: if node_name in ["HOST", "SELF"]: return "{{ IM_NODE_PUBLIC_IP }}" From f89ced60ac9544b3d9d975f29e51fb6c15015daf Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 24 Oct 2016 17:42:14 +0200 Subject: [PATCH 424/509] Add log message to show TOSCA doc --- IM/tosca/Tosca.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 93bc5d9ce..824ce5ab8 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -27,6 +27,7 @@ class Tosca: logger = logging.getLogger('InfrastructureManager') def __init__(self, yaml_str): + Tosca.logger.debug("TOSCA: %s" % yaml_str) self.yaml = yaml.load(yaml_str) self.tosca = ToscaTemplate(yaml_dict_tpl=copy.deepcopy(self.yaml)) From 8bb87af90429aa18266b8a9bd0a2aad50341ef51 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 25 Oct 2016 08:50:32 +0200 Subject: [PATCH 425/509] Style changes --- IM/tosca/Tosca.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 824ce5ab8..544d0648e 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -141,9 +141,8 @@ def _order_deploys(self, radl): pub.append(d) else: priv.append(d) - + radl.deploys = pub + priv - def _get_num_instances(self, sys_name, inf_info): """ From 763a3f92fd1d4dd74fb7a6325c2d2c023883f792 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 25 Oct 2016 15:56:37 +0200 Subject: [PATCH 426/509] Enable to not specify disk device --- IM/connectors/OpenNebula.py | 2 +- IM/tosca/Tosca.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 6a4f3bb58..31d404154 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -485,7 +485,7 @@ def getONETemplate(self, radl, auth_data): while system.getValue("disk." + str(cont) + ".image.url") or system.getValue("disk." + str(cont) + ".size"): disk_image = system.getValue("disk." + str(cont) + ".image.url") if disk_image: - disks += '\nDISK = [ IMAGE_ID = "%s" ]\n' % uriparse(disk_image)[ + disks += 'DISK = [ IMAGE_ID = "%s" ]\n' % uriparse(disk_image)[ 2][1:] else: disk_size = system.getFeature( diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 544d0648e..789ae3024 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -718,7 +718,9 @@ def _get_attribute_result(self, func, node, inf_info): res = res[index] return res else: - return vm.getPrivateIP() + Tosca.logger.warn("Attribute credential of capability endpoint only" + " supported in tosca.nodes.indigo.Compute nodes.") + return None elif attribute_name == "private_address": if node.type == "tosca.nodes.indigo.Compute": res = [vm.getPrivateIP() for vm in vm_list[node.name]] @@ -1204,7 +1206,7 @@ def _get_attached_disks(node, nodetemplates): size = None location = None # set a default device - device = "hdb" + device = None for prop in props: if prop.name == "location": From 92f9caf9ee253c1ffdbcddf894e58b5d9e5af88b Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 27 Oct 2016 08:53:42 +0200 Subject: [PATCH 427/509] Bugfix --- IM/tosca/Tosca.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 789ae3024..b1aa32276 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -768,7 +768,8 @@ def _get_attribute_result(self, func, node, inf_info): if index is not None: return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PRIVATE_IP'] }}" % (node.name, index) else: - return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PRIVATE_IP')|list }}" % node.name + return ("{{ groups['%s']|map('extract', hostvars,'IM_NODE_PRIVATE_IP')|list" + " if '%s' in groups else []}}" % (node.name, node.name)) else: if node_name in ["HOST", "SELF"]: return "{{ IM_NODE_PRIVATE_IP }}" @@ -780,7 +781,8 @@ def _get_attribute_result(self, func, node, inf_info): if index is not None: return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PUBLIC_IP'] }}" % (node.name, index) else: - return "{{ groups['%s']|map('extract', hostvars,'IM_NODE_PUBLIC_IP')|list }}" % node.name + return ("{{ groups['%s']|map('extract', hostvars,'IM_NODE_PUBLIC_IP')|list" + " if '%s' in groups else []}}" % (node.name, node.name)) else: if node_name in ["HOST", "SELF"]: return "{{ IM_NODE_PUBLIC_IP }}" From c86401849453d63b7c9ba4f942b461f71899adc6 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 28 Oct 2016 11:50:42 +0200 Subject: [PATCH 428/509] Improve _get_dependency_level function --- IM/tosca/Tosca.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index b1aa32276..43fcfddaa 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -978,10 +978,14 @@ def _get_dependency_level(node): """ Check the relations to get the contextualization level """ - if node.related_nodes: + if node.requirements: maxl = 0 - for node_depend in node.related_nodes: - level = Tosca._get_dependency_level(node_depend) + for r, n in node.relationships.iteritems(): + if Tosca._is_derived_from(r, r.HOSTEDON): + level = Tosca._get_dependency_level(n) + else: + level = 0 + if level > maxl: maxl = level return maxl + 1 From 0cc1688b9d26babea93a209ae3667c7da3f77b02 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 31 Oct 2016 15:16:22 +0100 Subject: [PATCH 429/509] Bugfix --- IM/tosca/Tosca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 43fcfddaa..a8f0ab08a 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -402,7 +402,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): val = self._final_function_result( param_value, node) - if val: + if val is not None: env[param_name] = val else: raise Exception("input value for %s in interface %s of node %s not valid" % ( From 309cd9a7cd883685370fa1cbc2aac089b15bd88c Mon Sep 17 00:00:00 2001 From: alpegon Date: Fri, 4 Nov 2016 13:28:45 +0100 Subject: [PATCH 430/509] Update gitbook documentation --- README.md | 223 +----------------- SUMMARY.md | 7 + doc/gitbook/docker-image.md | 22 ++ doc/gitbook/installation.md | 193 ++++++++++++++++ doc/gitbook/rest-api.md | 374 +++++++++++++++++++++++++++++++ doc/gitbook/service-reference.md | 49 ++++ 6 files changed, 647 insertions(+), 221 deletions(-) create mode 100644 SUMMARY.md create mode 100644 doc/gitbook/docker-image.md create mode 100644 doc/gitbook/installation.md create mode 100644 doc/gitbook/rest-api.md create mode 100644 doc/gitbook/service-reference.md diff --git a/README.md b/README.md index c7e9446c6..32fb63346 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. -This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/). It is used by the [INDIGO Orchestrator](https://github.com/indigo-dc/orchestrator) to contact Cloud sites to finally deploy the VMs/containers. +This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/). It is used by the [INDIGO Orchestrator](https://github.com/indigo-dc/orchestrator) to contact Cloud sites to finally deploy the VMs/containers. New features added: @@ -23,226 +23,7 @@ New features added: + Support for the Token Translation Service (TTS) to support IAM authetication on OpenNebula Clouds. + Improvements to access OpenStack Clouds that support IAM. -Read the documentation and more at http://www.grycap.upv.es/im. +Read the documentation and more at the [IM Webpage](http://www.grycap.upv.es/im) or at [Gitbook](https://indigo-dc.gitbooks.io/im/content/). There is also an Infrastructure Manager YouTube reproduction list with a set of videos with demos of the functionality of the platform: https://www.youtube.com/playlist?list=PLgPH186Qwh_37AMhEruhVKZSfoYpHkrUp. - - -1. INSTALLATION -=============== - -1.1 REQUISITES --------------- - -IM is based on Python, so Python 2.6 or higher runtime and standard library must -be installed in the system. - - + The RADL parser (https://github.com/grycap/radl), available in pip - as the 'RADL' package. - - + The paramiko ssh2 protocol library for python version 1.14 or later -(http://www.lag.net/paramiko/), typically available as the 'python-paramiko' package. - - + The YAML library for Python, typically available as the 'python-yaml' or 'PyYAML' package. - - + The SOAPpy library for Python, typically available as the 'python-soappy' or 'SOAPpy' package. - - + The Netaddr library for Python, typically available as the 'python-netaddr' package. - - + The boto library version 2.29 or later - must be installed (http://boto.readthedocs.org/en/latest/). - - + The apache-libcloud library version 0.18 or later - must be installed (http://libcloud.apache.org/). To support OpenStack sites with IAM authentication, - version 1.0.0 or later must be installed. - - + The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at - https://github.com/indigo-dc/tosca-parser but we are working to improve the mainstream version - to enable to use it with the IM. - - + The Bottle framework (http://bottlepy.org/) must be installed, typically available as the 'python-bottle' package. - - + The CherryPy Web framework (http://www.cherrypy.org/) must be installed, typically available as the 'python-cherrypy' - or 'python-cherrypy3' package. - - + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. - In particular, Ansible 1.4.2+ must be installed. The current recommended version is 1.9.4 untill the 2.X versions become stable. - To ensure the functionality the following values must be set in the ansible.cfg file (usually found in /etc/ansible/): - -``` -[defaults] -transport = smart -host_key_checking = False -# For old versions 1.X -sudo_user = root -sudo_exe = sudo - -# For new versions 2.X -become_user = root -become_method = sudo - -[paramiko_connection] - -record_host_keys=False - -[ssh_connection] - -# Only in systems with OpenSSH support to ControlPersist -ssh_args = -o ControlMaster=auto -o ControlPersist=900s -# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) -#ssh_args = -pipelining = True -``` - -1.2 OPTIONAL PACKAGES ---------------------- - -In case of using the SSL secured version of the XMLRPC API the SpringPython -framework (http://springpython.webfactional.com/) must be installed. - -In case of using the SSL secured version of the REST API pyOpenSSL must be installed. - -1.3 INSTALLING --------------- - -### 1.3.1 FROM RPM - -You must have the epel repository enabled: - -```sh -$ yum install epel-release -``` - -Then you have to enable the INDIGO - DataCloud packages repositories. See full instructions -[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the repo file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1.repo) in your /etc/yum.repos.d folder. - -```sh -$ cd /etc/yum.repos.d -$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1.repo -``` - -And then install the GPG key for the INDIGO repository: - -```sh -$ rpm --import http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc -``` - -Finally install the IM package. - -```sh -$ yum install IM -``` - -### 1.3.2 FROM DEB - -You have to enable the INDIGO - DataCloud packages repositories. See full instructions -[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the list file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list) in your /etc/apt/sources.list.d folder. - -```sh -$ cd /etc/apt/sources.list.d -$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list -``` - -And then install the GPG key for INDIGO the repository: - -```sh -$ wget -q -O - http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc | sudo apt-key add - -``` - -Finally install the IM package. - -```sh -$ apt update -$ apt install python-im -``` - -1.4 CONFIGURATION ------------------ - -In case that you want the IM service to be started at boot time, you must -execute the next set of commands: - -On Debian Systems: - -```sh -$ chkconfig im on -``` - -Or for newer systems like ubuntu 14.04: - -```sh -$ sysv-rc-conf im on -``` - -On RedHat Systems: - -```sh -$ update-rc.d im start 99 2 3 4 5 . stop 05 0 1 6 . -``` - -Or you can do it manually: - -```sh -$ ln -s /etc/init.d/im /etc/rc2.d/S99im -$ ln -s /etc/init.d/im /etc/rc3.d/S99im -$ ln -s /etc/init.d/im /etc/rc5.d/S99im -$ ln -s /etc/init.d/im /etc/rc1.d/K05im -$ ln -s /etc/init.d/im /etc/rc6.d/K05im -``` - -Adjust the installation path by setting the IMDAEMON variable at /etc/init.d/im -to the path where the IM im_service.py file is installed (e.g. /usr/local/im/im_service.py), -or set the name of the script file (im_service.py) if the file is in the PATH -(pip puts the im_service.py file in the PATH as default). - -Check the parameters in $IM_PATH/etc/im.cfg or /etc/im/im.cfg. Please pay attention -to the next configuration variables, as they are the most important - -DATA_FILE - must be set to the full path where the IM data file will be created - (e.g. /usr/local/im/inf.dat). Be careful if you have two different instances - of the IM service running in the same machine!!. - -CONTEXTUALIZATION_DIR - must be set to the full path where the IM contextualization files - are located. In case of using pip installation the default value is correct - (/usr/share/im/contextualization) in case of installing from sources set to - $IM_PATH/contextualization (e.g. /usr/local/im/contextualization) - -### 1.4.1 SECURITY - -Security is disabled by default. Please notice that someone with local network access can "sniff" the traffic and -get the messages with the IM with the authorisation data with the cloud providers. - -Security can be activated both in the XMLRPC and REST APIs. Setting this variables: - -XMLRCP_SSL = True - -or - -REST_SSL = True - -And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates paths. - -2. DOCKER IMAGE -=============== - -A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the -default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. - -How to launch the IM service using docker:: - -```sh -$ sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im -``` -You can also specify an external MySQL server to store IM data using the IM_DATA_DB environment variable:: - -```sh -$ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_DATA_DB=mysql://username:password@server/db_name --name im indigodatacloud/im -``` - -You can use the IM as an entry point of an OpenNebula cloud provider as a TOSCA compliant endpoint for your site:: - -```sh -$ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_SINGLE_SITE_ONE_HOST=oneserver.com --name im indigodatacloud/im -``` - \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 000000000..6404d5d10 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,7 @@ +# Summary + +* [About IM](README.md) +* [Installation](doc/gitbook/installation.md) +* [Docker Image](doc/gitbook/docker-image.md) +* [REST API](doc/gitbook/rest-api.md) +* [Service Card](doc/gitbook/service-reference.md) diff --git a/doc/gitbook/docker-image.md b/doc/gitbook/docker-image.md new file mode 100644 index 000000000..29904d3dd --- /dev/null +++ b/doc/gitbook/docker-image.md @@ -0,0 +1,22 @@ +2. DOCKER IMAGE +=============== + +A Docker image named `indigodatacloud/im` has been created to make easier the deployment of an IM service using the +default configuration. Information about this image can be found here: https://hub.docker.com/r/indigodatacloud/im/. + +How to launch the IM service using docker:: + +```sh +$ sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im +``` +You can also specify an external MySQL server to store IM data using the IM_DATA_DB environment variable:: + +```sh +$ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_DATA_DB=mysql://username:password@server/db_name --name im indigodatacloud/im +``` + +You can use the IM as an entry point of an OpenNebula cloud provider as a TOSCA compliant endpoint for your site:: + +```sh +$ sudo docker run -d -p 8899:8899 -p 8800:8800 -e IM_SINGLE_SITE_ONE_HOST=oneserver.com --name im indigodatacloud/im +``` diff --git a/doc/gitbook/installation.md b/doc/gitbook/installation.md new file mode 100644 index 000000000..5ea0ab3ce --- /dev/null +++ b/doc/gitbook/installation.md @@ -0,0 +1,193 @@ +1. INSTALLATION +=============== + +1.1 REQUISITES +-------------- + +IM is based on Python, so Python 2.6 or higher runtime and standard library must +be installed in the system. + + + The RADL parser (https://github.com/grycap/radl), available in pip + as the 'RADL' package. + + + The paramiko ssh2 protocol library for python version 1.14 or later +(http://www.lag.net/paramiko/), typically available as the 'python-paramiko' package. + + + The YAML library for Python, typically available as the 'python-yaml' or 'PyYAML' package. + + + The SOAPpy library for Python, typically available as the 'python-soappy' or 'SOAPpy' package. + + + The Netaddr library for Python, typically available as the 'python-netaddr' package. + + + The boto library version 2.29 or later + must be installed (http://boto.readthedocs.org/en/latest/). + + + The apache-libcloud library version 0.18 or later + must be installed (http://libcloud.apache.org/). To support OpenStack sites with IAM authentication, + version 1.0.0 or later must be installed. + + + The TOSCA-Parser library for Python. Currently it must be used the INDIGO version located at + https://github.com/indigo-dc/tosca-parser but we are working to improve the mainstream version + to enable to use it with the IM. + + + The Bottle framework (http://bottlepy.org/) must be installed, typically available as the 'python-bottle' package. + + + The CherryPy Web framework (http://www.cherrypy.org/) must be installed, typically available as the 'python-cherrypy' + or 'python-cherrypy3' package. + + + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. + In particular, Ansible 1.4.2+ must be installed. The current recommended version is 1.9.4 untill the 2.X versions become stable. + To ensure the functionality the following values must be set in the ansible.cfg file (usually found in /etc/ansible/): + +``` +[defaults] +transport = smart +host_key_checking = False +# For old versions 1.X +sudo_user = root +sudo_exe = sudo + +# For new versions 2.X +become_user = root +become_method = sudo + +[paramiko_connection] + +record_host_keys=False + +[ssh_connection] + +# Only in systems with OpenSSH support to ControlPersist +ssh_args = -o ControlMaster=auto -o ControlPersist=900s +# In systems with older versions of OpenSSH (RHEL 6, CentOS 6, SLES 10 or SLES 11) +#ssh_args = +pipelining = True +``` + +1.2 OPTIONAL PACKAGES +--------------------- + +In case of using the SSL secured version of the XMLRPC API the SpringPython +framework (http://springpython.webfactional.com/) must be installed. + +In case of using the SSL secured version of the REST API pyOpenSSL must be installed. + +1.3 INSTALLING +-------------- + +### 1.3.1 FROM RPM + +You must have the epel repository enabled: + +```sh +$ yum install epel-release +``` + +Then you have to enable the INDIGO - DataCloud packages repositories. See full instructions +[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the repo file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1.repo) in your /etc/yum.repos.d folder. + +```sh +$ cd /etc/yum.repos.d +$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1.repo +``` + +And then install the GPG key for the INDIGO repository: + +```sh +$ rpm --import http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc +``` + +Finally install the IM package. + +```sh +$ yum install IM +``` + +### 1.3.2 FROM DEB + +You have to enable the INDIGO - DataCloud packages repositories. See full instructions +[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the list file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list) in your /etc/apt/sources.list.d folder. + +```sh +$ cd /etc/apt/sources.list.d +$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list +``` + +And then install the GPG key for INDIGO the repository: + +```sh +$ wget -q -O - http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc | sudo apt-key add - +``` + +Finally install the IM package. + +```sh +$ apt update +$ apt install python-im +``` + +1.4 CONFIGURATION +----------------- + +In case that you want the IM service to be started at boot time, you must +execute the next set of commands: + +On Debian Systems: + +```sh +$ chkconfig im on +``` + +Or for newer systems like ubuntu 14.04: + +```sh +$ sysv-rc-conf im on +``` + +On RedHat Systems: + +```sh +$ update-rc.d im start 99 2 3 4 5 . stop 05 0 1 6 . +``` + +Or you can do it manually: + +```sh +$ ln -s /etc/init.d/im /etc/rc2.d/S99im +$ ln -s /etc/init.d/im /etc/rc3.d/S99im +$ ln -s /etc/init.d/im /etc/rc5.d/S99im +$ ln -s /etc/init.d/im /etc/rc1.d/K05im +$ ln -s /etc/init.d/im /etc/rc6.d/K05im +``` + +Adjust the installation path by setting the IMDAEMON variable at /etc/init.d/im +to the path where the IM im_service.py file is installed (e.g. /usr/local/im/im_service.py), +or set the name of the script file (im_service.py) if the file is in the PATH +(pip puts the im_service.py file in the PATH as default). + +Check the parameters in $IM_PATH/etc/im.cfg or /etc/im/im.cfg. Please pay attention +to the next configuration variables, as they are the most important + +DATA_FILE - must be set to the full path where the IM data file will be created + (e.g. /usr/local/im/inf.dat). Be careful if you have two different instances + of the IM service running in the same machine!!. + +CONTEXTUALIZATION_DIR - must be set to the full path where the IM contextualization files + are located. In case of using pip installation the default value is correct + (/usr/share/im/contextualization) in case of installing from sources set to + $IM_PATH/contextualization (e.g. /usr/local/im/contextualization) + +### 1.4.1 SECURITY + +Security is disabled by default. Please notice that someone with local network access can "sniff" the traffic and +get the messages with the IM with the authorisation data with the cloud providers. + +Security can be activated both in the XMLRPC and REST APIs. Setting this variables: + +XMLRCP_SSL = True + +or + +REST_SSL = True + +And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates paths. diff --git a/doc/gitbook/rest-api.md b/doc/gitbook/rest-api.md new file mode 100644 index 000000000..70aacdf49 --- /dev/null +++ b/doc/gitbook/rest-api.md @@ -0,0 +1,374 @@ +# IM REST API + + +The IM Service can be accessed through a REST(ful) API. + +Every HTTP request must be accompanied by the header `AUTHORIZATION` with +the content of the [auth-file](http://imdocs.readthedocs.io/en/devel/client.html#authorization-file), but putting all the elements in one line +using "\\n" as separator. If the content of some of the values has a also a "new line" character it must be replaced by a "\\\\n" as separator. +If the content cannot be parsed successfully, +or the user and password are not valid, it is returned the HTTP error +code 401. + +In the special case of an IM configured as "Single site" support standard HTTP `AUTHORIZATION` header can be used: +* Basic: With a cloud provider that supports simple user/password authentication. +* Bearer: With a cloud provider that supports INDIGO IAM token authentication. + +Next tables summaries the resources and the HTTP methods available. + +| HTTP method | /infrastructures | /infrastructures/<infId> | /infrastructures/<infId>/vms/<vmId> | +| -- | -- | -- | -- | +| **GET** | List the infrastructure IDs.| List the virtual machines in the infrastructure infId | Get information associated to the virtual machine vmId in infId. | +| **POST** | Create a new infrastructure based on the RADL posted | Create a new virtual machine based on the RADL posted. | | +| **PUT** | | | Modify the virtual machine based on the RADL posted. | +| **DELETE** | | Undeploy all the virtual machines in the infrastructure. | Undeploy the virtual machine. | + + +| HTTP method | /infrastructures/<infId>/stop | /infrastructures/<infId>/start | /infrastructures/<infId>/reconfigure | +| -- | -- | -- | -- | +| **PUT** | Stop the infrastructure. | Start the infrastructure. | Reconfigure the infrastructure. | + + +| HTTP method | /infrastructures/<infId>/vms/<vmId>/<property_name> | /infrastructures/<infId>/<property_name> | +| -- | -- | -- | +| **GET** | Get the specified property property_name associated to the machine vmId in infId. It has one special property: contmsg. | Get the specified property property_name associated to the infrastructure infId. It has four properties: contmsg, radl, state and outputs. | + + +| HTTP method | /infrastructures/<infId>/vms/<vmId>/stop | /infrastructures/<infId>/vms/<vmId>/start | +| -- | -- | -- | +| **PUT** | Stop the machine vmId in infId. | Start the machine vmId in infId. | + + + +The error message returned by the service will depend on the `Accept` +header of the request: + +- text/plain: (default option). +- application/json: The request has a "Accept" header with + value "application/json". In this case the format will be: + +```json + { + "message": "Error message text", + "code" : 400 + } +``` + +- text/html: The request has a "Accept" with value to "text/html". + +**GET** `http://imserver.com/infrastructures`: + + * Response Content-type: text/uri-list or application/json + + * ok response: 200 OK + + * fail response: 401, 400 + + Return a list of URIs referencing the infrastructures associated to + the IM user. The result is JSON format has the following format: + +```json + { + "uri-list": [ + { "uri" : "http://server.com:8800/infrastructures/inf_id1" }, + { "uri" : "http://server.com:8800/infrastructures/inf_id2" } + ] + } +``` + +**POST** `http://imserver.com/infrastructures`: + + * body: `RADL document` + + * body Content-type: text/plain or application/json + + * Response Content-type: text/uri-list + + * ok response: 200 OK + + * fail response: 401, 400, 415 + + Create and configure an infrastructure with the requirements + specified in the RADL document of the body contents (in plain RADL + or in JSON formats). If success, it is returned the URI of the new + infrastructure. The result is JSON format has the following format: + +```json + { + "uri" : "http://server.com:8800/infrastructures/inf_id + } +``` + +**GET** `http://imserver.com/infrastructures/`: + + * Response Content-type: text/uri-list or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Return a list of URIs referencing the virtual machines associated to + the infrastructure with ID `infId`. The result is JSON format has + the following format: + +```json + { + "uri-list": [ + { "uri" : "http://server.com:8800/infrastructures/inf_id/vms/0" }, + { "uri" : "http://server.com:8800/infrastructures/inf_id/vms/1" } + ] + } +``` + +**GET** `http://imserver.com/infrastructures//`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400, 403 + + Return property `property_name` associated to the infrastructure with ID `infId`. It has three properties: + + * `outputs`: in case of TOSCA documents it will return a JSON object with + the outputs of the TOSCA document. + + * `contmsg`: a string with the contextualization message. + + * `radl`: a string with the original specified RADL of the infrastructure. + + * `state`: a JSON object with two elements: + + * `state`: a string with the aggregated state of the infrastructure. + + * `vm_states`: a dict indexed with the VM ID and the value the VM state. + + The result is JSON format has the following format: + +```json + { + ["radl"|"state"|"contmsg"|"outputs"]: + } +``` + +**POST** `http://imserver.com/infrastructures/`: + + * body: `RADL document` + + * body Content-type: text/plain or application/json + + * input fields: `context` (optional) + + * Response Content-type: text/uri-list + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400, 415 + + Add the resources specified in the body contents (in plain RADL or + in JSON formats) to the infrastructure with ID `infId`. The RADL + restrictions are the same as + in RPC-XML AddResource <addresource-xmlrpc>. If success, it is + returned a list of URIs of the new virtual machines. The `context` + parameter is optional and is a flag to specify if the + contextualization step will be launched just after the VM addition. + Accetable values: yes, no, true, false, 1 or 0. If not specified the + flag is set to True. The result is JSON format has the following + format: + +```json + { + "uri-list": [ + { "uri" : "http://server.com:8800/infrastructures/inf_id/vms/2" }, + { "uri" : "http://server.com:8800/infrastructures/inf_id/vms/3" } + ] + } +``` + +**PUT** `http://imserver.com/infrastructures//stop`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Perform the `stop` action in all the virtual machines in the the + infrastructure with ID `infID`. If the operation has been performed + successfully the return value is an empty string. + +**PUT** `http://imserver.com/infrastructures//start`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Perform the `start` action in all the virtual machines in the the + infrastructure with ID `infID`. If the operation has been performed + successfully the return value is an empty string. + +**PUT** `http://imserver.com/infrastructures//reconfigure`: + + * body: `RADL document` + + * body Content-type: text/plain or application/json + + * input fields: `vm_list` (optional) + + * Response Content-type: text/plain + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400, 415 + + Perform the `reconfigure` action in all the virtual machines in the + the infrastructure with ID `infID`. It updates the configuration of + the infrastructure as indicated in the body contents (in plain RADL + or in JSON formats). The RADL restrictions are the same as + in RPC-XML Reconfigure <reconfigure-xmlrpc>. If no RADL are + specified, the contextualization process is stated again. The + `vm_list` parameter is optional and is a coma separated list of IDs + of the VMs to reconfigure. If not specified all the VMs will be + reconfigured. If the operation has been performed successfully the + return value is an empty string. + +**DELETE** `http://imserver.com/infrastructures/`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Undeploy the virtual machines associated to the infrastructure with + ID `infId`. If the operation has been performed successfully the + return value is an empty string. + +**GET** `http://imserver.com/infrastructures//vms/`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Return information about the virtual machine with ID `vmId` + associated to the infrastructure with ID `infId`. The returned + string is in RADL format, either in plain RADL or in JSON formats. + See more the details of the output in + GetVMInfo <GetVMInfo-xmlrpc>. The result is JSON format has + the following format: + +```json + { + ["radl"|"state"|"contmsg"]: "" + } +``` + +**GET** `http://imserver.com/infrastructures//vms//`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Return property `property_name` from to the virtual machine with ID + `vmId` associated to the infrastructure with ID `infId`. It also has + one special property `contmsg` that provides a string with the + contextualization message of this VM. The result is JSON format has + the following format: + +```json + { + "": "" + } +``` + +**PUT** `http://imserver.com/infrastructures//vms/`: + + * body: `RADL document` + + * body Content-type: text/plain or application/json + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400, 415 + + Change the features of the virtual machine with ID `vmId` in the + infrastructure with with ID `infId`, specified by the RADL document + specified in the body contents (in plain RADL or in JSON formats). + If the operation has been performed successfully the return value + the return value is an RADL document with the VM properties modified + (also in plain RADL or in JSON formats). The result is JSON format + has the following format: + +```json + { + "radl": + } +``` + +**DELETE** `http://imserver.com/infrastructures//vms/`: + + * input fields: `context` (optional) + + * Response Content-type: text/plain + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Undeploy the virtual machine with ID `vmId` associated to the + infrastructure with ID `infId`. If `vmId` is a comma separated list + of VM IDs, all the VMs of this list will be undeployed. The + `context` parameter is optional and is a flag to specify if the + contextualization step will be launched just after the VM addition. + Accetable values: yes, no, true, false, 1 or 0. If not specified the + flag is set to True. If the operation has been performed + successfully the return value is an empty string. + +**PUT** `http://imserver.com/infrastructures//vms//start`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Perform the `start` action in the virtual machine with ID `vmId` + associated to the infrastructure with ID `infId`. If the operation + has been performed successfully the return value is an empty string. + +**PUT** `http://imserver.com/infrastructures//vms//stop`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 401, 403, 404, 400 + + Perform the `stop` action in the virtual machine with ID `vmId` + associated to the infrastructure with ID `infId`. If the operation + has been performed successfully the return value is an empty string. + +**GET** `http://imserver.com/version`: + + * Response Content-type: text/plain or application/json + + * ok response: 200 OK + + * fail response: 400 + + Return the version of the IM service. The result is JSON format has + the following format: + +```json + { + "version": "1.4.4" + } +``` diff --git a/doc/gitbook/service-reference.md b/doc/gitbook/service-reference.md new file mode 100644 index 000000000..c962fda61 --- /dev/null +++ b/doc/gitbook/service-reference.md @@ -0,0 +1,49 @@ +# Infrastructure Manager - Service Reference Card + +**Functional description:** + IM is a tool that deploys complex and customized virtual infrastructures on IaaS Cloud deployments (such as AWS, OpenStack, etc.). It eases the access and the usability of IaaS clouds by automating the VMI (Virtual Machine Image) selection, deployment, configuration, software installation, monitoring and update of the virtual infrastructure. + It supports APIs from a large number of virtual platforms, making user applications cloud-agnostic. In addition it integrates a contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. + This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/). It is used by the [INDIGO Orchestrator](https://github.com/indigo-dc/orchestrator) to contact Cloud sites to finally deploy the VMs/containers. + +**Services running:** + * im: Im daemon + + +**Configuration:** + * Adjust the installation path by setting the IMDAEMON variable at `/etc/init.d/im` to the path where the IM im_service.py file is installed (e.g. /usr/local/im/im_service.py), or set the name of the script file (im_service.py) if the file is in the PATH (pip puts the im_service.py file in the PATH as default). + + * Check the parameters in `$IM_PATH/etc/im.cfg` or `/etc/im/im.cfg`. Please pay attention to the next configuration variables, as they are the most important + + * DATA_FILE - must be set to the full path where the IM data file will be created (e.g. `/usr/local/im/inf.dat`). Be careful if you have two different instances of the IM service running in the same machine!!. + + * DATA_DB - must be set to a full URL of a MySQL databse to store the IM data (e.g. mysql://username:password@server/db_name). If this value is set it overwrites the DATA_FILE value. + + * CONTEXTUALIZATION_DIR - must be set to the full path where the IM contextualization files are located. In case of using pip installation the default value is correct (`/usr/share/im/contextualization`) in case of installing from sources set to $IM_PATH/contextualization (e.g. /usr/local/im/contextualization) + +**Logfile locations (and management) and other useful audit information:** + * *IM log:* The log file is defined in the LOG_FILE variable of the im.cfg file. The default value is `/var/log/im/im.log`. + +**Open ports needed:** + * Default ports used by the IM: + * XML-RPC API: + * 8899 + * REST API: + * 8800 + +**Where is service state held (and can it be rebuilt):** + Configuration information is stored in a data file or data base. Check the configuration section for more info about the files. + +**Cron jobs:** + None + +**Security information** + * Security is disabled by default. Please notice that someone with local network access can "sniff" the traffic and get the messages with the IM with the authorization data with the cloud providers. + * Security can be activated both in the XMLRPC and REST APIs. Setting this variables: + * XMLRCP_SSL = True + or + * REST_SSL = True + + And then set the variables: XMLRCPSSL or RESTSSL to your certificates paths. + +**Location of reference documentation:** + [IM on Gitbook](https://indigo-dc.gitbooks.io/im/content/) From 1f0e40c8417bcf4fd185d393456df8d8c2da231d Mon Sep 17 00:00:00 2001 From: alpegon Date: Wed, 9 Nov 2016 13:10:50 +0100 Subject: [PATCH 431/509] Update gitbook documentation --- doc/gitbook/service-reference.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/gitbook/service-reference.md b/doc/gitbook/service-reference.md index c962fda61..328b56f79 100644 --- a/doc/gitbook/service-reference.md +++ b/doc/gitbook/service-reference.md @@ -1,13 +1,12 @@ # Infrastructure Manager - Service Reference Card **Functional description:** - IM is a tool that deploys complex and customized virtual infrastructures on IaaS Cloud deployments (such as AWS, OpenStack, etc.). It eases the access and the usability of IaaS clouds by automating the VMI (Virtual Machine Image) selection, deployment, configuration, software installation, monitoring and update of the virtual infrastructure. - It supports APIs from a large number of virtual platforms, making user applications cloud-agnostic. In addition it integrates a contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. - This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/). It is used by the [INDIGO Orchestrator](https://github.com/indigo-dc/orchestrator) to contact Cloud sites to finally deploy the VMs/containers. + * IM is a tool that deploys complex and customized virtual infrastructures on IaaS Cloud deployments (such as AWS, OpenStack, etc.). It eases the access and the usability of IaaS clouds by automating the VMI (Virtual Machine Image) selection, deployment, configuration, software installation, monitoring and update of the virtual infrastructure. + It supports APIs from a large number of virtual platforms, making user applications cloud-agnostic. In addition it integrates a contextualization system to enable the installation and configuration of all the user required applications providing the user with a fully functional infrastructure. + This version evolved in the INDIGO-Datacloud project (https://www.indigo-datacloud.eu/). It is used by the [INDIGO Orchestrator](https://github.com/indigo-dc/orchestrator) to contact Cloud sites to finally deploy the VMs/containers. **Services running:** - * im: Im daemon - + * im: Im daemon **Configuration:** * Adjust the installation path by setting the IMDAEMON variable at `/etc/init.d/im` to the path where the IM im_service.py file is installed (e.g. /usr/local/im/im_service.py), or set the name of the script file (im_service.py) if the file is in the PATH (pip puts the im_service.py file in the PATH as default). @@ -31,14 +30,14 @@ * 8800 **Where is service state held (and can it be rebuilt):** - Configuration information is stored in a data file or data base. Check the configuration section for more info about the files. + * Configuration information is stored in a data file or data base. Check the configuration section for more info about the files. **Cron jobs:** - None + * None **Security information** - * Security is disabled by default. Please notice that someone with local network access can "sniff" the traffic and get the messages with the IM with the authorization data with the cloud providers. - * Security can be activated both in the XMLRPC and REST APIs. Setting this variables: + * Security is disabled by default. Please notice that someone with local network access can "sniff" the traffic and get the messages with the IM with the authorization data with the cloud providers. + * Security can be activated both in the XMLRPC and REST APIs. Setting this variables: * XMLRCP_SSL = True or * REST_SSL = True From f388ec1ddc15b5737ff61f90c518aa4cc59481d7 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 10 Nov 2016 13:15:03 +0100 Subject: [PATCH 432/509] Add list of OIDC issuers supported --- IM/InfrastructureManager.py | 30 ++++++++++++++++-------------- IM/config.py | 1 + etc/im.cfg | 3 +++ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 4691339e0..6fcddd7af 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1206,35 +1206,37 @@ def check_im_user(auth): return True @staticmethod - def check_iam_token(im_auth): + def check_oidc_token(im_auth): token = im_auth["token"] success = False try: # decode the token to get the issuer decoded_token = JWT().get_info(token) - success, userinfo = OpenIDClient.get_user_info_request(token) - if success: - # convert to username to use it in the rest of the IM - im_auth['username'] = str(userinfo.get("preferred_username")) - im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) + if decoded_token['iss'] in Config.OIDC_ISSUERS: + success, userinfo = OpenIDClient.get_user_info_request(token) + if success: + # convert to username to use it in the rest of the IM + im_auth['username'] = str(userinfo.get("preferred_username")) + im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) + else: + InfrastructureManager.logger.error("Incorrect OIDC issuer: %s" % decoded_token['iss']) + raise InvaliddUserException("Invalid InfrastructureManager credentials. Issuer not accepted.") except Exception, ex: - InfrastructureManager.logger.exception( - "Error trying to validate auth token: %s" % str(ex)) - raise Exception("Error trying to validate auth token: %s" % str(ex)) + InfrastructureManager.logger.exception("Error trying to validate OIDC auth token: %s" % str(ex)) + raise Exception("Error trying to validate OIDC auth token: %s" % str(ex)) if not success: - InfrastructureManager.logger.error( - "Incorrect auth token: %s" % userinfo) - raise InvaliddUserException("Invalid InfrastructureManager credentials %s" % userinfo) + InfrastructureManager.logger.error("Incorrect OIDC auth token: %s" % userinfo) + raise InvaliddUserException("Invalid InfrastructureManager credentials. %s." % userinfo) @staticmethod def check_auth_data(auth): # First check if it is configured to check the users from a list im_auth = auth.getAuthInfo("InfrastructureManager") - # First check if the IAM token is included + # First check if an OIDC token is included if "token" in im_auth[0]: - InfrastructureManager.check_iam_token(im_auth[0]) + InfrastructureManager.check_oidc_token(im_auth[0]) else: # if not assume the basic user/password auth data if not InfrastructureManager.check_im_user(im_auth): diff --git a/IM/config.py b/IM/config.py index 372cb40d1..84bada585 100644 --- a/IM/config.py +++ b/IM/config.py @@ -95,6 +95,7 @@ class Config: SINGLE_SITE_TYPE = '' SINGLE_SITE_AUTH_HOST = '' SINGLE_SITE_IMAGE_URL_PREFIX = '' + OIDC_ISSUERS = ["https://iam-test.indigo-datacloud.eu/"] config = ConfigParser.ConfigParser() config.read([Config.IM_PATH + '/../im.cfg', Config.IM_PATH + diff --git a/etc/im.cfg b/etc/im.cfg index 76a50bdf1..c255fbe40 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -113,6 +113,9 @@ SINGLE_SITE_AUTH_HOST = http://server.com:2633 # Set the url prefix of the images of the single site SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com/ +# List of OIDC issuers supported +OIDC_ISSUERS = https://iam-test.indigo-datacloud.eu/ + [OpenNebula] # OpenNebula connector configuration values From a57faae05a27c5ffe93739b2e46621352e20eccc Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 10 Nov 2016 13:46:24 +0100 Subject: [PATCH 433/509] Complete test --- test/unit/test_im_logic.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index d52473e57..6fc209d6a 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -646,7 +646,7 @@ def test_tosca_get_outputs(self): 'user': 'ubuntu'}}) @patch('httplib.HTTPSConnection') - def test_check_iam_token(self, connection): + def test_check_oidc_token(self, connection): im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" "joxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg" @@ -664,10 +664,18 @@ def test_check_iam_token(self, connection): resp.read.return_value = user_info conn.getresponse.return_value = resp - IM.check_iam_token(im_auth) + IM.check_oidc_token(im_auth) self.assertEqual(im_auth['username'], "micafer") self.assertEqual(im_auth['password'], "https://iam-test.indigo-datacloud.eu/sub") + + Config.OIDC_ISSUERS = ["https://other_issuer"] + + with self.assertRaises(Exception) as ex: + IM.check_oidc_token(im_auth) + self.assertEqual(str(ex.exception), + ("Error trying to validate OIDC auth token: Invalid " + "InfrastructureManager credentials. Issuer not accepted.")) @patch('IM.InfrastructureManager.DataBase.connect') @patch('IM.InfrastructureManager.DataBase.table_exists') From 6a20b74a430f35c17e784f483784b8edfc3bc1aa Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 10 Nov 2016 13:48:13 +0100 Subject: [PATCH 434/509] Style changes --- test/unit/test_im_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 6fc209d6a..9617e5333 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -668,9 +668,9 @@ def test_check_oidc_token(self, connection): self.assertEqual(im_auth['username'], "micafer") self.assertEqual(im_auth['password'], "https://iam-test.indigo-datacloud.eu/sub") - + Config.OIDC_ISSUERS = ["https://other_issuer"] - + with self.assertRaises(Exception) as ex: IM.check_oidc_token(im_auth) self.assertEqual(str(ex.exception), From 841d3e42ef12c921161fb7fb54ab9be418818c34 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 21 Nov 2016 09:15:58 +0100 Subject: [PATCH 435/509] Bugfix --- IM/InfrastructureInfo.py | 5 +++++ IM/tosca/Tosca.py | 7 +++++++ README | 7 ++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 76816e02a..99fd60795 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -30,6 +30,7 @@ from IM.openid.JWT import JWT from IM.VirtualMachine import VirtualMachine from IM.auth import Authentication +from IM.tosca import Tosca class IncorrectVMException(Exception): @@ -112,6 +113,8 @@ def serialize(self): odict['auth'] = odict['auth'].serialize() if odict['radl']: odict['radl'] = dump_radl_json(odict['radl']) + if odict['extra_info'] and "TOSCA" in odict['extra_info']: + odict['extra_info'] = odict['extra_info'].serialize() return json.dumps(odict) @staticmethod @@ -127,6 +130,8 @@ def deserialize(str_data): dic['auth'] = Authentication.deserialize(dic['auth']) if dic['radl']: dic['radl'] = parse_radl_json(dic['radl']) + if dic['extra_info'] and "TOSCA" in dic['extra_info']: + dic['extra_info'] = Tosca.deserialize(dic['extra_info']) newinf.__dict__.update(dic) newinf.cloud_connector = None # Set the ConfManager object and the lock to the data loaded diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a8f0ab08a..7abfe6dfe 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -31,6 +31,13 @@ def __init__(self, yaml_str): self.yaml = yaml.load(yaml_str) self.tosca = ToscaTemplate(yaml_dict_tpl=copy.deepcopy(self.yaml)) + def serialize(self): + return yaml.dump(self.yaml) + + @staticmethod + def deserialize(str_data): + return Tosca(str_data) + def to_radl(self, inf_info=None): """ Converts the current ToscaTemplate object in a RADL object diff --git a/README b/README index df5307c8f..47105574f 100644 --- a/README +++ b/README @@ -177,9 +177,10 @@ or set the name of the script file (im_service.py) if the file is in the PATH Check the parameters in $IM_PATH/etc/im.cfg or /etc/im/im.cfg. Please pay attention to the next configuration variables, as they are the most important -DATA_FILE - must be set to the full path where the IM data file will be created - (e.g. /usr/local/im/inf.dat). Be careful if you have two different instances - of the IM service running in the same machine!!. +DATA_DB - must be set to the URL to access the database to store the IM data. + Be careful if you have two different instances of the IM service running in the same machine!!. + It can be a MySQL DB: 'mysql://username:password@server/db_name' or + a SQLite one: 'sqlite:///etc/im/inf.dat'. CONTEXTUALIZATION_DIR - must be set to the full path where the IM contextualization files are located. In case of using pip installation the default value is correct From 55148da9253ce1643bebbea9a572d7e582b75912 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 21 Nov 2016 09:16:10 +0100 Subject: [PATCH 436/509] Update test --- test/files/data.json | 2 +- test/unit/test_im_logic.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/files/data.json b/test/files/data.json index 047a64ba3..c6a55ea48 100644 --- a/test/files/data.json +++ b/test/files/data.json @@ -1 +1 @@ -{"system_counter": 0, "vm_list": ["{\"info\": \"[\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"public\\\",\\n \\\"outbound\\\": \\\"yes\\\",\\n \\\"provider_id\\\": \\\"publica\\\"\\n },\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"privada\\\",\\n \\\"provider_id\\\": \\\"privada\\\"\\n },\\n {\\n \\\"class\\\": \\\"system\\\",\\n \\\"cpu.arch\\\": \\\"x86_64\\\",\\n \\\"cpu.count\\\": 1,\\n \\\"disk.0.device\\\": \\\"hda\\\",\\n \\\"disk.0.image.name\\\": \\\"OneCloud_Ubuntu_14_04\\\",\\n \\\"disk.0.image.url\\\": \\\"one://onecloud.i3m.upv.es/77\\\",\\n \\\"disk.0.os.credentials.new.password\\\": \\\"N0tan+mala\\\",\\n \\\"disk.0.os.credentials.password\\\": \\\"yoyoyo\\\",\\n \\\"disk.0.os.credentials.username\\\": \\\"ubuntu\\\",\\n \\\"disk.0.os.flavour\\\": \\\"ubuntu\\\",\\n \\\"disk.0.os.name\\\": \\\"linux\\\",\\n \\\"disk.0.os.version\\\": \\\"14.04\\\",\\n \\\"disk.0.size\\\": 20971520000,\\n \\\"id\\\": \\\"node\\\",\\n \\\"instance_id\\\": \\\"24962\\\",\\n \\\"instance_name\\\": \\\"OneCloud_Ubuntu_14_04\\\",\\n \\\"launch_time\\\": 1479402015,\\n \\\"memory.size\\\": 1073741824,\\n \\\"net_interface.0.connection\\\": \\\"public\\\",\\n \\\"net_interface.0.dns_name\\\": \\\"testnode\\\",\\n \\\"net_interface.0.ip\\\": \\\"158.42.105.16\\\",\\n \\\"net_interface.1.connection\\\": \\\"privada\\\",\\n \\\"net_interface.1.ip\\\": \\\"10.10.2.24\\\",\\n \\\"provider.host\\\": \\\"onecloud.i3m.upv.es\\\",\\n \\\"provider.port\\\": 2633,\\n \\\"provider.type\\\": \\\"OpenNebula\\\",\\n \\\"state\\\": \\\"off\\\",\\n \\\"virtual_system_type\\\": \\\"qemu\\\"\\n },\\n {\\n \\\"class\\\": \\\"configure\\\",\\n \\\"id\\\": \\\"node\\\",\\n \\\"recipes\\\": \\\"\\\\n\\\\n---\\\\n - tasks:\\\\n - name: test\\\\n command: sleep 30\\\\n\\\\n\\\\n\\\"\\n },\\n {\\n \\\"class\\\": \\\"deploy\\\",\\n \\\"system\\\": \\\"node\\\",\\n \\\"vm_number\\\": 1\\n }\\n]\", \"im_id\": 0, \"requested_radl\": \"[\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"public\\\",\\n \\\"outbound\\\": \\\"yes\\\"\\n },\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"privada\\\"\\n },\\n {\\n \\\"class\\\": \\\"system\\\",\\n \\\"cpu.count_min\\\": 1,\\n \\\"disk.0.os.credentials.new.password\\\": \\\"N0tan+mala\\\",\\n \\\"disk.0.os.flavour\\\": \\\"ubuntu\\\",\\n \\\"disk.0.os.name\\\": \\\"linux\\\",\\n \\\"id\\\": \\\"node\\\",\\n \\\"memory.size_min\\\": 1073741824,\\n \\\"net_interface.0.connection\\\": \\\"public\\\",\\n \\\"net_interface.0.dns_name\\\": \\\"testnode\\\",\\n \\\"net_interface.1.connection\\\": \\\"privada\\\"\\n },\\n {\\n \\\"class\\\": \\\"configure\\\",\\n \\\"id\\\": \\\"node\\\",\\n \\\"recipes\\\": \\\"\\\\n\\\\n---\\\\n - tasks:\\\\n - name: test\\\\n command: sleep 30\\\\n\\\\n\\\\n\\\"\\n },\\n {\\n \\\"class\\\": \\\"deploy\\\",\\n \\\"system\\\": \\\"node\\\",\\n \\\"vm_number\\\": 1\\n }\\n]\", \"ctxt_pid\": null, \"last_update\": 1479401757, \"state\": \"off\", \"cont_out\": \"\", \"id\": \"24962\", \"destroy\": true, \"ssh_connect_errors\": 0, \"configured\": null, \"cloud\": \"{\\\"protocol\\\": \\\"\\\", \\\"id\\\": \\\"onecloud\\\", \\\"path\\\": \\\"\\\", \\\"server\\\": \\\"onecloud.i3m.upv.es\\\", \\\"type\\\": \\\"OpenNebula\\\", \\\"port\\\": 2633}\"}"], "radl": "[\n {\n \"class\": \"network\",\n \"id\": \"public\",\n \"outbound\": \"yes\"\n },\n {\n \"class\": \"network\",\n \"id\": \"privada\"\n },\n {\n \"class\": \"system\",\n \"cpu.count_min\": 1,\n \"disk.0.os.credentials.new.password\": \"N0tan+mala\",\n \"disk.0.os.flavour\": \"ubuntu\",\n \"disk.0.os.name\": \"linux\",\n \"id\": \"node\",\n \"memory.size_min\": 1073741824,\n \"net_interface.0.connection\": \"public\",\n \"net_interface.0.dns_name\": \"testnode\",\n \"net_interface.1.connection\": \"privada\"\n },\n {\n \"class\": \"configure\",\n \"id\": \"node\",\n \"recipes\": \"\\n\\n---\\n - tasks:\\n - name: test\\n command: sleep 30\\n\\n\\n\"\n },\n {\n \"class\": \"deploy\",\n \"cloud\": \"onecloud\",\n \"system\": \"node\",\n \"vm_number\": 1\n }\n]", "deleted": true, "configured": true, "vm_master": 0, "auth": "[{\"username\": \"micafer\", \"password\": \"grycap01\", \"type\": \"InfrastructureManager\"}]", "cont_out": "2016-11-17 17:55:57.713062: Select master VM\n2016-11-17 17:55:57.713789: Wait master VM to boot\n", "id": "a091a1d8-ace6-11e6-903b-02421c3eaeeb", "private_networks": {"privada": "onecloud"}, "last_ganglia_update": 0, "vm_id": 1, "ansible_configured": null} \ No newline at end of file +{"system_counter": 0, "vm_list": ["{\"info\": \"[\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"public\\\",\\n \\\"outbound\\\": \\\"yes\\\",\\n \\\"provider_id\\\": \\\"publica\\\"\\n },\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"privada\\\",\\n \\\"provider_id\\\": \\\"privada\\\"\\n },\\n {\\n \\\"class\\\": \\\"system\\\",\\n \\\"cpu.arch\\\": \\\"x86_64\\\",\\n \\\"cpu.count\\\": 1,\\n \\\"disk.0.device\\\": \\\"hda\\\",\\n \\\"disk.0.image.name\\\": \\\"OneCloud_Ubuntu_14_04\\\",\\n \\\"disk.0.image.url\\\": \\\"one://onecloud.i3m.upv.es/77\\\",\\n \\\"disk.0.os.credentials.new.password\\\": \\\"N0tan+mala\\\",\\n \\\"disk.0.os.credentials.password\\\": \\\"yoyoyo\\\",\\n \\\"disk.0.os.credentials.username\\\": \\\"ubuntu\\\",\\n \\\"disk.0.os.flavour\\\": \\\"ubuntu\\\",\\n \\\"disk.0.os.name\\\": \\\"linux\\\",\\n \\\"disk.0.os.version\\\": \\\"14.04\\\",\\n \\\"disk.0.size\\\": 20971520000,\\n \\\"id\\\": \\\"node\\\",\\n \\\"instance_id\\\": \\\"24962\\\",\\n \\\"instance_name\\\": \\\"OneCloud_Ubuntu_14_04\\\",\\n \\\"launch_time\\\": 1479402015,\\n \\\"memory.size\\\": 1073741824,\\n \\\"net_interface.0.connection\\\": \\\"public\\\",\\n \\\"net_interface.0.dns_name\\\": \\\"testnode\\\",\\n \\\"net_interface.0.ip\\\": \\\"158.42.105.16\\\",\\n \\\"net_interface.1.connection\\\": \\\"privada\\\",\\n \\\"net_interface.1.ip\\\": \\\"10.10.2.24\\\",\\n \\\"provider.host\\\": \\\"onecloud.i3m.upv.es\\\",\\n \\\"provider.port\\\": 2633,\\n \\\"provider.type\\\": \\\"OpenNebula\\\",\\n \\\"state\\\": \\\"off\\\",\\n \\\"virtual_system_type\\\": \\\"qemu\\\"\\n },\\n {\\n \\\"class\\\": \\\"configure\\\",\\n \\\"id\\\": \\\"node\\\",\\n \\\"recipes\\\": \\\"\\\\n\\\\n---\\\\n - tasks:\\\\n - name: test\\\\n command: sleep 30\\\\n\\\\n\\\\n\\\"\\n },\\n {\\n \\\"class\\\": \\\"deploy\\\",\\n \\\"system\\\": \\\"node\\\",\\n \\\"vm_number\\\": 1\\n }\\n]\", \"im_id\": 0, \"requested_radl\": \"[\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"public\\\",\\n \\\"outbound\\\": \\\"yes\\\"\\n },\\n {\\n \\\"class\\\": \\\"network\\\",\\n \\\"id\\\": \\\"privada\\\"\\n },\\n {\\n \\\"class\\\": \\\"system\\\",\\n \\\"cpu.count_min\\\": 1,\\n \\\"disk.0.os.credentials.new.password\\\": \\\"N0tan+mala\\\",\\n \\\"disk.0.os.flavour\\\": \\\"ubuntu\\\",\\n \\\"disk.0.os.name\\\": \\\"linux\\\",\\n \\\"id\\\": \\\"node\\\",\\n \\\"memory.size_min\\\": 1073741824,\\n \\\"net_interface.0.connection\\\": \\\"public\\\",\\n \\\"net_interface.0.dns_name\\\": \\\"testnode\\\",\\n \\\"net_interface.1.connection\\\": \\\"privada\\\"\\n },\\n {\\n \\\"class\\\": \\\"configure\\\",\\n \\\"id\\\": \\\"node\\\",\\n \\\"recipes\\\": \\\"\\\\n\\\\n---\\\\n - tasks:\\\\n - name: test\\\\n command: sleep 30\\\\n\\\\n\\\\n\\\"\\n },\\n {\\n \\\"class\\\": \\\"deploy\\\",\\n \\\"system\\\": \\\"node\\\",\\n \\\"vm_number\\\": 1\\n }\\n]\", \"ctxt_pid\": null, \"last_update\": 1479401757, \"state\": \"off\", \"cont_out\": \"\", \"id\": \"24962\", \"destroy\": true, \"ssh_connect_errors\": 0, \"configured\": null, \"cloud\": \"{\\\"protocol\\\": \\\"\\\", \\\"id\\\": \\\"onecloud\\\", \\\"path\\\": \\\"\\\", \\\"server\\\": \\\"onecloud.i3m.upv.es\\\", \\\"type\\\": \\\"OpenNebula\\\", \\\"port\\\": 2633}\"}"], "radl": "[\n {\n \"class\": \"network\",\n \"id\": \"public\",\n \"outbound\": \"yes\"\n },\n {\n \"class\": \"network\",\n \"id\": \"privada\"\n },\n {\n \"class\": \"system\",\n \"cpu.count_min\": 1,\n \"disk.0.os.credentials.new.password\": \"N0tan+mala\",\n \"disk.0.os.flavour\": \"ubuntu\",\n \"disk.0.os.name\": \"linux\",\n \"id\": \"node\",\n \"memory.size_min\": 1073741824,\n \"net_interface.0.connection\": \"public\",\n \"net_interface.0.dns_name\": \"testnode\",\n \"net_interface.1.connection\": \"privada\"\n },\n {\n \"class\": \"configure\",\n \"id\": \"node\",\n \"recipes\": \"\\n\\n---\\n - tasks:\\n - name: test\\n command: sleep 30\\n\\n\\n\"\n },\n {\n \"class\": \"deploy\",\n \"cloud\": \"onecloud\",\n \"system\": \"node\",\n \"vm_number\": 1\n }\n]", "extra_info":"{}", "deleted": true, "configured": true, "vm_master": 0, "auth": "[{\"username\": \"micafer\", \"password\": \"grycap01\", \"type\": \"InfrastructureManager\"}]", "cont_out": "2016-11-17 17:55:57.713062: Select master VM\n2016-11-17 17:55:57.713789: Wait master VM to boot\n", "id": "a091a1d8-ace6-11e6-903b-02421c3eaeeb", "private_networks": {"privada": "onecloud"}, "last_ganglia_update": 0, "vm_id": 1, "ansible_configured": null} \ No newline at end of file diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 549b69929..be36ea3f2 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -59,7 +59,7 @@ def setUp(self): IM._reinit() # Patch save_data - IM.save_data = staticmethod(lambda *args: None) + InfrastructureList.save_data = staticmethod(lambda *args: None) ch = logging.StreamHandler(sys.stdout) log = logging.getLogger('InfrastructureManager') From 39673bbfa882bbb412ab8229e654ff7025182534 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 21 Nov 2016 09:16:45 +0100 Subject: [PATCH 437/509] Update doc --- doc/gitbook/installation.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/gitbook/installation.md b/doc/gitbook/installation.md index 8a33291e5..bb6a426ce 100644 --- a/doc/gitbook/installation.md +++ b/doc/gitbook/installation.md @@ -171,9 +171,10 @@ or set the name of the script file (im_service.py) if the file is in the PATH Check the parameters in $IM_PATH/etc/im.cfg or /etc/im/im.cfg. Please pay attention to the next configuration variables, as they are the most important -DATA_FILE - must be set to the full path where the IM data file will be created - (e.g. /usr/local/im/inf.dat). Be careful if you have two different instances - of the IM service running in the same machine!!. +DATA_DB - must be set to the URL to access the database to store the IM data. + Be careful if you have two different instances of the IM service running in the same machine!!. + It can be a MySQL DB: 'mysql://username:password@server/db_name' or + a SQLite one: 'sqlite:///etc/im/inf.dat'. CONTEXTUALIZATION_DIR - must be set to the full path where the IM contextualization files are located. In case of using pip installation the default value is correct From a083b9f7891a441465f14b99134164ea14840401 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 21 Nov 2016 09:16:57 +0100 Subject: [PATCH 438/509] Set allow_world_readable_tmpfiles --- contextualization/conf-ansible.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index f59e5e594..ded2d0e26 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -137,3 +137,6 @@ - name: Set jinja2.ext.do to jinja2_extensions in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=jinja2_extensions value=jinja2.ext.do + + - name: Set allow_world_readable_tmpfiles to True ansible.cfg + ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=allow_world_readable_tmpfiles value=True From d2d6df64a1d30845290a9434e16ead348812baf2 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 21 Nov 2016 10:13:14 +0100 Subject: [PATCH 439/509] Add packages of DB systems --- ansible_install.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index 977b5b70d..8ca676b4b 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -21,7 +21,7 @@ when: ansible_os_family == "Debian" - name: Ubuntu install Ansible with apt - apt: name=python-im,ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip force=yes + apt: name=python-im,ansible,python-pip,python-jinja2,sshpass,openssh-client,unzip,python-mysqldb,python-sqlite force=yes when: ansible_distribution == "Ubuntu" - name: RH indigo repos @@ -32,7 +32,7 @@ when: ansible_os_family == "RedHat" - name: RH7 install Ansible with yum - yum: name=IM,ansible,python-pip,python-jinja2,sshpass,openssh-clients,unzip + yum: name=IM,ansible,python-pip,python-jinja2,sshpass,openssh-clients,unzip,MySQL-python,python-sqlite3dbm when: ansible_os_family == "RedHat" ################################################ Configure Ansible ################################################### @@ -60,4 +60,4 @@ when: (ansible_os_family == "RedHat" and ansible_distribution_major_version < 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version < 12) - name: Activate SSH pipelining in ansible.cfg - ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=pipelining value=True \ No newline at end of file + ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=pipelining value=True From 46c5359690d484612e221235b6d641443722d4dd Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 Jan 2017 16:26:08 +0100 Subject: [PATCH 440/509] Fix test --- test/integration/TestREST.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 6ccf48566..7f5740a48 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -147,14 +147,9 @@ def test_12_list_with_incorrect_token(self): if line.find("type = InfrastructureManager") == -1: auth_data += line.strip() + "\\n" - server = httplib.HTTPConnection(HOSTNAME, TEST_PORT) - server.request('GET', "/infrastructures", - headers={'AUTHORIZATION': auth_data}) - resp = server.getresponse() - output = str(resp.read()) - server.close() - self.assertEqual(resp.status, 401, - msg="ERROR using an invalid token. A 401 error is expected:" + output) + resp = self.create_request("GET", "/infrastructures/", headers={'AUTHORIZATION': auth_data}) + self.assertEqual(resp.status_code, 401, + msg="ERROR using an invalid token. A 401 error is expected:" + resp.text) def test_15_get_incorrect_info(self): resp = self.create_request("GET", "/infrastructures/999999") @@ -407,7 +402,7 @@ def test_93_create_tosca(self): self.assertEqual(resp.status_code, 200, msg="ERROR creating the infrastructure:" + resp.text) - self.__class__.inf_id = str(os.path.basename(output)) + self.__class__.inf_id = str(os.path.basename(resp.text)) all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 600) self.assertTrue( From c80beebc608373e5b4f77481d5db6fd3fab2d02e Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 10 Jan 2017 16:45:22 +0100 Subject: [PATCH 441/509] Fix test --- test/integration/TestREST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/TestREST.py b/test/integration/TestREST.py index 7f5740a48..f8d525edd 100755 --- a/test/integration/TestREST.py +++ b/test/integration/TestREST.py @@ -147,7 +147,7 @@ def test_12_list_with_incorrect_token(self): if line.find("type = InfrastructureManager") == -1: auth_data += line.strip() + "\\n" - resp = self.create_request("GET", "/infrastructures/", headers={'AUTHORIZATION': auth_data}) + resp = self.create_request("GET", "/infrastructures", headers={'AUTHORIZATION': auth_data}) self.assertEqual(resp.status_code, 401, msg="ERROR using an invalid token. A 401 error is expected:" + resp.text) From 853f7fec7c6e06c181701cbdfb062d23b946c6e2 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 13 Jan 2017 08:30:33 +0100 Subject: [PATCH 442/509] Update REST api docs --- doc/gitbook/rest-api.md | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/doc/gitbook/rest-api.md b/doc/gitbook/rest-api.md index 70aacdf49..b51209523 100644 --- a/doc/gitbook/rest-api.md +++ b/doc/gitbook/rest-api.md @@ -78,9 +78,9 @@ header of the request: **POST** `http://imserver.com/infrastructures`: - * body: `RADL document` + * body: `RADL or TOSCA document` - * body Content-type: text/plain or application/json + * body Content-type: text/yaml, text/plain or application/json * Response Content-type: text/uri-list @@ -89,7 +89,7 @@ header of the request: * fail response: 401, 400, 415 Create and configure an infrastructure with the requirements - specified in the RADL document of the body contents (in plain RADL + specified in the RADL or TOSCA document of the body contents (RADL in plain text or in JSON formats). If success, it is returned the URI of the new infrastructure. The result is JSON format has the following format: @@ -153,9 +153,9 @@ header of the request: **POST** `http://imserver.com/infrastructures/`: - * body: `RADL document` + * body: `RADL or TOSCA document` - * body Content-type: text/plain or application/json + * body Content-type: text/yaml or text/plain or application/json * input fields: `context` (optional) @@ -165,14 +165,20 @@ header of the request: * fail response: 401, 403, 404, 400, 415 - Add the resources specified in the body contents (in plain RADL or + Add the resources specified in the body contents (in TOSCA, plain RADL or in JSON formats) to the infrastructure with ID `infId`. The RADL - restrictions are the same as - in RPC-XML AddResource <addresource-xmlrpc>. If success, it is - returned a list of URIs of the new virtual machines. The `context` - parameter is optional and is a flag to specify if the + restrictions are the same as in RPC-XML AddResource <addresource-xmlrpc>. + + In case of TOSCA a whole TOSCA document is expected. In case of new template is + added to the TOSCA document or the ``count`` of a node is increased new nodes + will be added to de infrastructure. In case decreasing the number of the ``count`` + scalable property of a node a ``removal_list`` property has to be added to specify + the ID of the VM to delete (see an example [here](https://github.com/indigo-dc/im/blob/master/test/files/tosca_remove.yml)). + + If success, it is returned a list of URIs of the new virtual machines. + The `context` parameter is optional and is a flag to specify if the contextualization step will be launched just after the VM addition. - Accetable values: yes, no, true, false, 1 or 0. If not specified the + Acceptable values: yes, no, true, false, 1 or 0. If not specified the flag is set to True. The result is JSON format has the following format: @@ -328,7 +334,7 @@ header of the request: of VM IDs, all the VMs of this list will be undeployed. The `context` parameter is optional and is a flag to specify if the contextualization step will be launched just after the VM addition. - Accetable values: yes, no, true, false, 1 or 0. If not specified the + Acceptable values: yes, no, true, false, 1 or 0. If not specified the flag is set to True. If the operation has been performed successfully the return value is an empty string. From ae749413eecaa558912c0654f8fca9c9ba8487c7 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 13 Jan 2017 12:34:27 +0100 Subject: [PATCH 443/509] Minor improvement --- IM/InfrastructureInfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index d00a5cc28..1c0871d43 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -129,7 +129,7 @@ def deserialize(str_data): dic['auth'] = Authentication.deserialize(dic['auth']) if dic['radl']: dic['radl'] = parse_radl_json(dic['radl']) - if dic['extra_info'] and "TOSCA" in dic['extra_info']: + if 'extra_info' in dic and dic['extra_info'] and "TOSCA" in dic['extra_info']: dic['extra_info'] = Tosca.deserialize(dic['extra_info']) newinf.__dict__.update(dic) newinf.cloud_connector = None From ae54cdb8ad9031d78c6662da4fff5da9deddf21d Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 13 Jan 2017 12:34:56 +0100 Subject: [PATCH 444/509] Minor improvement --- IM/InfrastructureManager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 25cc1565f..ca0a11ef0 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1213,10 +1213,10 @@ def check_auth_data(auth): # First check if an OIDC token is included if "token" in im_auth[0]: InfrastructureManager.check_oidc_token(im_auth[0]) - else: - # if not assume the basic user/password auth data - if not InfrastructureManager.check_im_user(im_auth): - raise InvaliddUserException() + + # Now check if the user is in authorized + if not InfrastructureManager.check_im_user(im_auth): + raise InvaliddUserException() if Config.SINGLE_SITE: vmrc_auth = auth.getAuthInfo("VMRC") From 52822c3d4afe984559f5762d12649f1bfbe173f6 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 13 Jan 2017 12:35:30 +0100 Subject: [PATCH 445/509] Support INDIGO Openstack sites as single site --- IM/REST.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index c22108c16..162c06844 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -174,9 +174,16 @@ def get_auth_header(): im_auth = {"type": "InfrastructureManager", "username": "user", "token": token} - single_site_auth = {"type": Config.SINGLE_SITE_TYPE, - "host": Config.SINGLE_SITE_AUTH_HOST, - "token": token} + if Config.SINGLE_SITE_TYPE == "OpenStack": + single_site_auth = {"type": Config.SINGLE_SITE_TYPE, + "host": Config.SINGLE_SITE_AUTH_HOST, + "username": "indigo-dc", + "tenant": "oidc", + "password": token} + else: + single_site_auth = {"type": Config.SINGLE_SITE_TYPE, + "host": Config.SINGLE_SITE_AUTH_HOST, + "token": token} return Authentication([im_auth, single_site_auth]) auth_data = auth_header.replace(AUTH_NEW_LINE_SEPARATOR, "\n") auth_data = auth_data.split(AUTH_LINE_SEPARATOR) From fee799f7e1bbf2a1f2ecdef65beea0e252ff43d3 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 26 Jan 2017 17:02:57 +0100 Subject: [PATCH 446/509] Fix issue: #132 --- IM/tosca/Tosca.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 7abfe6dfe..708f7ba9f 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -220,6 +220,7 @@ def _add_node_nets(node, radl, system, nodetemplates): public_ip = node_props["public_ip"].value # This is the solution using endpoints + net_provider_id = None dns_name = None ports = {} node_caps = node.get_capabilities() @@ -229,6 +230,11 @@ def _add_node_nets(node, radl, system, nodetemplates): if cap_props and "network_name" in cap_props: if cap_props["network_name"].value == "PUBLIC": public_ip = True + # In this case the user is specifying the provider_id + elif str(cap_props["network_name"].value).endswith(".PUBLIC"): + public_ip = True + parts = cap_props["network_name"].value.split(".") + net_provider_id = ".".join(parts[:-1]) if cap_props and "dns_name" in cap_props: dns_name = cap_props["dns_name"].value if cap_props and "ports" in cap_props: @@ -262,6 +268,8 @@ def _add_node_nets(node, radl, system, nodetemplates): if ports: public_net.setValue("outports", Tosca._format_outports(ports)) + if net_provider_id: + public_net.setValue("provider_id", net_provider_id) system.setValue('net_interface.%d.connection' % num_net, public_net.id) @@ -290,11 +298,12 @@ def _add_node_nets(node, radl, system, nodetemplates): radl.networks.append(private_net) num_net = system.getNumNetworkIfaces() - system.setValue('net_interface.' + str(num_net) + - '.connection', private_net.id) + system.setValue('net_interface.' + str(num_net) + '.connection', private_net.id) if dns_name: system.setValue('net_interface.0.dns_name', dns_name) + if not public_ip and net_provider_id: + private_net.setValue("provider_id", net_provider_id) @staticmethod def _get_scalable_properties(node): From 0995993dac405489b9fb3f6b1688a7f03dc47b1d Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 27 Jan 2017 12:58:47 +0100 Subject: [PATCH 447/509] Enable set net provider_id in private nets --- IM/tosca/Tosca.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 708f7ba9f..19f16673b 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -235,6 +235,9 @@ def _add_node_nets(node, radl, system, nodetemplates): public_ip = True parts = cap_props["network_name"].value.split(".") net_provider_id = ".".join(parts[:-1]) + elif str(cap_props["network_name"].value).endswith(".PRIVATE"): + parts = cap_props["network_name"].value.split(".") + net_provider_id = ".".join(parts[:-1]) if cap_props and "dns_name" in cap_props: dns_name = cap_props["dns_name"].value if cap_props and "ports" in cap_props: From 30d55fff257e0e236c3776f11f71685232640672 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 30 Jan 2017 13:18:04 +0100 Subject: [PATCH 448/509] Fix error #135 --- IM/InfrastructureInfo.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 1c0871d43..a02561a73 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -30,7 +30,7 @@ from IM.openid.JWT import JWT from IM.VirtualMachine import VirtualMachine from IM.auth import Authentication -from IM.tosca import Tosca +from IM.tosca.Tosca import Tosca class IncorrectVMException(Exception): @@ -114,7 +114,10 @@ def serialize(self): if odict['radl']: odict['radl'] = dump_radl_json(odict['radl']) if odict['extra_info'] and "TOSCA" in odict['extra_info']: - odict['extra_info'] = odict['extra_info'].serialize() + if isinstance(odict['extra_info']['TOSCA'], Tosca): + odict['extra_info']['TOSCA'] = odict['extra_info']['TOSCA'].serialize() + else: + odict['extra_info']['TOSCA'] = odict['extra_info']['TOSCA'] return json.dumps(odict) @staticmethod @@ -130,7 +133,8 @@ def deserialize(str_data): if dic['radl']: dic['radl'] = parse_radl_json(dic['radl']) if 'extra_info' in dic and dic['extra_info'] and "TOSCA" in dic['extra_info']: - dic['extra_info'] = Tosca.deserialize(dic['extra_info']) + if not isinstance(dic['extra_info']['TOSCA'], str): + dic['extra_info']['TOSCA'] = Tosca.deserialize(dic['extra_info']['TOSCA']) newinf.__dict__.update(dic) newinf.cloud_connector = None # Set the ConfManager object and the lock to the data loaded From d6fed0cbc4bc8b004a40296b6c8ad95c76d7c2fe Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 1 Feb 2017 09:46:50 +0100 Subject: [PATCH 449/509] Fix bug saving TOSCA data --- IM/InfrastructureInfo.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index a02561a73..0dc5261cf 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -114,10 +114,7 @@ def serialize(self): if odict['radl']: odict['radl'] = dump_radl_json(odict['radl']) if odict['extra_info'] and "TOSCA" in odict['extra_info']: - if isinstance(odict['extra_info']['TOSCA'], Tosca): - odict['extra_info']['TOSCA'] = odict['extra_info']['TOSCA'].serialize() - else: - odict['extra_info']['TOSCA'] = odict['extra_info']['TOSCA'] + odict['extra_info'] = {'TOSCA': odict['extra_info']['TOSCA'].serialize()} return json.dumps(odict) @staticmethod @@ -133,8 +130,7 @@ def deserialize(str_data): if dic['radl']: dic['radl'] = parse_radl_json(dic['radl']) if 'extra_info' in dic and dic['extra_info'] and "TOSCA" in dic['extra_info']: - if not isinstance(dic['extra_info']['TOSCA'], str): - dic['extra_info']['TOSCA'] = Tosca.deserialize(dic['extra_info']['TOSCA']) + dic['extra_info']['TOSCA'] = Tosca.deserialize(dic['extra_info']['TOSCA']) newinf.__dict__.update(dic) newinf.cloud_connector = None # Set the ConfManager object and the lock to the data loaded From 18acb7b39e5bc41bad88df0ed50e3b9f92496f40 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 2 Feb 2017 15:51:03 +0100 Subject: [PATCH 450/509] Fix script with TOSCA data --- scripts/db_1_5_0_to_1_5_1.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/db_1_5_0_to_1_5_1.py b/scripts/db_1_5_0_to_1_5_1.py index 0c377d0da..1c4ce1fa9 100644 --- a/scripts/db_1_5_0_to_1_5_1.py +++ b/scripts/db_1_5_0_to_1_5_1.py @@ -60,6 +60,8 @@ def deserialize_info(str_data): dic['auth'] = Authentication.deserialize(dic['auth']) if dic['radl']: dic['radl'] = parse_radl_json(dic['radl']) + if 'extra_info' in dic and dic['extra_info'] and "TOSCA" in dic['extra_info']: + dic['extra_info']['TOSCA'] = Tosca.deserialize(dic['extra_info']['TOSCA']) newinf.__dict__.update(dic) newinf.cloud_connector = None # Set the ConfManager object and the lock to the data loaded From b31828bc276507389dc2dd05de17b3b7b38f90cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Molt=C3=B3?= Date: Mon, 6 Feb 2017 08:53:38 +0100 Subject: [PATCH 451/509] Update Dockerfile --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a1431b880..54ff374f7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,7 +16,7 @@ RUN apt-get update && apt-get install -y \ libssl-dev \ libffi-dev \ libmysqld-dev \ - python-pysqlite2 + python-pysqlite2 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* From d3faf0b13d39f47e3ac278284ee0eb8baa4e8efc Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Feb 2017 17:33:55 +0100 Subject: [PATCH 452/509] Use correct version of cherrypy --- docker/Dockerfile | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 54ff374f7..6adb25c20 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,8 @@ RUN apt-get update && apt-get install -y \ # Install CherryPy to enable HTTPS in REST API RUN pip install setuptools --upgrade -I -RUN pip install pbr CherryPy pyOpenSSL --upgrade -I +RUN pip install CherryPy==8.9.1 +RUN pip install pbr pyOpenSSL --upgrade -I # Install pip optional libraries RUN pip install MySQL-python msrest msrestazure azure-common azure-mgmt-storage azure-mgmt-compute azure-mgmt-network azure-mgmt-resource diff --git a/setup.py b/setup.py index c7af53e7b..da1bd4745 100644 --- a/setup.py +++ b/setup.py @@ -58,5 +58,5 @@ platforms=["any"], install_requires=["ansible >= 1.8", "paramiko >= 1.14", "PyYAML", "suds", "pysqlite", "boto >= 2.29", "apache-libcloud >= 0.17", "RADL", "bottle", "netaddr", "requests", - "scp", "cherrypy"] + "scp", "cherrypy <= 8.9.1"] ) From 691b63f506af4073071b48ca8f9d0d48acc129e5 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 7 Feb 2017 10:43:52 +0100 Subject: [PATCH 453/509] Add ctxt_log attribute to the INDIGO Compute --- IM/tosca/Tosca.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 19f16673b..36c52b507 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -720,6 +720,13 @@ def _get_attribute_result(self, func, node, inf_info): return vm.id elif attribute_name == "tosca_name": return node.name + elif attribute_name == "ctxt_log": + if node.type == "tosca.nodes.indigo.Compute": + return vm.cont_out + else: + Tosca.logger.warn("Attribute ctxt_log only supported" + " in tosca.nodes.indigo.Compute nodes.") + return None elif attribute_name == "credential" and capability_name == "endpoint": if node.type == "tosca.nodes.indigo.Compute": res = [] From dfba3babf8d69fb9a03a7d5b2ab4296748c972c4 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 7 Feb 2017 15:51:25 +0100 Subject: [PATCH 454/509] Install python requests from ubuntu repos --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 6adb25c20..0cd830e8c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,6 +17,7 @@ RUN apt-get update && apt-get install -y \ libffi-dev \ libmysqld-dev \ python-pysqlite2 \ + python-requests \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* From 3bda399752c54e10d824d8b199b7f4c307df8450 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 14 Feb 2017 12:44:19 +0100 Subject: [PATCH 455/509] Fix issue #145 --- IM/connectors/OpenNebula.py | 37 +---------- IM/tts/onetts.py | 45 +++++++++++++ IM/tts/tts.py | 125 +++++++++++++++++++----------------- test/unit/onetts.py | 51 +++++++++++++++ test/unit/tts.py | 96 ++++++++++++++------------- 5 files changed, 212 insertions(+), 142 deletions(-) create mode 100644 IM/tts/onetts.py create mode 100755 test/unit/onetts.py diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 31d404154..af3187047 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -26,8 +26,7 @@ from IM.config import ConfigOpenNebula from netaddr import IPNetwork, IPAddress from IM.config import Config -from IM.tts.tts import TTSClient -from IM.openid.JWT import JWT +from IM.tts.onetts import ONETTSClient # Set of classes to parse the XML results of the ONE API @@ -188,37 +187,6 @@ def concreteSystem(self, radl_system, auth_data): return res - def get_auth_from_tts(self, token): - """ - Get username and password from the TTS service - """ - tts_uri = uriparse(ConfigOpenNebula.TTS_URL) - scheme = tts_uri[0] - host = tts_uri[1] - port = None - if host.find(":") != -1: - parts = host.split(":") - host = parts[0] - port = int(parts[1]) - - decoded_token = JWT.get_info(token) - ttsc = TTSClient(token, decoded_token['iss'], host, port, scheme) - - success, svc = ttsc.find_service("opennebula", self.cloud.server) - if not success: - raise Exception("Error getting credentials from TTS: %s" % svc) - succes, cred = ttsc.request_credential(svc["id"]) - if succes: - username = password = None - for elem in cred: - if elem['name'] == 'Username': - username = elem['value'] - elif elem['name'] == 'Password': - password = elem['value'] - return username, password - else: - raise Exception("Error getting credentials from TTS: %s" % cred) - def getSessionID(self, auth_data, hash_password=None): """ Get the ONE Session ID from the auth data @@ -246,7 +214,8 @@ def getSessionID(self, auth_data, hash_password=None): return auth['username'] + ":" + passwd elif 'token' in auth: - username, passwd = self.get_auth_from_tts(auth['token']) + username, passwd = ONETTSClient.get_auth_from_tts(ConfigOpenNebula.TTS_URL, + self.cloud.server, auth['token']) if not username or not passwd: raise Exception("Error getting ONE credentials using TTS.") auth["username"] = username diff --git a/IM/tts/onetts.py b/IM/tts/onetts.py new file mode 100644 index 000000000..c3db4480d --- /dev/null +++ b/IM/tts/onetts.py @@ -0,0 +1,45 @@ +''' +Created on 16 de jun. de 2016 + +@author: micafer +''' + +from IM.uriparse import uriparse +from IM.tts.tts import TTSClient + + +class ONETTSClient(): + """ + Class to interact get user/password credentials to OpenNebula using the TTS + """ + + @staticmethod + def get_auth_from_tts(tts_url, one_server, token): + """ + Get username and password from the TTS service + """ + tts_uri = uriparse(tts_url) + scheme = tts_uri[0] + host = tts_uri[1] + port = None + if host.find(":") != -1: + parts = host.split(":") + host = parts[0] + port = int(parts[1]) + + ttsc = TTSClient(token, host, port, scheme) + + success, svc = ttsc.find_service(one_server) + if not success: + raise Exception("Error getting credentials from TTS: %s" % svc) + succes, cred = ttsc.request_credential(svc["id"]) + if succes: + username = password = None + for elem in cred['credential']['entries']: + if elem['name'] == 'Username': + username = elem['value'] + elif elem['name'] == 'Password': + password = elem['value'] + return username, password + else: + raise Exception("Error getting credentials from TTS: %s" % cred) diff --git a/IM/tts/tts.py b/IM/tts/tts.py index adcb774a6..22637901d 100644 --- a/IM/tts/tts.py +++ b/IM/tts/tts.py @@ -5,90 +5,63 @@ ''' import json -import httplib +import requests class TTSClient: """ - Class to interact with the TTS + Class to interact with the TTS using v2 of the REST API https://github.com/indigo-dc/tts """ - def __init__(self, token, iss, host, port=None, uri_scheme=None): + def __init__(self, token, host, port=None, uri_scheme=None, ssl_verify=False): self.host = host self.port = port if not self.port: self.port = 8080 self.token = token - self.iss = iss self.uri_scheme = uri_scheme if not self.uri_scheme: self.uri_scheme = "http" + self.ssl_verify = ssl_verify - def _get_http_connection(self): - """ - Get the HTTP connection to contact the TTS server - """ - if self.uri_scheme == 'https': - conn = httplib.HTTPSConnection(self.host, self.port) - else: - conn = httplib.HTTPConnection(self.host, self.port) - - return conn - - def _perform_get(self, url): + def _perform_get(self, url, headers={}): """ Perform the GET operation on the TTS with the specified URL """ - headers = {} - headers['Authorization'] = 'Bearer %s' % self.token - headers['Content-Type'] = 'application/json' - headers['X-OpenId-Connect-Issuer'] = self.iss - conn = self._get_http_connection() - conn.request('GET', url, headers=headers) - resp = conn.getresponse() - output = resp.read() - - if resp.status >= 200 and resp.status <= 299: - return True, output + url = "%s://%s:%s%s" % (self.uri_scheme, self.host, self.port, url) + resp = requests.request("GET", url, verify=self.ssl_verify, headers=headers) + + if resp.status_code >= 200 and resp.status_code <= 299: + return True, resp.text else: - return False, "Error code %d. Msg: %s" % (resp.status, output) + return False, "Error code %d. Msg: %s" % (resp.status_code, resp.text) - def _perform_post(self, url, body): + def _perform_post(self, url, headers, body): """ - Perform the POR operation on the TTS with the specified URL + Perform the POST operation on the TTS with the specified URL and using the body specified """ - conn = self._get_http_connection() - - conn.putrequest('POST', url) - - conn.putheader('Authorization', 'Bearer %s' % self.token) - conn.putheader('Content-Type', 'application/json') - conn.putheader('X-OpenId-Connect-Issuer', self.iss) - - conn.putheader('Content-Length', len(body)) - conn.endheaders(body) - - resp = conn.getresponse() - output = str(resp.read()) - - if resp.status == 303: - # in case of redirection get the response from the new URL - return self._perform_get(resp.msg['location']) - elif resp.status >= 200 and resp.status <= 299: - return True, output + url = "%s://%s:%s%s" % (self.uri_scheme, self.host, self.port, url) + resp = requests.request("POST", url, verify=self.ssl_verify, headers=headers, data=body) + if resp.status_code >= 200 and resp.status_code <= 299: + return True, resp.text else: - return False, "Error code %d. Msg: %s" % (resp.status, output) + return False, "Error code %d. Msg: %s" % (resp.status_code, resp.text) def request_credential(self, sid): """ Request a credential for the specified service """ + success, provider = self.get_provider() + if not success: + return False, provider + body = '{"service_id":"%s"}' % sid - url = "/api/credential/" + url = "/api/v2/%s/credential" % provider["id"] try: - success, res = self._perform_post(url, body) + headers = {'Authorization': 'Bearer %s' % self.token, 'Content-Type': 'application/json'} + success, res = self._perform_post(url, headers, body) except Exception, ex: success = False res = str(ex) @@ -97,11 +70,11 @@ def request_credential(self, sid): else: return False, res - def list_endservices(self): + def list_providers(self): """ - Get the list of services + Get the list of providers """ - url = "/api/service" + url = "/api/v2/oidcp" try: success, output = self._perform_get(url) except Exception, ex: @@ -112,14 +85,48 @@ def list_endservices(self): else: return True, json.loads(output) - def find_service(self, stype, host): + def list_endservices(self, provider): """ - Find a service with the specified type and host values + Get the list of services """ - success, services = self.list_endservices() + url = "/api/v2/%s/service" % provider + try: + headers = {'Authorization': 'Bearer %s' % self.token} + success, output = self._perform_get(url, headers) + except Exception, ex: + success = False + output = str(ex) + if not success: + return False, output + else: + return True, json.loads(output) + + def get_provider(self): + """ + Get the first provider available + """ + success, providers = self.list_providers() + if not success: + return False, providers + else: + if providers['openid_provider_list']: + return True, providers['openid_provider_list'][0] + else: + return False, "No provider found." + + def find_service(self, host): + """ + Find a service for the specified host + """ + success, provider = self.get_provider() + if not success: + return False, provider + + success, services = self.list_endservices(provider["id"]) if success: for service in services["service_list"]: - if service["type"] == stype and service["host"] == host: + # we assume that if the host appears in the description it is our service + if service["description"].find(host) != -1: return True, service else: return False, services diff --git a/test/unit/onetts.py b/test/unit/onetts.py new file mode 100755 index 000000000..7017ead4b --- /dev/null +++ b/test/unit/onetts.py @@ -0,0 +1,51 @@ +#! /usr/bin/env python +# +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from IM.uriparse import uriparse +from IM.tts.onetts import ONETTSClient +from mock import patch, MagicMock + + +class TestONETTSClient(unittest.TestCase): + """ + Class to test the OneTTSClient class + """ + @patch('IM.tts.onetts.TTSClient') + def test_list_providers(self, ttscli): + tts = MagicMock() + ttscli.return_value = tts + tts.get_provider.return_value = True, {"id": "iam"} + tts.find_service.return_value = True, {"id": "sid"} + tts.request_credential.return_value = True, {"credential": {"entries": [ + {'name': 'Username', 'type': 'text', 'value': 'username'}, + {'name': 'Password', 'type': 'text', 'value': 'password'}]}} + + token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" + "LCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE" + "0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi" + "0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_" + "oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0") + username, password = ONETTSClient.get_auth_from_tts("https://localhost:8443", "oneserver", token) + + self.assertEqual(username, "username", msg="ERROR: getting one auth from TTS, incorrect username.") + self.assertEqual(password, "password", msg="ERROR: getting one auth from TTS, incorrect password.") + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/tts.py b/test/unit/tts.py index bf32e0b4f..d71efe58e 100755 --- a/test/unit/tts.py +++ b/test/unit/tts.py @@ -17,10 +17,9 @@ # along with this program. If not, see . import unittest -import os +from IM.uriparse import uriparse from IM.tts.tts import TTSClient -from radl import radl_parse from mock import patch, MagicMock @@ -30,83 +29,82 @@ class TestTTSClient(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls.last_op = None, None token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcwNjYi" "LCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJpYXQiOjE" "0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xdGAZWIOKtx9Wi" "0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiCIwQ3JBz9azBT1o_" "oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0") - iss = "https://iam-test.indigo-datacloud.eu/" - cls.ttsc = TTSClient(token, iss, "localhost") - - def get_response(self): - method, url = self.__class__.last_op + cls.ttsc = TTSClient(token, "localhost") + def get_response(self, method, url, verify=False, cert=None, headers={}, data=None): resp = MagicMock() + parts = uriparse(url) + url = parts[2] if method == "GET": - if "/api/credential/somecred" == url: - resp.status = 200 - resp.read.return_value = ('[{"name": "Username", "type": "text", "value": "username"},' - '{"name": "Password", "type": "text", "value": "password"}]') - if "/api/service" == url: - resp.status = 200 - resp.read.return_value = ('{"service_list": [{"id":"sid", "type":"stype", "host": "shost"}]}') + if "/api/v2/oidcp" == url: + resp.status_code = 200 + resp.text = '{"openid_provider_list": [{"id": "iam"}]}' + elif "/api/v2/iam/service" == url: + resp.status_code = 200 + resp.text = ('{"service_list": [{"id":"sid", "description": "shost"}]}') + else: + resp.status_code = 400 elif method == "POST": - if url == "/api/credential/": - resp.status = 303 - resp.msg = {'location': "/api/credential/somecred"} + if url == "/api/v2/iam/credential": + resp.status_code = 200 + resp.text = ('{ "credential": { "entries": [{"name": "Username", "type": "text", "value": "username"},' + '{"name": "Password", "type": "text", "value": "password"}]}}') + else: + resp.status_code = 401 + else: + resp.status_code = 402 return resp - def request(self, method, url, body=None, headers={}): - self.__class__.last_op = method, url + @patch('requests.request') + def test_list_providers(self, requests): + requests.side_effect = self.get_response + + success, providers = self.ttsc.list_providers() + + expected_providers = {"openid_provider_list": [{"id": "iam"}]} - @patch('httplib.HTTPConnection') - def test_list_endservices(self, connection): - conn = MagicMock() - connection.return_value = conn + self.assertTrue(success, msg="ERROR: getting providers: %s." % providers) + self.assertEqual(providers, expected_providers, msg="ERROR: getting providers: Unexpected providers.") - conn.request.side_effect = self.request - conn.putrequest.side_effect = self.request - conn.getresponse.side_effect = self.get_response + @patch('requests.request') + def test_list_endservices(self, requests): + requests.side_effect = self.get_response - success, services = self.ttsc.list_endservices() + _, provider = self.ttsc.get_provider() + success, services = self.ttsc.list_endservices(provider["id"]) - expected_services = {"service_list": [{"id": "sid", "type": "stype", "host": "shost"}]} + expected_services = {"service_list": [{"id": "sid", "description": "shost"}]} self.assertTrue(success, msg="ERROR: getting services: %s." % services) self.assertEqual(services, expected_services, msg="ERROR: getting services: Unexpected services.") - @patch('httplib.HTTPConnection') - def test_find_service(self, connection): - conn = MagicMock() - connection.return_value = conn + @patch('requests.request') + def test_find_service(self, requests): + requests.side_effect = self.get_response - conn.request.side_effect = self.request - conn.putrequest.side_effect = self.request - conn.getresponse.side_effect = self.get_response + success, service = self.ttsc.find_service("shost") - success, service = self.ttsc.find_service("stype", "shost") - - expected_service = {"id": "sid", "type": "stype", "host": "shost"} + expected_service = {"id": "sid", "description": "shost"} self.assertTrue(success) self.assertEqual(service, expected_service) - @patch('httplib.HTTPConnection') - def test_request_credential(self, connection): - conn = MagicMock() - connection.return_value = conn - - conn.request.side_effect = self.request - conn.putrequest.side_effect = self.request - conn.getresponse.side_effect = self.get_response + @patch('requests.request') + def test_request_credential(self, requests): + requests.side_effect = self.get_response success, cred = self.ttsc.request_credential("sid") - expected_cred = [{'name': 'Username', 'type': 'text', 'value': 'username'}, - {'name': 'Password', 'type': 'text', 'value': 'password'}] + expected_cred = {"credential": {"entries": [ + {'name': 'Username', 'type': 'text', 'value': 'username'}, + {'name': 'Password', 'type': 'text', 'value': 'password'}]}} self.assertTrue(success, msg="ERROR: getting credentials: %s." % cred) self.assertEqual(cred, expected_cred, msg="ERROR: getting credentials: Unexpected credetials.") From d4b50ea6bae982706261a8a6351382408d2a55d4 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 15 Feb 2017 11:51:18 +0100 Subject: [PATCH 456/509] Style changes --- IM/REST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index fdb26ca10..407bc1c84 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -96,7 +96,7 @@ def run(self, handler): except: from cherrypy import wsgiserver server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler, request_queue_size=32) - + self.srv = server try: server.start() From 90a43dd83304167bed84622782b27f58c21e0b1c Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 15 Feb 2017 18:38:10 +0100 Subject: [PATCH 457/509] convert token to str --- IM/openid/JWT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/openid/JWT.py b/IM/openid/JWT.py index 9355aa642..01e5cb430 100644 --- a/IM/openid/JWT.py +++ b/IM/openid/JWT.py @@ -68,6 +68,6 @@ def get_info(token): :param token: The JWT token """ - part = tuple(token.split(b".")) + part = tuple(str(token).split(b".")) part = [JWT.b64d(p) for p in part] return json.loads(part[1]) From c7559369e162ba711a52353433dbf76b95531d36 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 16 Feb 2017 09:45:38 +0100 Subject: [PATCH 458/509] Check IAM token expiration time. Issue #148 --- IM/InfrastructureManager.py | 25 +++++++++---- IM/openid/OpenIDClient.py | 69 ++++++++++++++++++++++------------- test/files/iam_user_info.json | 2 +- test/unit/test_im_logic.py | 44 ++++++++++++++-------- 4 files changed, 90 insertions(+), 50 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index ef6b81833..4c6db05d2 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1188,17 +1188,26 @@ def check_oidc_token(im_auth): token = im_auth["token"] success = False try: - # decode the token to get the issuer + # decode the token to get the info decoded_token = JWT().get_info(token) - if decoded_token['iss'] in Config.OIDC_ISSUERS: - success, userinfo = OpenIDClient.get_user_info_request(token) - if success: - # convert to username to use it in the rest of the IM - im_auth['username'] = str(userinfo.get("preferred_username")) - im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) - else: + + # First check if the issuer is in valid + if decoded_token['iss'] not in Config.OIDC_ISSUERS: InfrastructureManager.logger.error("Incorrect OIDC issuer: %s" % decoded_token['iss']) raise InvaliddUserException("Invalid InfrastructureManager credentials. Issuer not accepted.") + + # Now check if the token is not expired + expired, msg = OpenIDClient.is_access_token_expired(token) + if expired: + InfrastructureManager.logger.error("OIDC auth %s." % msg) + raise InvaliddUserException("Invalid InfrastructureManager credentials. OIDC auth %s." % msg) + + # Now try to get user info + success, userinfo = OpenIDClient.get_user_info_request(token) + if success: + # convert to username to use it in the rest of the IM + im_auth['username'] = str(userinfo.get("preferred_username")) + im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) except Exception, ex: InfrastructureManager.logger.exception("Error trying to validate OIDC auth token: %s" % str(ex)) raise Exception("Error trying to validate OIDC auth token: %s" % str(ex)) diff --git a/IM/openid/OpenIDClient.py b/IM/openid/OpenIDClient.py index e1732f799..6b91ed509 100644 --- a/IM/openid/OpenIDClient.py +++ b/IM/openid/OpenIDClient.py @@ -16,29 +16,15 @@ ''' Class to contact with an OpenID server ''' -import httplib -import urlparse +import requests import json +import time from JWT import JWT class OpenIDClient(object): - @staticmethod - def get_connection(url): - """ - Get a HTTP/S connection with the specified server. - """ - parsed_url = urlparse.urlparse(url) - port = None - server = parsed_url[1] - if parsed_url[1].find(":") != -1: - parts = parsed_url[1].split(":") - server = parts[0] - port = int(parts[1]) - if parsed_url[0] == "https": - return httplib.HTTPSConnection(server, port) - else: - return httplib.HTTPConnection(server, port) + + VERIFY_SSL = False @staticmethod def get_user_info_request(token): @@ -48,13 +34,46 @@ def get_user_info_request(token): try: decoded_token = JWT().get_info(token) headers = {'Authorization': 'Bearer %s' % token} - conn = OpenIDClient.get_connection(decoded_token['iss']) - conn.request('GET', "/userinfo", headers=headers) - resp = conn.getresponse() + url = "%s%s" % (decoded_token['iss'], "/userinfo") + resp = requests.request("GET", url, verify=OpenIDClient.VERIFY_SSL, headers=headers) + if resp.status_code != 200: + return False, "Code: %d. Message: %s." % (resp.status_code, resp.text) + return True, json.loads(resp.text) + except Exception, ex: + return False, str(ex) - output = resp.read() - if resp.status != 200: - return False, resp.reason + "\n" + output - return True, json.loads(output) + @staticmethod + def get_token_introspection(token, client_id, client_secret): + """ + Get token introspection + """ + try: + decoded_token = JWT().get_info(token) + url = "%s%s" % (decoded_token['iss'], "/introspect?token=%s&token_type_hint=access_token" % token) + resp = requests.request("GET", url, verify=OpenIDClient.VERIFY_SSL, + auth=requests.auth.HTTPBasicAuth(client_id, client_secret)) + if resp.status_code != 200: + return False, "Code: %d. Message: %s." % (resp.status_code, resp.text) + return True, json.loads(resp.text) except Exception, ex: return False, str(ex) + + @staticmethod + def is_access_token_expired(token): + """ + Check if the current access token is expired + """ + if token: + try: + decoded_token = JWT().get_info(token) + now = int(time.time()) + expires = int(decoded_token['exp']) + validity = expires - now + if validity < 0: + return True, "Token expired" + else: + return False, "Valid Token for %d seconds" % validity + except: + return True, "Error getting token info" + else: + return True, "No token specified" diff --git a/test/files/iam_user_info.json b/test/files/iam_user_info.json index bea6cf394..7bc8b105c 100644 --- a/test/files/iam_user_info.json +++ b/test/files/iam_user_info.json @@ -17,4 +17,4 @@ } ], "organisation_name": "indigo-dc" -} +} \ No newline at end of file diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index d536cf90c..4671e0cb4 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -21,6 +21,7 @@ import logging import unittest import sys +import json from mock import Mock, patch, MagicMock @@ -651,8 +652,7 @@ def test_tosca_get_outputs(self): 'token': 'pass', 'user': 'ubuntu'}}) - @patch('httplib.HTTPSConnection') - def test_check_oidc_token(self, connection): + def test_check_oidc_invalid_token(self): im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" "joxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg" @@ -660,20 +660,11 @@ def test_check_oidc_token(self, connection): "me5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXU" "z5BzxBvANko")} - user_info = read_file_as_string('../files/iam_user_info.json') - - conn = MagicMock() - connection.return_value = conn - - resp = MagicMock() - resp.status = 200 - resp.read.return_value = user_info - conn.getresponse.return_value = resp - - IM.check_oidc_token(im_auth) - - self.assertEqual(im_auth['username'], "micafer") - self.assertEqual(im_auth['password'], "https://iam-test.indigo-datacloud.eu/sub") + with self.assertRaises(Exception) as ex: + IM.check_oidc_token(im_auth) + self.assertEqual(str(ex.exception), + ("Error trying to validate OIDC auth token: Invalid " + "InfrastructureManager credentials. OIDC auth Token expired.")) Config.OIDC_ISSUERS = ["https://other_issuer"] @@ -683,6 +674,27 @@ def test_check_oidc_token(self, connection): ("Error trying to validate OIDC auth token: Invalid " "InfrastructureManager credentials. Issuer not accepted.")) + @patch('IM.InfrastructureManager.OpenIDClient') + def test_check_oidc_valid_token(self, openidclient): + im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" + "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" + "joxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg" + "5ODM1N2FiYSJ9.DwpZizVaYtvIj7fagQqDFpDh96szFupf6BNMIVLcopqQtZ9dBvwN9lgZ_w7Htvb3r-erho_hc" + "me5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXU" + "z5BzxBvANko")} + + user_info = json.loads(read_file_as_string('../files/iam_user_info.json')) + + openidclient.is_access_token_expired.return_value = False, "Valid Token for 100 seconds" + openidclient.get_user_info_request.return_value = True, user_info + + Config.OIDC_ISSUERS = ["https://iam-test.indigo-datacloud.eu/"] + + IM.check_oidc_token(im_auth) + + self.assertEqual(im_auth['username'], "micafer") + self.assertEqual(im_auth['password'], "https://iam-test.indigo-datacloud.eu/sub") + def test_db(self): """ Test DB data access """ inf = InfrastructureInfo() From dfa13c347e12a6984f635d9114c9edfd40f6525f Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 16 Feb 2017 10:58:34 +0100 Subject: [PATCH 459/509] Return correct 401 error. --- IM/InfrastructureManager.py | 22 +++++++++++++--------- test/unit/test_im_logic.py | 6 ++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 4c6db05d2..3293013c4 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1190,18 +1190,22 @@ def check_oidc_token(im_auth): try: # decode the token to get the info decoded_token = JWT().get_info(token) + except Exception, ex: + InfrastructureManager.logger.exception("Error trying decode OIDC auth token: %s" % str(ex)) + raise Exception("Error trying to decode OIDC auth token: %s" % str(ex)) - # First check if the issuer is in valid - if decoded_token['iss'] not in Config.OIDC_ISSUERS: - InfrastructureManager.logger.error("Incorrect OIDC issuer: %s" % decoded_token['iss']) - raise InvaliddUserException("Invalid InfrastructureManager credentials. Issuer not accepted.") + # First check if the issuer is in valid + if decoded_token['iss'] not in Config.OIDC_ISSUERS: + InfrastructureManager.logger.error("Incorrect OIDC issuer: %s" % decoded_token['iss']) + raise InvaliddUserException("Invalid InfrastructureManager credentials. Issuer not accepted.") - # Now check if the token is not expired - expired, msg = OpenIDClient.is_access_token_expired(token) - if expired: - InfrastructureManager.logger.error("OIDC auth %s." % msg) - raise InvaliddUserException("Invalid InfrastructureManager credentials. OIDC auth %s." % msg) + # Now check if the token is not expired + expired, msg = OpenIDClient.is_access_token_expired(token) + if expired: + InfrastructureManager.logger.error("OIDC auth %s." % msg) + raise InvaliddUserException("Invalid InfrastructureManager credentials. OIDC auth %s." % msg) + try: # Now try to get user info success, userinfo = OpenIDClient.get_user_info_request(token) if success: diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 4671e0cb4..4bee8f89a 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -663,16 +663,14 @@ def test_check_oidc_invalid_token(self): with self.assertRaises(Exception) as ex: IM.check_oidc_token(im_auth) self.assertEqual(str(ex.exception), - ("Error trying to validate OIDC auth token: Invalid " - "InfrastructureManager credentials. OIDC auth Token expired.")) + 'Invalid InfrastructureManager credentials. OIDC auth Token expired.') Config.OIDC_ISSUERS = ["https://other_issuer"] with self.assertRaises(Exception) as ex: IM.check_oidc_token(im_auth) self.assertEqual(str(ex.exception), - ("Error trying to validate OIDC auth token: Invalid " - "InfrastructureManager credentials. Issuer not accepted.")) + "Invalid InfrastructureManager credentials. Issuer not accepted.") @patch('IM.InfrastructureManager.OpenIDClient') def test_check_oidc_valid_token(self, openidclient): From a06e6502c1cdc07d783cc27b29352185da9c064d Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Feb 2017 09:41:12 +0100 Subject: [PATCH 460/509] Fix issue #149 --- IM/tosca/Tosca.py | 143 ++++++++++++++++++------------------- test/files/tosca_long.yml | 21 +++++- test/unit/test_im_logic.py | 26 ------- 3 files changed, 89 insertions(+), 101 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 36c52b507..8913332f6 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -75,14 +75,14 @@ def to_radl(self, inf_info=None): else: if root_type == "tosca.nodes.Compute": # Add the system RADL element - sys = Tosca._gen_system(node, self.tosca.nodetemplates) + sys = self._gen_system(node, self.tosca.nodetemplates) # add networks using the simple method with the public_ip # property - Tosca._add_node_nets( + self._add_node_nets( node, radl, sys, self.tosca.nodetemplates) radl.systems.append(sys) # Add the deploy element for this system - min_instances, _, default_instances, count, removal_list = Tosca._get_scalable_properties( + min_instances, _, default_instances, count, removal_list = self._get_scalable_properties( node) if count is not None: # we must check the correct number of instances to @@ -108,7 +108,7 @@ def to_radl(self, inf_info=None): compute = node else: # Select the host to host this element - compute = Tosca._find_host_compute( + compute = self._find_host_compute( node, self.tosca.nodetemplates) if not compute: Tosca.logger.warn( @@ -195,9 +195,7 @@ def _format_outports(ports_dict): return res - @staticmethod - def _add_node_nets(node, radl, system, nodetemplates): - + def _add_node_nets(self, node, radl, system, nodetemplates): # Find associated Networks nets = Tosca._get_bind_networks(node, nodetemplates) if nets: @@ -217,7 +215,7 @@ def _add_node_nets(node, radl, system, nodetemplates): # This is the solution using the public_ip property node_props = node.get_properties() if node_props and "public_ip" in node_props: - public_ip = node_props["public_ip"].value + public_ip = self._final_function_result(node_props["public_ip"].value, node) # This is the solution using endpoints net_provider_id = None @@ -228,20 +226,48 @@ def _add_node_nets(node, radl, system, nodetemplates): if "endpoint" in node_caps: cap_props = node_caps["endpoint"].get_properties() if cap_props and "network_name" in cap_props: - if cap_props["network_name"].value == "PUBLIC": + network_name = str(self._final_function_result(cap_props["network_name"].value, node)) + if network_name == "PUBLIC": public_ip = True # In this case the user is specifying the provider_id - elif str(cap_props["network_name"].value).endswith(".PUBLIC"): + elif network_name.endswith(".PUBLIC"): public_ip = True - parts = cap_props["network_name"].value.split(".") + parts = network_name.split(".") net_provider_id = ".".join(parts[:-1]) - elif str(cap_props["network_name"].value).endswith(".PRIVATE"): - parts = cap_props["network_name"].value.split(".") + elif network_name.endswith(".PRIVATE"): + parts = network_name.split(".") net_provider_id = ".".join(parts[:-1]) if cap_props and "dns_name" in cap_props: - dns_name = cap_props["dns_name"].value + dns_name = self._final_function_result(cap_props["dns_name"].value, node) if cap_props and "ports" in cap_props: - ports = cap_props["ports"].value + ports = self._final_function_result(cap_props["ports"].value, node) + + # The private net is always added + private_nets = [] + for net in radl.networks: + if not net.isPublic(): + private_nets.append(net) + + if private_nets: + private_net = None + for net in private_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + private_net = net + break + + if not private_net: + # There are a public net but it has not been used in this + # VM + private_net = private_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + private_net = network.createNetwork("private_net", False) + radl.networks.append(private_net) + num_net = system.getNumNetworkIfaces() + + system.setValue('net_interface.' + str(num_net) + '.connection', private_net.id) # If the node needs a public IP if public_ip: @@ -276,56 +302,30 @@ def _add_node_nets(node, radl, system, nodetemplates): system.setValue('net_interface.%d.connection' % num_net, public_net.id) - # The private net is always added - private_nets = [] - for net in radl.networks: - if not net.isPublic(): - private_nets.append(net) - - if private_nets: - private_net = None - for net in private_nets: - num_net = system.getNumNetworkWithConnection(net.id) - if num_net is not None: - private_net = net - break - - if not private_net: - # There are a public net but it has not been used in this - # VM - private_net = private_nets[0] - num_net = system.getNumNetworkIfaces() - else: - # There no public net, create one - private_net = network.createNetwork("private_net", False) - radl.networks.append(private_net) - num_net = system.getNumNetworkIfaces() - - system.setValue('net_interface.' + str(num_net) + '.connection', private_net.id) + if not public_ip and net_provider_id: + private_net.setValue("provider_id", net_provider_id) if dns_name: system.setValue('net_interface.0.dns_name', dns_name) - if not public_ip and net_provider_id: - private_net.setValue("provider_id", net_provider_id) - @staticmethod - def _get_scalable_properties(node): + def _get_scalable_properties(self, node): count = min_instances = max_instances = default_instances = None removal_list = [] scalable = node.get_capability("scalable") if scalable: for prop in scalable.get_properties_objects(): if prop.value is not None: + final_value = self._final_function_result(prop.value, node) if prop.name == "count": - count = prop.value + count = final_value elif prop.name == "max_instances": - max_instances = prop.value + max_instances = final_value elif prop.name == "min_instances": - min_instances = prop.value + min_instances = final_value elif prop.name == "default_instances": - default_instances = prop.value + default_instances = final_value elif prop.name == "removal_list": - removal_list = prop.value + removal_list = final_value return min_instances, max_instances, default_instances, count, removal_list @@ -864,8 +864,7 @@ def _final_function_result(self, func, node, inf_info=None): pass return func - @staticmethod - def _find_host_compute(node, nodetemplates): + def _find_host_compute(self, node, nodetemplates): """ Select the node to host each node, using the node requirements In most of the cases the are directly specified, otherwise "node_filter" is used @@ -883,7 +882,7 @@ def _find_host_compute(node, nodetemplates): if root_type == "tosca.nodes.Compute": return n else: - return Tosca._find_host_compute(n, nodetemplates) + return self._find_host_compute(n, nodetemplates) # There are no direct HostedOn node # check node_filter requirements @@ -894,12 +893,11 @@ def _find_host_compute(node, nodetemplates): if isinstance(value, dict): if 'node_filter' in value: node_filter = value.get('node_filter') - return Tosca._get_compute_from_node_filter(node_filter, nodetemplates) + return self._get_compute_from_node_filter(node_filter, nodetemplates) return None - @staticmethod - def _node_fulfill_filter(node, node_filter): + def _node_fulfill_filter(self, node, node_filter): """ Check if a node fulfills the features of a node filter """ @@ -911,9 +909,9 @@ def _node_fulfill_filter(node, node_filter): for prop in node.get_capability(cap_type).get_properties_objects(): if prop.value is not None: unit = None - value = prop.value + value = self._final_function_result(prop.value, node) if prop.name in ['disk_size', 'mem_size']: - value, unit = Tosca._get_size_and_unit(prop.value) + value, unit = Tosca._get_size_and_unit(value) node_props[prop.name] = (value, unit) filter_props = {} @@ -984,8 +982,7 @@ def _node_fulfill_filter(node, node_filter): return True - @staticmethod - def _get_compute_from_node_filter(node_filter, nodetemplates): + def _get_compute_from_node_filter(self, node_filter, nodetemplates): """ Select the first node that fulfills the specified "node_filter" """ @@ -994,7 +991,7 @@ def _get_compute_from_node_filter(node_filter, nodetemplates): root_type = Tosca._get_root_parent_type(node).type if root_type == "tosca.nodes.Compute": - if Tosca._node_fulfill_filter(node, node_filter.get('capabilities')): + if self._node_fulfill_filter(node, node_filter.get('capabilities')): return node return None @@ -1078,8 +1075,7 @@ def _gen_network(node): return res - @staticmethod - def _add_ansible_roles(node, nodetemplates, system): + def _add_ansible_roles(self, node, nodetemplates, system): """ Find all the roles to be applied to this node and add them to the system as ansible.modules.* in 'disk.0.applications' @@ -1091,7 +1087,7 @@ def _add_ansible_roles(node, nodetemplates, system): compute = other_node else: # Select the host to host this element - compute = Tosca._find_host_compute(other_node, nodetemplates) + compute = self._find_host_compute(other_node, nodetemplates) if compute and compute.name == node.name: # Get the artifacts to see if there is a ansible galaxy role @@ -1114,8 +1110,7 @@ def _add_ansible_roles(node, nodetemplates, system): 'disk.0.applications', 'contains', app_features) system.addFeature(feature) - @staticmethod - def _gen_system(node, nodetemplates): + def _gen_system(self, node, nodetemplates): """ Take a node of type "Compute" and get the RADL.system to represent it """ @@ -1140,9 +1135,9 @@ def _gen_system(node, nodetemplates): name = property_map.get(prop.name, None) if name and prop.value: unit = None - value = prop.value + value = self._final_function_result(prop.value, node) if prop.name in ['disk_size', 'mem_size']: - value, unit = Tosca._get_size_and_unit(prop.value) + value, unit = Tosca._get_size_and_unit(value) elif prop.name == "version": value = str(value) elif prop.name == "image": @@ -1183,7 +1178,7 @@ def _gen_system(node, nodetemplates): res.addFeature(feature) # Find associated BlockStorages - disks = Tosca._get_attached_disks(node, nodetemplates) + disks = self._get_attached_disks(node, nodetemplates) for size, unit, location, device, num, fstype in disks: if size: @@ -1194,7 +1189,7 @@ def _gen_system(node, nodetemplates): res.setValue('disk.%d.mount_path' % num, location) res.setValue('disk.%d.fstype' % num, fstype) - Tosca._add_ansible_roles(node, nodetemplates, res) + self._add_ansible_roles(node, nodetemplates, res) return res @@ -1219,8 +1214,7 @@ def _get_bind_networks(node, nodetemplates): return nets - @staticmethod - def _get_attached_disks(node, nodetemplates): + def _get_attached_disks(self, node, nodetemplates): """ Get the disks attached to a node """ @@ -1241,10 +1235,11 @@ def _get_attached_disks(node, nodetemplates): device = None for prop in props: + value = self._final_function_result(prop.value, node) if prop.name == "location": - location = prop.value + location = value elif prop.name == "device": - device = prop.value + device = value if trgt.type_definition.type == "tosca.nodes.BlockStorage": size, unit = Tosca._get_size_and_unit( diff --git a/test/files/tosca_long.yml b/test/files/tosca_long.yml index b747bcaff..cfa0bd709 100644 --- a/test/files/tosca_long.yml +++ b/test/files/tosca_long.yml @@ -10,6 +10,20 @@ description: > topology_template: + inputs: + + network_name: + type: string + default: vpc-XX.subnet-XX + + access_key: + type: string + default: AKXX + + secret_key: + type: string + default: SKXX + node_templates: elastic_cluster_front_end: @@ -37,7 +51,7 @@ topology_template: endpoint: properties: dns_name: slurmserver - network_name: PUBLIC + network_name: { concat: [ { get_input: network_name }, ".PUBLIC" ] } ports: http_port: protocol: tcp @@ -126,3 +140,8 @@ topology_template: galaxy_url: value: { concat: [ 'http://', get_attribute: [ lrms_server, public_address, 0 ], ':8080' ] } + policies: + - deploy_on_aws: + type: tosca.policies.Placement + properties: { sla_id: SLA_provider-AWS-us-east-1_indigo-dc, username: {get_input: access_key}, password: {get_input: secret_key}} + targets: [ lrms_wn ] diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 4bee8f89a..7170384dd 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -42,7 +42,6 @@ from IM.connectors.CloudConnector import CloudConnector from IM.SSH import SSH from IM.InfrastructureInfo import InfrastructureInfo -from IM.tosca.Tosca import Tosca def read_file_as_string(file_name): @@ -627,31 +626,6 @@ def test_contextualize(self): IM.DestroyInfrastructure(infId, auth0) - def test_tosca_to_radl(self): - """Test TOSCA RADL translation""" - tosca_data = read_file_as_string('../files/tosca_long.yml') - tosca = Tosca(tosca_data) - _, radl = tosca.to_radl() - parse_radl(str(radl)) - - def test_tosca_get_outputs(self): - """Test TOSCA get_outputs function""" - tosca_data = read_file_as_string('../files/tosca_create.yml') - tosca = Tosca(tosca_data) - _, radl = tosca.to_radl() - radl.systems[0].setValue("net_interface.0.ip", "158.42.1.1") - radl.systems[0].setValue("disk.0.os.credentials.username", "ubuntu") - radl.systems[0].setValue("disk.0.os.credentials.password", "pass") - inf = InfrastructureInfo() - vm = VirtualMachine(inf, "1", None, radl, radl, None) - vm.requested_radl = radl - inf.vm_list = [vm] - outputs = tosca.get_outputs(inf) - self.assertEqual(outputs, {'server_url': ['158.42.1.1'], - 'server_creds': {'token_type': 'password', - 'token': 'pass', - 'user': 'ubuntu'}}) - def test_check_oidc_invalid_token(self): im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" From f021742eaacba86787f9e488150b3523317c1fb4 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 17 Feb 2017 10:56:52 +0100 Subject: [PATCH 461/509] Add separate TOSCA tests --- test/unit/Tosca.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100755 test/unit/Tosca.py diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py new file mode 100755 index 000000000..52963e853 --- /dev/null +++ b/test/unit/Tosca.py @@ -0,0 +1,78 @@ +#! /usr/bin/env python +# +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import unittest +import sys + +from mock import Mock, patch, MagicMock + +sys.path.append("..") +sys.path.append(".") + +from IM.VirtualMachine import VirtualMachine +from radl.radl_parse import parse_radl +from IM.InfrastructureInfo import InfrastructureInfo +from IM.tosca.Tosca import Tosca + + +def read_file_as_string(file_name): + tests_path = os.path.dirname(os.path.abspath(__file__)) + abs_file_path = os.path.join(tests_path, file_name) + return open(abs_file_path, 'r').read() + + +class TestTosca(unittest.TestCase): + + def __init__(self, *args): + unittest.TestCase.__init__(self, *args) + + def test_tosca_to_radl(self): + """Test TOSCA RADL translation""" + tosca_data = read_file_as_string('../files/tosca_long.yml') + tosca = Tosca(tosca_data) + _, radl = tosca.to_radl() + radl = parse_radl(str(radl)) + net = radl.get_network_by_id('public_net') + self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') + lrms_wn = radl.get_system_by_name('lrms_wn') + self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) + lrms_server = radl.get_system_by_name('lrms_server') + self.assertEqual(lrms_server.getValue('memory.size'), 1000000000) + self.assertEqual(lrms_server.getValue('net_interface.0.dns_name'), 'slurmserver') + + def test_tosca_get_outputs(self): + """Test TOSCA get_outputs function""" + tosca_data = read_file_as_string('../files/tosca_create.yml') + tosca = Tosca(tosca_data) + _, radl = tosca.to_radl() + radl.systems[0].setValue("net_interface.1.ip", "158.42.1.1") + radl.systems[0].setValue("disk.0.os.credentials.username", "ubuntu") + radl.systems[0].setValue("disk.0.os.credentials.password", "pass") + inf = InfrastructureInfo() + vm = VirtualMachine(inf, "1", None, radl, radl, None) + vm.requested_radl = radl + inf.vm_list = [vm] + outputs = tosca.get_outputs(inf) + self.assertEqual(outputs, {'server_url': ['158.42.1.1'], + 'server_creds': {'token_type': 'password', + 'token': 'pass', + 'user': 'ubuntu'}}) + +if __name__ == "__main__": + unittest.main() From 3a68eb9e32b2f0690ee00435ea0c527f95d5d53f Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 21 Feb 2017 15:22:47 +0100 Subject: [PATCH 462/509] Set https as TTS default protocol --- IM/config.py | 4 ++-- etc/im.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/config.py b/IM/config.py index 95460f028..f7fcc0099 100644 --- a/IM/config.py +++ b/IM/config.py @@ -121,7 +121,7 @@ class ConfigOpenNebula: TEMPLATE_CONTEXT = '' TEMPLATE_OTHER = 'GRAPHICS = [type="vnc",listen="0.0.0.0"]' IMAGE_UNAME = '' - TTS_URL = 'http://localhost:8080' + TTS_URL = 'https://localhost:8443' if config.has_section("OpenNebula"): parse_options(config, 'OpenNebula', ConfigOpenNebula) @@ -129,4 +129,4 @@ class ConfigOpenNebula: # In this case set assume that the TTS server is in the same server if 'IM_SINGLE_SITE_ONE_HOST' in os.environ: - ConfigOpenNebula.TTS_URL = 'http://%s:8080' % os.environ['IM_SINGLE_SITE_ONE_HOST'] + ConfigOpenNebula.TTS_URL = 'https://%s:8443' % os.environ['IM_SINGLE_SITE_ONE_HOST'] diff --git a/etc/im.cfg b/etc/im.cfg index a6723ca7f..b385e43a0 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -132,4 +132,4 @@ TEMPLATE_OTHER = GRAPHICS = [type="vnc",listen="0.0.0.0", keymap="es"] # Set the IMAGE_UNAME value in case of using the name of the disk image in the Template IMAGE_UNAME = oneadmin # URL of the Indigo TTS -TTS_URL = http://localhost:8080 +TTS_URL = https://localhost:8443 From a8b6bc28191beac1325513f85e6601df6bc44145 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 21 Feb 2017 15:23:16 +0100 Subject: [PATCH 463/509] Add / before the image_id --- IM/InfrastructureManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 3293013c4..50077d30e 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -430,7 +430,7 @@ def AddResource(inf_id, radl_data, auth, context=True, failed_clouds=[]): if Config.SINGLE_SITE: image_id = os.path.basename(s.getValue("disk.0.image.url")) - s.setValue("disk.0.image.url", Config.SINGLE_SITE_IMAGE_URL_PREFIX + image_id) + s.setValue("disk.0.image.url", Config.SINGLE_SITE_IMAGE_URL_PREFIX + '/' + image_id) if not s.getValue("disk.0.image.url") and len(vmrc_list) == 0: raise Exception( From 077e96b7a3e59d1c1db70fd55422840bfe5853f0 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 22 Feb 2017 17:05:08 +0100 Subject: [PATCH 464/509] Fix issue #154 --- IM/REST.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/REST.py b/IM/REST.py index 407bc1c84..4501e9b2e 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -184,7 +184,7 @@ def get_auth_header(): "password": user_pass[1]} return Authentication([im_auth, single_site_auth]) elif auth_header.startswith("Bearer "): - token = auth_header[7:] + token = auth_header[7:].strip() im_auth = {"type": "InfrastructureManager", "username": "user", "token": token} From b61eb3944a6f01d50204136def18ae6bbf7d78ac Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 23 Feb 2017 10:32:24 +0100 Subject: [PATCH 465/509] Add openid tests --- test/files/iam_token_info.json | 17 ++++++++ test/unit/openid.py | 78 ++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 test/files/iam_token_info.json create mode 100755 test/unit/openid.py diff --git a/test/files/iam_token_info.json b/test/files/iam_token_info.json new file mode 100644 index 000000000..c58697450 --- /dev/null +++ b/test/files/iam_token_info.json @@ -0,0 +1,17 @@ +{ + "active": true, + "scope": "address phone openid profile offline_access email", + "expires_at": "2016-02-16T09:17:46+0000", + "exp": 1480000000, + "sub": "xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxx", + "user_id": "username", + "client_id": "cid", + "token_type": "Bearer", + "groups": [ + "Users", + "Developers" + ], + "preferred_username": "username", + "organisation_name": "indigo-d", + "email": "me@server.com" +} diff --git a/test/unit/openid.py b/test/unit/openid.py new file mode 100755 index 000000000..70a3061a0 --- /dev/null +++ b/test/unit/openid.py @@ -0,0 +1,78 @@ +#! /usr/bin/env python +# +# IM - Infrastructure Manager +# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest +import os +import json + +from IM.openid.OpenIDClient import OpenIDClient +from mock import patch, MagicMock + + +def read_file_as_string(file_name): + tests_path = os.path.dirname(os.path.abspath(__file__)) + abs_file_path = os.path.join(tests_path, file_name) + return open(abs_file_path, 'r').read() + + +class TestOpenIDClient(unittest.TestCase): + """ + Class to test the TTCLient class + """ + @classmethod + def setUpClass(cls): + cls.token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcw" + "NjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJ" + "pYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xd" + "GAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiC" + "IwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0") + + def test_is_access_token_expired(self): + expired, msg = OpenIDClient.is_access_token_expired(self.token) + + self.assertTrue(expired) + self.assertEqual(msg, "Token expired") + + @patch('requests.request') + def test_get_user_info_request(self, requests): + mock_response = MagicMock() + mock_response.status_code = 200 + user_info = read_file_as_string('../files/iam_user_info.json') + mock_response.text = user_info + requests.return_value = mock_response + + success, user_info_resp = OpenIDClient.get_user_info_request(self.token) + + self.assertTrue(success) + self.assertEqual(json.loads(user_info), user_info_resp) + + @patch('requests.request') + def test_get_token_introspection(self, requests): + mock_response = MagicMock() + mock_response.status_code = 200 + token_info = read_file_as_string('../files/iam_token_info.json') + mock_response.text = token_info + requests.return_value = mock_response + + success, token_info_resp = OpenIDClient.get_token_introspection(self.token, "cid", "csec") + + self.assertTrue(success) + self.assertEqual(json.loads(token_info), token_info_resp) + +if __name__ == '__main__': + unittest.main() From 305399b58222bc6d5122eabad1b9f94beb17b059 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 23 Feb 2017 12:24:12 +0100 Subject: [PATCH 466/509] Fix issue #157 --- IM/InfrastructureManager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 50077d30e..f0cca67cf 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -428,14 +428,14 @@ def AddResource(inf_id, radl_data, auth, context=True, failed_clouds=[]): for system_id in set([d.id for d in radl.deploys if d.vm_number > 0]): s = radl.get_system_by_name(system_id) - if Config.SINGLE_SITE: - image_id = os.path.basename(s.getValue("disk.0.image.url")) - s.setValue("disk.0.image.url", Config.SINGLE_SITE_IMAGE_URL_PREFIX + '/' + image_id) - if not s.getValue("disk.0.image.url") and len(vmrc_list) == 0: raise Exception( "No correct VMRC auth data provided nor image URL") + if Config.SINGLE_SITE: + image_id = os.path.basename(s.getValue("disk.0.image.url")) + s.setValue("disk.0.image.url", Config.SINGLE_SITE_IMAGE_URL_PREFIX + '/' + image_id) + # Remove the requested apps from the system s_without_apps = radl.get_system_by_name(system_id).clone() s_without_apps.delValue("disk.0.applications") From 36f15947d00e8e8f8e1a5565d6205ffd0cc63ac2 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 23 Feb 2017 13:28:18 +0100 Subject: [PATCH 467/509] Add Single site conf in ansible recipe --- ansible_install.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/ansible_install.yaml b/ansible_install.yaml index 8ca676b4b..e54980f10 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -1,5 +1,12 @@ - hosts: localhost connection: local + vars: + # Set True to activate IM in single site more (OpenNebula sites) + SINGLE_SITE: False + # Hostname of the OpenNebula API server + SINGLE_SITE_HOST: onserver.domain.com + # URL of the IAM TTS server + TTS_URL: https://localhost:8443 tasks: - name: Yum install epel-release action: yum pkg=epel-release state=installed @@ -61,3 +68,20 @@ - name: Activate SSH pipelining in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=pipelining value=True + +################################################ Configure IM ################################################### + + - name: Activate SINGLE_SITE in the IM + ini_file: dest=/etc/im/im.cfg section=im option=SINGLE_SITE value=True + when: SINGLE_SITE + + - name: Set SINGLE_SITE_AUTH_HOST in the IM + ini_file: dest=/etc/im/im.cfg section=im option=SINGLE_SITE_AUTH_HOST value="http://{{SINGLE_SITE_HOST}}:2633" + when: SINGLE_SITE + + - name: Set SINGLE_SITE_IMAGE_URL_PREFIX in the IM + ini_file: dest=/etc/im/im.cfg section=im option=SINGLE_SITE_IMAGE_URL_PREFIX value="one://{{SINGLE_SITE_HOST}}/" + when: SINGLE_SITE + + - name: Set TTS_URL in the IM + ini_file: dest=/etc/im/im.cfg section=OpenNebula option=TTS_URL value="{{TTS_URL}}" From f268c2c81dbc900c8c9aff29a6ac3f920af2b7e9 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 3 Mar 2017 12:51:57 +0100 Subject: [PATCH 468/509] Check IAM token audience. Issue #148 --- IM/InfrastructureManager.py | 17 ++++++++++++++ IM/config.py | 1 + etc/im.cfg | 2 ++ test/unit/test_im_logic.py | 45 +++++++++++++++++++++++++++---------- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index f0cca67cf..67efece84 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1199,6 +1199,23 @@ def check_oidc_token(im_auth): InfrastructureManager.logger.error("Incorrect OIDC issuer: %s" % decoded_token['iss']) raise InvaliddUserException("Invalid InfrastructureManager credentials. Issuer not accepted.") + # Now check the audience + if Config.OIDC_AUDIENCE: + if 'aud' in decoded_token and decoded_token['aud']: + found = False + for aud in decoded_token['aud'].split(","): + if aud == Config.OIDC_AUDIENCE: + found = True + break + if found: + InfrastructureManager.logger.debug("Audience %s successfully checked." % Config.OIDC_AUDIENCE) + else: + InfrastructureManager.logger.error("Audience %s not found in access token." % Config.OIDC_AUDIENCE) + raise InvaliddUserException("Invalid InfrastructureManager credentials. Audience not accepted.") + else: + InfrastructureManager.logger.error("Audience %s not found in access token." % Config.OIDC_AUDIENCE) + raise InvaliddUserException("Invalid InfrastructureManager credentials. Audience not accepted.") + # Now check if the token is not expired expired, msg = OpenIDClient.is_access_token_expired(token) if expired: diff --git a/IM/config.py b/IM/config.py index 1c25059d8..6302a1457 100644 --- a/IM/config.py +++ b/IM/config.py @@ -96,6 +96,7 @@ class Config: SINGLE_SITE_AUTH_HOST = '' SINGLE_SITE_IMAGE_URL_PREFIX = '' OIDC_ISSUERS = ["https://iam-test.indigo-datacloud.eu/"] + OIDC_AUDIENCE = None INF_CACHE_TIME = None config = ConfigParser.ConfigParser() diff --git a/etc/im.cfg b/etc/im.cfg index 88c77a6a5..2ad19f6bb 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -117,6 +117,8 @@ SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com/ # List of OIDC issuers supported OIDC_ISSUERS = https://iam-test.indigo-datacloud.eu/ +# If set the IM will check that the string defined here appear in the "aud" claim of the OpenID access token +#OIDC_AUDIENCE = # Time (in seconds) the IM service will maintain the information of an infrastructure # in memory. Only used in case of IM in HA mode. diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 0cbba3659..18b1624ee 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -22,6 +22,7 @@ import unittest import sys import json +import base64 from mock import Mock, patch, MagicMock @@ -114,6 +115,20 @@ def get_cloud_connector_mock(self, name="MyMock0"): cloud.launch = Mock(side_effect=self.gen_launch_res) return cloud + def gen_token(self, aud=None, exp=None): + data = { + "sub": "user_sub", + "iss": "https://iam-test.indigo-datacloud.eu/", + "exp": 1465471354, + "iat": 1465467755, + "jti": "jti", + } + if aud: + data["aud"] = aud + if exp: + data["exp"] = int(time.time()) + exp + return "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.%s.ignored" % base64.urlsafe_b64encode(json.dumps(data)) + def test_inf_creation0(self): """Create infrastructure with empty RADL.""" @@ -651,18 +666,28 @@ def test_contextualize(self): IM.DestroyInfrastructure(infId, auth0) def test_check_oidc_invalid_token(self): - im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" - "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" - "joxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg" - "5ODM1N2FiYSJ9.DwpZizVaYtvIj7fagQqDFpDh96szFupf6BNMIVLcopqQtZ9dBvwN9lgZ_w7Htvb3r-erho_hc" - "me5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXU" - "z5BzxBvANko")} + im_auth = {"token": self.gen_token()} with self.assertRaises(Exception) as ex: IM.check_oidc_token(im_auth) self.assertEqual(str(ex.exception), 'Invalid InfrastructureManager credentials. OIDC auth Token expired.') + im_auth_aud = {"token": self.gen_token(aud="test1,test2")} + + Config.OIDC_AUDIENCE = "test" + with self.assertRaises(Exception) as ex: + IM.check_oidc_token(im_auth_aud) + self.assertEqual(str(ex.exception), + 'Invalid InfrastructureManager credentials. Audience not accepted.') + + Config.OIDC_AUDIENCE = "test2" + with self.assertRaises(Exception) as ex: + IM.check_oidc_token(im_auth_aud) + self.assertEqual(str(ex.exception), + 'Invalid InfrastructureManager credentials. OIDC auth Token expired.') + Config.OIDC_AUDIENCE = None + Config.OIDC_ISSUERS = ["https://other_issuer"] with self.assertRaises(Exception) as ex: @@ -672,12 +697,7 @@ def test_check_oidc_invalid_token(self): @patch('IM.InfrastructureManager.OpenIDClient') def test_check_oidc_valid_token(self, openidclient): - im_auth = {"token": ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGF" - "jMDUwMTcwNjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwI" - "joxNDY1NDcxMzU0LCJpYXQiOjE0NjU0Njc3NTUsImp0aSI6IjA3YjlkYmE4LTc3NWMtNGI5OS1iN2QzLTk4Njg" - "5ODM1N2FiYSJ9.DwpZizVaYtvIj7fagQqDFpDh96szFupf6BNMIVLcopqQtZ9dBvwN9lgZ_w7Htvb3r-erho_hc" - "me5mqDMVbSKwsA2GiHfiXSnh9jmNNVaVjcvSPNVGF8jkKNxeSSgoT3wED8xt4oU4s5MYiR075-RAkt6AcWqVbXU" - "z5BzxBvANko")} + im_auth = {"token": (self.gen_token())} user_info = json.loads(read_file_as_string('../files/iam_user_info.json')) @@ -685,6 +705,7 @@ def test_check_oidc_valid_token(self, openidclient): openidclient.get_user_info_request.return_value = True, user_info Config.OIDC_ISSUERS = ["https://iam-test.indigo-datacloud.eu/"] + Config.OIDC_AUDIENCE = None IM.check_oidc_token(im_auth) From 06b230e15784d717fea81e3e86cb2f8c20464db2 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 3 Mar 2017 13:22:12 +0100 Subject: [PATCH 469/509] Solve py3 issues --- IM/openid/OpenIDClient.py | 4 ++-- IM/tosca/Tosca.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/IM/openid/OpenIDClient.py b/IM/openid/OpenIDClient.py index 6b91ed509..064743da0 100644 --- a/IM/openid/OpenIDClient.py +++ b/IM/openid/OpenIDClient.py @@ -39,7 +39,7 @@ def get_user_info_request(token): if resp.status_code != 200: return False, "Code: %d. Message: %s." % (resp.status_code, resp.text) return True, json.loads(resp.text) - except Exception, ex: + except Exception as ex: return False, str(ex) @staticmethod @@ -55,7 +55,7 @@ def get_token_introspection(token, client_id, client_secret): if resp.status_code != 200: return False, "Code: %d. Message: %s." % (resp.status_code, resp.text) return True, json.loads(resp.text) - except Exception, ex: + except Exception as ex: return False, str(ex) @staticmethod diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 8913332f6..ed1610c77 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -2,7 +2,6 @@ import logging import yaml import copy -import tempfile import urllib from IM.uriparse import uriparse @@ -447,7 +446,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): script_content = response.read() if response.code != 200: raise Exception("") - except Exception, ex: + except Exception as ex: raise Exception("Error downloading the implementation script '%s': %s" % ( interface.implementation, str(ex))) else: @@ -464,7 +463,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): script_content = response.read() if response.code != 200: raise Exception("") - except Exception, ex: + except Exception as ex: raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s' " "or in the artifacts remote url '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH, @@ -1321,7 +1320,7 @@ def _merge_recipes(yaml1, yaml2): yamlo1o = yaml.load(yaml1)[0] if not isinstance(yamlo1o, dict): yamlo1o = {} - except Exception, ex: + except Exception as ex: raise Exception("Error parsing YAML: " + yaml1 + "\n. Error: %s" % str(ex)) yamlo2s = {} @@ -1329,7 +1328,7 @@ def _merge_recipes(yaml1, yaml2): yamlo2s = yaml.load(yaml2) if not isinstance(yamlo2s, list) or any([not isinstance(d, dict) for d in yamlo2s]): yamlo2s = {} - except Exception, ex: + except Exception as ex: raise Exception("Error parsing YAML: " + yaml2 + "\n. Error: %s" % str(ex)) if not yamlo2s and not yamlo1o: From 61d074ebcf1502d68856e4c02ce0848d4f508882 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 08:25:54 +0100 Subject: [PATCH 470/509] Solve Py3 issues --- IM/InfrastructureManager.py | 4 ++-- IM/openid/OpenIDClient.py | 4 ++-- IM/tosca/Tosca.py | 8 ++++---- IM/tts/tts.py | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 5e0a78dbc..feab451b9 100644 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1234,7 +1234,7 @@ def check_oidc_token(im_auth): try: # decode the token to get the info decoded_token = JWT().get_info(token) - except Exception, ex: + except Exception as ex: InfrastructureManager.logger.exception("Error trying decode OIDC auth token: %s" % str(ex)) raise Exception("Error trying to decode OIDC auth token: %s" % str(ex)) @@ -1273,7 +1273,7 @@ def check_oidc_token(im_auth): # convert to username to use it in the rest of the IM im_auth['username'] = str(userinfo.get("preferred_username")) im_auth['password'] = str(decoded_token['iss']) + str(userinfo.get("sub")) - except Exception, ex: + except Exception as ex: InfrastructureManager.logger.exception("Error trying to validate OIDC auth token: %s" % str(ex)) raise Exception("Error trying to validate OIDC auth token: %s" % str(ex)) diff --git a/IM/openid/OpenIDClient.py b/IM/openid/OpenIDClient.py index 6b91ed509..064743da0 100644 --- a/IM/openid/OpenIDClient.py +++ b/IM/openid/OpenIDClient.py @@ -39,7 +39,7 @@ def get_user_info_request(token): if resp.status_code != 200: return False, "Code: %d. Message: %s." % (resp.status_code, resp.text) return True, json.loads(resp.text) - except Exception, ex: + except Exception as ex: return False, str(ex) @staticmethod @@ -55,7 +55,7 @@ def get_token_introspection(token, client_id, client_secret): if resp.status_code != 200: return False, "Code: %d. Message: %s." % (resp.status_code, resp.text) return True, json.loads(resp.text) - except Exception, ex: + except Exception as ex: return False, str(ex) @staticmethod diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 8913332f6..19e229c58 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -447,7 +447,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): script_content = response.read() if response.code != 200: raise Exception("") - except Exception, ex: + except Exception as ex: raise Exception("Error downloading the implementation script '%s': %s" % ( interface.implementation, str(ex))) else: @@ -464,7 +464,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): script_content = response.read() if response.code != 200: raise Exception("") - except Exception, ex: + except Exception as ex: raise Exception("Implementation file: '%s' is not located in the artifacts folder '%s' " "or in the artifacts remote url '%s'." % (interface.implementation, Tosca.ARTIFACTS_PATH, @@ -1321,7 +1321,7 @@ def _merge_recipes(yaml1, yaml2): yamlo1o = yaml.load(yaml1)[0] if not isinstance(yamlo1o, dict): yamlo1o = {} - except Exception, ex: + except Exception as ex: raise Exception("Error parsing YAML: " + yaml1 + "\n. Error: %s" % str(ex)) yamlo2s = {} @@ -1329,7 +1329,7 @@ def _merge_recipes(yaml1, yaml2): yamlo2s = yaml.load(yaml2) if not isinstance(yamlo2s, list) or any([not isinstance(d, dict) for d in yamlo2s]): yamlo2s = {} - except Exception, ex: + except Exception as ex: raise Exception("Error parsing YAML: " + yaml2 + "\n. Error: %s" % str(ex)) if not yamlo2s and not yamlo1o: diff --git a/IM/tts/tts.py b/IM/tts/tts.py index 22637901d..a50199e49 100644 --- a/IM/tts/tts.py +++ b/IM/tts/tts.py @@ -62,7 +62,7 @@ def request_credential(self, sid): try: headers = {'Authorization': 'Bearer %s' % self.token, 'Content-Type': 'application/json'} success, res = self._perform_post(url, headers, body) - except Exception, ex: + except Exception as ex: success = False res = str(ex) if success: @@ -77,7 +77,7 @@ def list_providers(self): url = "/api/v2/oidcp" try: success, output = self._perform_get(url) - except Exception, ex: + except Exception as ex: success = False output = str(ex) if not success: @@ -93,7 +93,7 @@ def list_endservices(self, provider): try: headers = {'Authorization': 'Bearer %s' % self.token} success, output = self._perform_get(url, headers) - except Exception, ex: + except Exception as ex: success = False output = str(ex) if not success: From fc2175b8e02ceb5bdefe99210cba85d9891ce91e Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 08:30:44 +0100 Subject: [PATCH 471/509] Solve Py3 issues --- IM/openid/OpenIDClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/openid/OpenIDClient.py b/IM/openid/OpenIDClient.py index 064743da0..6e6d9d96e 100644 --- a/IM/openid/OpenIDClient.py +++ b/IM/openid/OpenIDClient.py @@ -19,7 +19,7 @@ import requests import json import time -from JWT import JWT +from .JWT import JWT class OpenIDClient(object): From 265da70f9e2eba66107242d9b349b85a095c8422 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 09:15:42 +0100 Subject: [PATCH 472/509] Solve Py3 issues --- IM/connectors/OpenNebula.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/connectors/OpenNebula.py b/IM/connectors/OpenNebula.py index 7ac7258b2..2e6900ec1 100644 --- a/IM/connectors/OpenNebula.py +++ b/IM/connectors/OpenNebula.py @@ -208,13 +208,13 @@ def getSessionID(self, auth_data, hash_password=None): auth = auths[0] if 'username' in auth and 'password' in auth: - passwd = auth['password'].encode('utf-8') + passwd = auth['password'] if hash_password is None: one_ver = self.getONEVersion(auth_data) if one_ver == "2.0.0" or one_ver == "3.0.0": hash_password = True if hash_password: - passwd = hashlib.sha1(passwd.strip()).hexdigest() + passwd = hashlib.sha1(passwd.strip().encode('utf-8')).hexdigest() return auth['username'] + ":" + passwd elif 'token' in auth: username, passwd = ONETTSClient.get_auth_from_tts(ConfigOpenNebula.TTS_URL, From a57b3e98c6436eed499f26f527618d0583d27aae Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 11:14:48 +0100 Subject: [PATCH 473/509] Fix py3 issues --- IM/openid/JWT.py | 4 ++-- IM/tosca/Tosca.py | 41 +++++++++++++++++++++++--------------- test/files/tosca_long.yml | 4 ++-- test/unit/test_im_logic.py | 2 +- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/IM/openid/JWT.py b/IM/openid/JWT.py index 01e5cb430..6ee26c3c7 100644 --- a/IM/openid/JWT.py +++ b/IM/openid/JWT.py @@ -68,6 +68,6 @@ def get_info(token): :param token: The JWT token """ - part = tuple(str(token).split(b".")) + part = tuple(token.encode("utf-8").split(b".")) part = [JWT.b64d(p) for p in part] - return json.loads(part[1]) + return json.loads(part[1].decode("utf-8")) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 19e229c58..8f45349f2 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -3,7 +3,16 @@ import yaml import copy import tempfile -import urllib +try: + import urllib.request as urllib +except: + import urllib +try: + unicode("hola") +except NameError: + unicode = str + + from IM.uriparse import uriparse from toscaparser.tosca_template import ToscaTemplate @@ -50,7 +59,7 @@ def to_radl(self, inf_info=None): relationships = [] for node in self.tosca.nodetemplates: # Store relationships to check later - for relationship, trgt in node.relationships.iteritems(): + for relationship, trgt in node.relationships.items(): src = node relationships.append((src, trgt, relationship)) @@ -407,7 +416,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): # Get the inputs env = {} if interface.inputs: - for param_name, param_value in interface.inputs.iteritems(): + for param_name, param_value in interface.inputs.items(): val = None if self._is_artifact(param_value): @@ -472,7 +481,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if script_path.endswith(".yaml") or script_path.endswith(".yml"): if env: - for var_name, var_value in env.iteritems(): + for var_name, var_value in env.items(): if isinstance(var_value, str) and not var_value.startswith("|"): var_value = '"%s"' % var_value else: @@ -494,7 +503,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): os.path.basename(script_path) + "\n" if env: recipe += " environment:\n" - for var_name, var_value in env.iteritems(): + for var_name, var_value in env.items(): recipe += " %s: %s\n" % (var_name, var_value) recipe_list.append(recipe) @@ -713,7 +722,7 @@ def _get_attribute_result(self, func, node, inf_info): else: # As default assume that there will be only one VM per group vm = vm_list[node.name][0] - if len(vm_list[node.name]) < index: + if index is not None and len(vm_list[node.name]) < index: index = len(vm_list[node.name]) - 1 if attribute_name == "tosca_id": @@ -876,7 +885,7 @@ def _find_host_compute(self, node, nodetemplates): return node if node.requirements: - for r, n in node.relationships.iteritems(): + for r, n in node.relationships.items(): if Tosca._is_derived_from(r, r.HOSTEDON) or Tosca._is_derived_from(r, r.BINDSTO): root_type = Tosca._get_root_parent_type(n).type if root_type == "tosca.nodes.Compute": @@ -921,11 +930,11 @@ def _node_fulfill_filter(self, node, node_filter): for cap_type in ['os', 'host']: if cap_type in elem: for p in elem.get(cap_type).get('properties'): - p_name = p.keys()[0] - p_value = p.values()[0] + p_name = list(p.keys())[0] + p_value = list(p.values())[0] if isinstance(p_value, dict): - filter_props[p_name] = ( - p_value.keys()[0], p_value.values()[0]) + filter_props[p_name] = (list(p_value.keys())[0], + list(p_value.values())[0]) else: filter_props[p_name] = ("equal", p_value) @@ -938,7 +947,7 @@ def _node_fulfill_filter(self, node, node_filter): } # Compare the properties - for name, value in filter_props.iteritems(): + for name, value in filter_props.items(): operator, filter_value = value if name in ['disk_size', 'mem_size']: filter_value, _ = Tosca._get_size_and_unit(filter_value) @@ -1003,7 +1012,7 @@ def _get_dependency_level(node): """ if node.requirements: maxl = 0 - for r, n in node.relationships.iteritems(): + for r, n in node.relationships.items(): if Tosca._is_derived_from(r, r.HOSTEDON): level = Tosca._get_dependency_level(n) else: @@ -1221,7 +1230,7 @@ def _get_attached_disks(self, node, nodetemplates): disks = [] count = 1 - for rel, trgt in node.relationships.iteritems(): + for rel, trgt in node.relationships.items(): src = node rel_tpl = Tosca._get_relationship_template(rel, src, trgt) # TODO: ver root_type @@ -1295,7 +1304,7 @@ def _get_interfaces(node): while True: if node_type.interfaces and 'Standard' in node_type.interfaces: - for name, elems in node_type.interfaces['Standard'].iteritems(): + for name, elems in node_type.interfaces['Standard'].items(): if name in ['create', 'configure', 'start', 'stop', 'delete']: if name not in interfaces: interfaces[name] = InterfacesDef( @@ -1385,7 +1394,7 @@ def _merge_yaml(yaml1, yaml2): if yaml2 is None: return yaml1 elif isinstance(yaml1, dict) and isinstance(yaml2, dict): - for k, v in yaml2.iteritems(): + for k, v in yaml2.items(): if k not in yaml1: yaml1[k] = v else: diff --git a/test/files/tosca_long.yml b/test/files/tosca_long.yml index cfa0bd709..a02977eed 100644 --- a/test/files/tosca_long.yml +++ b/test/files/tosca_long.yml @@ -126,12 +126,12 @@ topology_template: - host: node_filter: capabilities: - # Constraints for selecting “host” (Container Capability) + # Constraints for selecting "host" (Container Capability) - host: properties: - num_cpus: { in_range: [ 1, 4 ] } - mem_size: { greater_or_equal: 2 GB } - # Constraints for selecting “os” (OperatingSystem Capability) + # Constraints for selecting "os" (OperatingSystem Capability) - os: properties: - type: linux diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 56caf1941..2313bcb91 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -139,7 +139,7 @@ def gen_token(self, aud=None, exp=None): data["aud"] = aud if exp: data["exp"] = int(time.time()) + exp - return "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.%s.ignored" % base64.urlsafe_b64encode(json.dumps(data)) + return "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.%s.ignored" % base64.urlsafe_b64encode(json.dumps(data).encode("utf-8")).decode("utf-8") def test_inf_creation0(self): """Create infrastructure with empty RADL.""" From fc5f2cead3e46d5ea5badb6f61be52ef505ef5c0 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 11:18:51 +0100 Subject: [PATCH 474/509] Style changes --- test/unit/test_im_logic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 2313bcb91..7d9ad35e2 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -139,7 +139,8 @@ def gen_token(self, aud=None, exp=None): data["aud"] = aud if exp: data["exp"] = int(time.time()) + exp - return "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.%s.ignored" % base64.urlsafe_b64encode(json.dumps(data).encode("utf-8")).decode("utf-8") + return ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.%s.ignored" % + base64.urlsafe_b64encode(json.dumps(data).encode("utf-8")).decode("utf-8")) def test_inf_creation0(self): """Create infrastructure with empty RADL.""" From 12132dfdd7e6673cfe1283d123cd1ae4e88aa280 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 11:20:17 +0100 Subject: [PATCH 475/509] Style changes --- IM/tosca/Tosca.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 8f45349f2..2fe92b79d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -12,8 +12,6 @@ except NameError: unicode = str - - from IM.uriparse import uriparse from toscaparser.tosca_template import ToscaTemplate from toscaparser.elements.interfaces import InterfacesDef From 57e23ea4fd8fd300116d86a5b2e8f0a70a2e69fa Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 11:42:36 +0100 Subject: [PATCH 476/509] Fix tosca test --- test/unit/Tosca.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index 52963e853..df1ee68ef 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -61,12 +61,14 @@ def test_tosca_get_outputs(self): tosca_data = read_file_as_string('../files/tosca_create.yml') tosca = Tosca(tosca_data) _, radl = tosca.to_radl() - radl.systems[0].setValue("net_interface.1.ip", "158.42.1.1") - radl.systems[0].setValue("disk.0.os.credentials.username", "ubuntu") - radl.systems[0].setValue("disk.0.os.credentials.password", "pass") + radl1 = radl.clone() + radl1.systems = [radl.get_system_by_name('web_server')] + radl1.systems[0].setValue("net_interface.1.ip", "158.42.1.1") + radl1.systems[0].setValue("disk.0.os.credentials.username", "ubuntu") + radl1.systems[0].setValue("disk.0.os.credentials.password", "pass") inf = InfrastructureInfo() - vm = VirtualMachine(inf, "1", None, radl, radl, None) - vm.requested_radl = radl + vm = VirtualMachine(inf, "1", None, radl1, radl1, None) + vm.requested_radl = radl1 inf.vm_list = [vm] outputs = tosca.get_outputs(inf) self.assertEqual(outputs, {'server_url': ['158.42.1.1'], From 8ae4d8bfea311ca8eb45da0a7b78ed03a334f3e1 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 6 Mar 2017 12:10:28 +0100 Subject: [PATCH 477/509] Minor changes --- IM/tosca/Tosca.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 2fe92b79d..18509066d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -2,11 +2,10 @@ import logging import yaml import copy -import tempfile try: - import urllib.request as urllib + from urllib.request import urlopen except: - import urllib + from urllib import urlopen try: unicode("hola") except NameError: @@ -450,7 +449,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): if implementation_url[0] in ['http', 'https', 'ftp']: script_path = implementation_url[2] try: - response = urllib.urlopen(interface.implementation) + response = urlopen(interface.implementation) script_content = response.read() if response.code != 200: raise Exception("") @@ -466,8 +465,7 @@ def _gen_configure_from_interfaces(self, radl, node, interfaces, compute): f.close() else: try: - response = urllib.urlopen( - Tosca.ARTIFACTS_REMOTE_REPO + interface.implementation) + response = urlopen(Tosca.ARTIFACTS_REMOTE_REPO + interface.implementation) script_content = response.read() if response.code != 200: raise Exception("") From 15737ea9e4a5b26ee743659213847374e20206ca Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 14 Mar 2017 11:38:59 +0100 Subject: [PATCH 478/509] Update docs --- doc/gitbook/installation.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/doc/gitbook/installation.md b/doc/gitbook/installation.md index d79cccc28..1e7603763 100644 --- a/doc/gitbook/installation.md +++ b/doc/gitbook/installation.md @@ -207,3 +207,39 @@ or REST_SSL = True And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates paths. + +### 1.4.2 SINGLE SITE + +To configure the IM as the orchestrator of a single site you can use the SINGLE_SITE* +configuration variables. This is an example for an OpenNebula based site assuming that +the hostname of the OpenNebula server is 'server.com': + +SINGLE_SITE = True +SINGLE_SITE_TYPE = OpenNebula +SINGLE_SITE_AUTH_HOST = http://server.com:2633 +SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com/ + +And this second example shows how to configure for an OpenStack based site assuming that +the hostname of the OpenStack keystone server is 'server.com': + +SINGLE_SITE = True +SINGLE_SITE_TYPE = OpenStack +SINGLE_SITE_AUTH_HOST = https://server.com:5000 +SINGLE_SITE_IMAGE_URL_PREFIX = ost://server.com/ + +Using this kind of configuration combined with OIDC tokens the IM authentication is +simplified and an standard 'Bearer' authorization header can be using to interact with the IM service. + +### 1.4.3 OPENNEBULA TTS INTEGRATION + +The IM service enables to configure a WaTTS - the INDIGO Token Translation Service (https://github.com/indigo-dc/tts) +to access OpenNebula sites. IM uses version 2 of the WaTTS API (https://indigo-dc.gitbooks.io/token-translation-service/content/api.html) + +To configure it you must set the value of the TTS_URL in the OpenNebula section: +TTS_URL = https://localhost:8443 + +In particular the WaTTS instance must be configured to include the hostname of the OpenNebula server +in the plugin configuration of the WaTTS service, for example like this: + +service.onesite.description = server.com + From eecab62d5d0c78d0a369a4764a99e4135fa2844d Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 14 Mar 2017 12:02:19 +0100 Subject: [PATCH 479/509] Fix problem with end / in the SINGLE_SITE_IMAGE_URL_PREFIX --- IM/InfrastructureManager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 3a7efddc1..d91da3fa7 100644 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -436,7 +436,10 @@ def AddResource(inf_id, radl_data, auth, context=True, failed_clouds=[]): if Config.SINGLE_SITE: image_id = os.path.basename(s.getValue("disk.0.image.url")) - s.setValue("disk.0.image.url", Config.SINGLE_SITE_IMAGE_URL_PREFIX + '/' + image_id) + url_prefix = Config.SINGLE_SITE_IMAGE_URL_PREFIX + if not url_prefix.endswith("/"): + url_prefix = url_prefix + "/" + s.setValue("disk.0.image.url", url_prefix + image_id) # Remove the requested apps from the system s_without_apps = radl.get_system_by_name(system_id).clone() From ec3c4308c7be47ca29e5f59456e669640840460f Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 14 Mar 2017 12:06:15 +0100 Subject: [PATCH 480/509] Update docs --- doc/gitbook/installation.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/gitbook/installation.md b/doc/gitbook/installation.md index 1e7603763..cac9d9668 100644 --- a/doc/gitbook/installation.md +++ b/doc/gitbook/installation.md @@ -215,16 +215,22 @@ configuration variables. This is an example for an OpenNebula based site assumin the hostname of the OpenNebula server is 'server.com': SINGLE_SITE = True + SINGLE_SITE_TYPE = OpenNebula + SINGLE_SITE_AUTH_HOST = http://server.com:2633 + SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com/ And this second example shows how to configure for an OpenStack based site assuming that the hostname of the OpenStack keystone server is 'server.com': SINGLE_SITE = True + SINGLE_SITE_TYPE = OpenStack + SINGLE_SITE_AUTH_HOST = https://server.com:5000 + SINGLE_SITE_IMAGE_URL_PREFIX = ost://server.com/ Using this kind of configuration combined with OIDC tokens the IM authentication is @@ -236,6 +242,7 @@ The IM service enables to configure a WaTTS - the INDIGO Token Translation Servi to access OpenNebula sites. IM uses version 2 of the WaTTS API (https://indigo-dc.gitbooks.io/token-translation-service/content/api.html) To configure it you must set the value of the TTS_URL in the OpenNebula section: + TTS_URL = https://localhost:8443 In particular the WaTTS instance must be configured to include the hostname of the OpenNebula server From 496546c373c39c1e1add2f82d5f7bed4ca71d9a2 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 14 Mar 2017 15:45:38 +0100 Subject: [PATCH 481/509] Update docs --- doc/gitbook/installation.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/doc/gitbook/installation.md b/doc/gitbook/installation.md index cac9d9668..a83d85c33 100644 --- a/doc/gitbook/installation.md +++ b/doc/gitbook/installation.md @@ -183,6 +183,7 @@ or set the name of the script file (im_service.py) if the file is in the PATH Check the parameters in $IM_PATH/etc/im.cfg or /etc/im/im.cfg. Please pay attention to the next configuration variables, as they are the most important +``` DATA_DB - must be set to the URL to access the database to store the IM data. Be careful if you have two different instances of the IM service running in the same machine!!. It can be a MySQL DB: 'mysql://username:password@server/db_name' or @@ -192,6 +193,7 @@ CONTEXTUALIZATION_DIR - must be set to the full path where the IM contextualizat are located. In case of using pip installation the default value is correct (/usr/share/im/contextualization) in case of installing from sources set to $IM_PATH/contextualization (e.g. /usr/local/im/contextualization) +``` ### 1.4.1 SECURITY @@ -200,11 +202,15 @@ get the messages with the IM with the authorisation data with the cloud provider Security can be activated both in the XMLRPC and REST APIs. Setting this variables: +``` XMLRCP_SSL = True +``` or +``` REST_SSL = True +``` And then set the variables: XMLRCP_SSL_* or REST_SSL_* to your certificates paths. @@ -214,24 +220,22 @@ To configure the IM as the orchestrator of a single site you can use the SINGLE_ configuration variables. This is an example for an OpenNebula based site assuming that the hostname of the OpenNebula server is 'server.com': +``` SINGLE_SITE = True - SINGLE_SITE_TYPE = OpenNebula - SINGLE_SITE_AUTH_HOST = http://server.com:2633 - -SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com/ +SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com +``` And this second example shows how to configure for an OpenStack based site assuming that the hostname of the OpenStack keystone server is 'server.com': +``` SINGLE_SITE = True - SINGLE_SITE_TYPE = OpenStack - SINGLE_SITE_AUTH_HOST = https://server.com:5000 - -SINGLE_SITE_IMAGE_URL_PREFIX = ost://server.com/ +SINGLE_SITE_IMAGE_URL_PREFIX = ost://server.com +``` Using this kind of configuration combined with OIDC tokens the IM authentication is simplified and an standard 'Bearer' authorization header can be using to interact with the IM service. @@ -243,10 +247,14 @@ to access OpenNebula sites. IM uses version 2 of the WaTTS API (https://indigo-d To configure it you must set the value of the TTS_URL in the OpenNebula section: +``` TTS_URL = https://localhost:8443 +``` In particular the WaTTS instance must be configured to include the hostname of the OpenNebula server in the plugin configuration of the WaTTS service, for example like this: +``` service.onesite.description = server.com +``` From 3e62086c35f5dc17d3a523d20011621b1f52bf0e Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 14 Mar 2017 16:46:40 +0100 Subject: [PATCH 482/509] Add scope validation. Issue #148 --- IM/InfrastructureManager.py | 18 ++++++++++++++++++ IM/config.py | 3 +++ etc/im.cfg | 8 +++++++- test/unit/test_im_logic.py | 29 ++++++++++++++++++++++++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 418dabb0d..f23330c78 100644 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1225,6 +1225,24 @@ def check_oidc_token(im_auth): InfrastructureManager.logger.error("Audience %s not found in access token." % Config.OIDC_AUDIENCE) raise InvaliddUserException("Invalid InfrastructureManager credentials. Audience not accepted.") + if Config.OIDC_SCOPES and Config.OIDC_CLIENT_ID and Config.OIDC_CLIENT_SECRET: + success, res = OpenIDClient.get_token_introspection(token, + Config.OIDC_CLIENT_ID, + Config.OIDC_CLIENT_SECRET) + if not success: + raise InvaliddUserException("Invalid InfrastructureManager credentials. " + "Invalid token or Client credentials.") + else: + if not res["scope"]: + raise InvaliddUserException("Invalid InfrastructureManager credentials. " + "No scope obtained from introspection.") + else: + scopes = res["scope"].split(" ") + if not all([elem in scopes for elem in Config.OIDC_SCOPES]): + raise InvaliddUserException("Invalid InfrastructureManager credentials. Scopes %s " + "not in introspection scopes: %s" % (" ".join(Config.OIDC_SCOPES), + res["scope"])) + # Now check if the token is not expired expired, msg = OpenIDClient.is_access_token_expired(token) if expired: diff --git a/IM/config.py b/IM/config.py index c156c9f52..42ed5361c 100644 --- a/IM/config.py +++ b/IM/config.py @@ -99,6 +99,9 @@ class Config: OIDC_AUDIENCE = None INF_CACHE_TIME = None VMINFO_JSON = False + OIDC_CLIENT_ID = None + OIDC_CLIENT_SECRET = None + OIDC_SCOPES = [] config = ConfigParser() config.read([Config.IM_PATH + '/../im.cfg', Config.IM_PATH + diff --git a/etc/im.cfg b/etc/im.cfg index f27afa963..4b7308aea 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -121,7 +121,13 @@ SINGLE_SITE_IMAGE_URL_PREFIX = one://server.com/ # List of OIDC issuers supported OIDC_ISSUERS = https://iam-test.indigo-datacloud.eu/ # If set the IM will check that the string defined here appear in the "aud" claim of the OpenID access token -#OIDC_AUDIENCE = +#OIDC_AUDIENCE = +# OIDC client ID and secret of the IM service +#OIDC_CLIENT_ID = +#OIDC_CLIENT_SECRET = +# List of scopes that must appear in the token request to access the IM service +# Client ID and Secret must be provided to make it work +#OIDC_SCOPES = # Time (in seconds) the IM service will maintain the information of an infrastructure # in memory. Only used in case of IM in HA mode. diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index aa97911a9..c25d821e1 100755 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -965,7 +965,8 @@ def test_contextualize(self): IM.DestroyInfrastructure(infId, auth0) - def test_check_oidc_invalid_token(self): + @patch('requests.request') + def test_check_oidc_invalid_token(self, request): im_auth = {"token": self.gen_token()} with self.assertRaises(Exception) as ex: @@ -988,6 +989,32 @@ def test_check_oidc_invalid_token(self): 'Invalid InfrastructureManager credentials. OIDC auth Token expired.') Config.OIDC_AUDIENCE = None + Config.OIDC_SCOPES = ["scope1", "scope2"] + Config.OIDC_CLIENT_ID = "client" + Config.OIDC_CLIENT_SECRET = "secret" + response = MagicMock() + response.status_code = 200 + response.text = '{ "scope": "profile scope1" }' + request.return_value = response + with self.assertRaises(Exception) as ex: + IM.check_oidc_token(im_auth_aud) + self.assertEqual(str(ex.exception), + 'Invalid InfrastructureManager credentials. ' + 'Scopes scope1 scope2 not in introspection scopes: profile scope1') + + response.status_code = 200 + response.text = '{ "scope": "address profile scope1 scope2" }' + request.return_value = response + with self.assertRaises(Exception) as ex: + IM.check_oidc_token(im_auth_aud) + self.assertEqual(str(ex.exception), + 'Invalid InfrastructureManager credentials. ' + 'OIDC auth Token expired.') + + Config.OIDC_SCOPES = [] + Config.OIDC_CLIENT_ID = None + Config.OIDC_CLIENT_SECRET = None + Config.OIDC_ISSUERS = ["https://other_issuer"] with self.assertRaises(Exception) as ex: From 899a64425e067e8a26b610fc33f6bb2e151d1d6a Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 24 Mar 2017 09:31:20 +0100 Subject: [PATCH 483/509] Fix #170 --- IM/tosca/Tosca.py | 9 ++++++--- test/files/tosca_long.yml | 15 +++++++++++++++ test/unit/Tosca.py | 2 ++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 18509066d..b388bb7a4 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -190,10 +190,10 @@ def _format_outports(ports_dict): # In case of source_range do not use port mapping only direct ports if source_range: - for port_in_range in range(source_range[0], source_range[1]): + for port_in_range in range(source_range[0], source_range[1] + 1): if res: res += "," - res += "%s" % (port_in_range) + res += "%s/%s" % (port_in_range, protocol) else: if res: res += "," @@ -302,7 +302,10 @@ def _add_node_nets(self, node, radl, system, nodetemplates): num_net = system.getNumNetworkIfaces() if ports: - public_net.setValue("outports", Tosca._format_outports(ports)) + outports = Tosca._format_outports(ports) + if public_net.getValue("outports"): + outports = "%s,%s" % (public_net.getValue("outports"), outports) + public_net.setValue("outports", outports) if net_provider_id: public_net.setValue("provider_id", net_provider_id) diff --git a/test/files/tosca_long.yml b/test/files/tosca_long.yml index a02977eed..5a4de041e 100644 --- a/test/files/tosca_long.yml +++ b/test/files/tosca_long.yml @@ -45,6 +45,21 @@ topology_template: requirements: - host: lrms_server + other_server: + type: tosca.nodes.indigo.Compute + capabilities: + endpoint: + properties: + network_name: PUBLIC + ports: + port_range: + protocol: tcp + source_range: [ 1, 4 ] + host: + properties: + num_cpus: 1 + mem_size: 1 GB + lrms_server: type: tosca.nodes.indigo.Compute capabilities: diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index df1ee68ef..f4cbc25a8 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -47,9 +47,11 @@ def test_tosca_to_radl(self): tosca_data = read_file_as_string('../files/tosca_long.yml') tosca = Tosca(tosca_data) _, radl = tosca.to_radl() + print radl radl = parse_radl(str(radl)) net = radl.get_network_by_id('public_net') self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') + self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp,1/tcp,2/tcp,3/tcp,4/tcp1') lrms_wn = radl.get_system_by_name('lrms_wn') self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) lrms_server = radl.get_system_by_name('lrms_server') From af7d3f22b6579954460fbc52f41d4fb70adf5923 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 24 Mar 2017 09:41:19 +0100 Subject: [PATCH 484/509] Remove test lines. Issue #170 --- test/unit/Tosca.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index f4cbc25a8..08fc13ab7 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -47,11 +47,10 @@ def test_tosca_to_radl(self): tosca_data = read_file_as_string('../files/tosca_long.yml') tosca = Tosca(tosca_data) _, radl = tosca.to_radl() - print radl radl = parse_radl(str(radl)) net = radl.get_network_by_id('public_net') self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') - self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp,1/tcp,2/tcp,3/tcp,4/tcp1') + self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp,1/tcp,2/tcp,3/tcp,4/tcp') lrms_wn = radl.get_system_by_name('lrms_wn') self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) lrms_server = radl.get_system_by_name('lrms_server') From 5391372ed6df56f2d06841081e760973fede74a6 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Mar 2017 08:18:28 +0200 Subject: [PATCH 485/509] Enable to specify a port range in endpoint ports #172 --- IM/tosca/Tosca.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index b388bb7a4..a87a2c0d9 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -190,10 +190,9 @@ def _format_outports(ports_dict): # In case of source_range do not use port mapping only direct ports if source_range: - for port_in_range in range(source_range[0], source_range[1] + 1): - if res: - res += "," - res += "%s/%s" % (port_in_range, protocol) + if res: + res += "," + res += "%s:%s/%s" % (source_range[0], source_range[1], protocol) else: if res: res += "," @@ -798,7 +797,6 @@ def _get_attribute_result(self, func, node, inf_info): return node.name elif attribute_name == "private_address": if node.type == "tosca.nodes.indigo.Compute": - # This only works with Ansible 2.1, wait for it to be released if index is not None: return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PRIVATE_IP'] }}" % (node.name, index) else: @@ -811,7 +809,6 @@ def _get_attribute_result(self, func, node, inf_info): return "{{ hostvars[groups['%s'][0]]['IM_NODE_PRIVATE_IP'] }}" % node.name elif attribute_name == "public_address": if node.type == "tosca.nodes.indigo.Compute": - # This only works with Ansible 2.1, wait for it to be released if index is not None: return "{{ hostvars[groups['%s'][%d]]['IM_NODE_PUBLIC_IP'] }}" % (node.name, index) else: From 54d5067c62156aa9e42635831dcc46225af5452b Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Mar 2017 08:20:04 +0200 Subject: [PATCH 486/509] Fix test #172 --- test/unit/Tosca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index 08fc13ab7..5da597bec 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -50,7 +50,7 @@ def test_tosca_to_radl(self): radl = parse_radl(str(radl)) net = radl.get_network_by_id('public_net') self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') - self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp,1/tcp,2/tcp,3/tcp,4/tcp') + self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp,1:4/tcp') lrms_wn = radl.get_system_by_name('lrms_wn') self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) lrms_server = radl.get_system_by_name('lrms_server') From e57b33a15496c2f3862c0db32ec8d72c5647cc78 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Mar 2017 08:38:41 +0200 Subject: [PATCH 487/509] Fix test #172 --- test/unit/Tosca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index 5da597bec..b053155c4 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -50,7 +50,7 @@ def test_tosca_to_radl(self): radl = parse_radl(str(radl)) net = radl.get_network_by_id('public_net') self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') - self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp,1:4/tcp') + self.assertIn(net.getValue("outports"), ['8080/tcp-8080/tcp,1:4/tcp', '1:4/tcp,8080/tcp-8080/tcp']) lrms_wn = radl.get_system_by_name('lrms_wn') self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) lrms_server = radl.get_system_by_name('lrms_server') From 02eb8beea94f7a15059977f094540900dc21928f Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 29 Mar 2017 08:42:54 +0200 Subject: [PATCH 488/509] Fix test #172 --- test/unit/Tosca.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index b053155c4..afb3e24c0 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -50,7 +50,8 @@ def test_tosca_to_radl(self): radl = parse_radl(str(radl)) net = radl.get_network_by_id('public_net') self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') - self.assertIn(net.getValue("outports"), ['8080/tcp-8080/tcp,1:4/tcp', '1:4/tcp,8080/tcp-8080/tcp']) + self.assertIn(net.getValue("outports"), ['1:4/tcp,8080/tcp-8080/tcp', + '8080/tcp-8080/tcp,1:4/tcp']) lrms_wn = radl.get_system_by_name('lrms_wn') self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) lrms_server = radl.get_system_by_name('lrms_server') From 557365be3b324a9f2d09cadfa1bbd7159d311aec Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 30 Mar 2017 12:01:01 +0200 Subject: [PATCH 489/509] Fix #174 --- IM/InfrastructureInfo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/IM/InfrastructureInfo.py b/IM/InfrastructureInfo.py index 181577ac5..b6101d4b0 100644 --- a/IM/InfrastructureInfo.py +++ b/IM/InfrastructureInfo.py @@ -137,7 +137,11 @@ def deserialize(str_data): if dic['radl']: dic['radl'] = parse_radl(dic['radl']) if 'extra_info' in dic and dic['extra_info'] and "TOSCA" in dic['extra_info']: - dic['extra_info']['TOSCA'] = Tosca.deserialize(dic['extra_info']['TOSCA']) + try: + dic['extra_info']['TOSCA'] = Tosca.deserialize(dic['extra_info']['TOSCA']) + except: + del dic['extra_info']['TOSCA'] + InfrastructureInfo.logger.exception("Error deserializing TOSCA document") newinf.__dict__.update(dic) newinf.cloud_connector = None # Set the ConfManager object and the lock to the data loaded From 6834835bd3af996a2abdaf2abad8b46887e46b5a Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 4 Apr 2017 12:31:25 +0200 Subject: [PATCH 490/509] Implements #176 --- IM/tosca/Tosca.py | 33 ++++++++++----------------------- test/unit/Tosca.py | 5 +++-- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index a87a2c0d9..585afac9b 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -276,29 +276,16 @@ def _add_node_nets(self, node, radl, system, nodetemplates): # If the node needs a public IP if public_ip: - public_nets = [] - for net in radl.networks: - if net.isPublic(): - public_nets.append(net) - - if public_nets: - public_net = None - for net in public_nets: - num_net = system.getNumNetworkWithConnection(net.id) - if num_net is not None: - public_net = net - break - - if not public_net: - # There are a public net but it has not been used in - # this VM - public_net = public_nets[0] - num_net = system.getNumNetworkIfaces() - else: - # There no public net, create one - public_net = network.createNetwork("public_net", True) - radl.networks.append(public_net) - num_net = system.getNumNetworkIfaces() + # Always create a public IP per VM + # to enable to specify different outports + net_name = "public_net" + i = 1 + while radl.get_network_by_id(net_name) is not None: + net_name = "public_net_%d" % i + i += 1 + public_net = network.createNetwork(net_name, True) + radl.networks.append(public_net) + num_net = system.getNumNetworkIfaces() if ports: outports = Tosca._format_outports(ports) diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index afb3e24c0..b4884c35c 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -50,8 +50,9 @@ def test_tosca_to_radl(self): radl = parse_radl(str(radl)) net = radl.get_network_by_id('public_net') self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') - self.assertIn(net.getValue("outports"), ['1:4/tcp,8080/tcp-8080/tcp', - '8080/tcp-8080/tcp,1:4/tcp']) + self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp') + net1 = radl.get_network_by_id('public_net_1') + self.assertEqual(net1.getValue("outports"), '1:4/tcp') lrms_wn = radl.get_system_by_name('lrms_wn') self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) lrms_server = radl.get_system_by_name('lrms_server') From 1f5d95cca812e5e5076982003725e747e5942b9d Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 4 Apr 2017 16:21:45 +0200 Subject: [PATCH 491/509] Fix test --- test/unit/Tosca.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index b4884c35c..0dda407d4 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -49,10 +49,15 @@ def test_tosca_to_radl(self): _, radl = tosca.to_radl() radl = parse_radl(str(radl)) net = radl.get_network_by_id('public_net') - self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') - self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp') net1 = radl.get_network_by_id('public_net_1') - self.assertEqual(net1.getValue("outports"), '1:4/tcp') + self.assertIn(net.getValue('provider_id'), ['vpc-XX.subnet-XX', None]) + if net.getValue('provider_id') is None: + self.assertEqual(net.getValue("outports"), '1:4/tcp') + self.assertEqual(net1.getValue("outports"), '8080/tcp-8080/tcp') + else: + self.assertEqual(net.getValue('provider_id'), 'vpc-XX.subnet-XX') + self.assertEqual(net.getValue("outports"), '8080/tcp-8080/tcp') + self.assertEqual(net1.getValue("outports"), '1:4/tcp') lrms_wn = radl.get_system_by_name('lrms_wn') self.assertEqual(lrms_wn.getValue('memory.size'), 2000000000) lrms_server = radl.get_system_by_name('lrms_server') From dedbd001b20c7a8da19ce21e647085c0c43a2acd Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 11 May 2017 15:27:26 +0200 Subject: [PATCH 492/509] Implements #179 --- IM/tosca/Tosca.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 585afac9b..57e988c6d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -996,7 +996,7 @@ def _get_dependency_level(node): if node.requirements: maxl = 0 for r, n in node.relationships.items(): - if Tosca._is_derived_from(r, r.HOSTEDON): + if Tosca._is_derived_from(r, [r.HOSTEDON, r.DEPENDSON]): level = Tosca._get_dependency_level(n) else: level = 0 @@ -1249,8 +1249,12 @@ def _is_derived_from(rel, parent_type): """ Check if a node is a descendant from a specified parent type """ + if isinstance(parent_type, list): + parent_types = parent_type + else: + parent_types = [parent_type] while True: - if rel.type == parent_type: + if rel.type in parent_types: return True else: if rel.parent_type: From ea61f21bdf2d1ea859e075b663dca7faede08f06 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 25 May 2017 09:55:31 +0200 Subject: [PATCH 493/509] Convert versions to int --- ansible_install.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index e54980f10..3f5713503 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -52,19 +52,19 @@ - name: Set transport to ssh in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=transport value=ssh - when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version >= 10) + when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int >= 10) - name: Set transport to smart in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=defaults option=transport value=smart - when: (ansible_os_family == "RedHat" and ansible_distribution_major_version < 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version < 10) + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int < 10) - name: Change ssh_args to set ControlPersist to 15 min in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=ssh_args value="-o ControlMaster=auto -o ControlPersist=900s" - when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version >= 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version >= 12) + when: ansible_os_family == "Debian" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int >= 12) - name: Change ssh_args to remove ControlPersist in REL 6 and older in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=ssh_args value="" - when: (ansible_os_family == "RedHat" and ansible_distribution_major_version < 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version < 12) + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 7) or (ansible_os_family == "Suse" and ansible_distribution_major_version|int < 12) - name: Activate SSH pipelining in ansible.cfg ini_file: dest=/etc/ansible/ansible.cfg section=ssh_connection option=pipelining value=True From 8198724e989537b118b9c462f071e357fbd36fd9 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 26 May 2017 10:15:53 +0200 Subject: [PATCH 494/509] Implements #182 --- IM/tosca/Tosca.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 57e988c6d..bd3719643 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -1116,9 +1116,10 @@ def _gen_system(self, node, nodetemplates): 'image': 'disk.0.image.url', 'credential': 'disk.0.os.credentials', 'num_cpus': 'cpu.count', - 'disk_size': 'disk.0.size', + 'disk_size': 'disks.free_size', 'mem_size': 'memory.size', - 'cpu_frequency': 'cpu.performance' + 'cpu_frequency': 'cpu.performance', + 'instance_type': 'instance_type', } for cap_type in ['os', 'host']: From 601c20e45123a784dfadfb0aea973bc1642be34f Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 26 May 2017 10:33:00 +0200 Subject: [PATCH 495/509] Improve Dockerfile --- docker/Dockerfile | 69 +++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0858d9ac4..9b81e993f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,49 +1,36 @@ # Dockerfile to create a container with the IM service and TOSCA support FROM ubuntu:16.04 MAINTAINER Miguel Caballer -LABEL version="1.5.4" +LABEL version="1.5.2" LABEL description="Container image to run the IM service with TOSCA support. (http://www.grycap.upv.es/im)" -# Update and install all the necessary packages -RUN apt-get update && apt-get install -y \ - gcc \ - python-dev \ - python-pip \ - python-dateutil \ - openssh-client \ - sshpass \ - git \ - libssl-dev \ - libffi-dev \ - libmysqld-dev \ - python-pysqlite2 \ - python-requests \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install cheroot to enable REST API -RUN pip install setuptools --upgrade -I -RUN pip install cheroot -RUN pip install pbr pyOpenSSL --upgrade -I -# Install pip optional libraries -RUN pip install MySQL-python msrest msrestazure azure-common azure-mgmt-storage azure-mgmt-compute azure-mgmt-network azure-mgmt-resource - -# Install tosca-parser -RUN cd tmp \ - && git clone --recursive https://github.com/indigo-dc/tosca-parser.git \ - && cd tosca-parser \ - && pip install /tmp/tosca-parser - -# Install im indigo tosca fork -RUN cd tmp \ - && git clone --recursive https://github.com/indigo-dc/im.git \ - && cd im \ - && pip install /tmp/im - -COPY ansible.cfg /etc/ansible/ansible.cfg - -# Turn on the REST services -RUN sed -i "/ACTIVATE_REST =.*/c\ACTIVATE_REST=True" /etc/im/im.cfg +# Install Ansible +RUN echo "deb http://ppa.launchpad.net/ansible/ansible/ubuntu xenial main" > /etc/apt/sources.list.d/ansible-ubuntu-ansible-xenial.list && \ + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 93C4A3FD7BB9C367 && \ + apt-get update && apt-get -y install wget ansible && \ + rm -rf /var/lib/apt/lists/* + +# Install Azure python SDK +RUN wget https://launchpad.net/ubuntu/+archive/primary/+files/python-msrest_0.4.4-1_all.deb && \ + wget https://launchpad.net/ubuntu/+archive/primary/+files/python-msrestazure_0.4.3-1_all.deb && \ + wget https://launchpad.net/ubuntu/+archive/primary/+files/python-azure_2.0.0~rc6+dfsg-2_all.deb && \ + dpkg -i python-msrest_*_all.deb ; \ + dpkg -i python-msrestazure_*_all.deb ; \ + dpkg -i python-azure_*_all.deb ; \ + rm *.deb && \ + apt update && apt install -f -y && \ + rm -rf /var/lib/apt/lists/* + +# Install IM +RUN wget https://github.com/grycap/RADL/releases/download/v1.0.6/python-radl_1.0.6-1_all.deb && \ + wget https://github.com/indigo-dc/im/releases/download/v1.5.2/python-im_1.5.2-1_all.deb && \ + wget https://github.com/indigo-dc/tosca-parser/releases/download/0.7.2/python-tosca-parser_0.7.2-1_all.deb && \ + dpkg -i python-radl_*_all.deb ; \ + dpkg -i python-tosca-parser_*_all.deb ; \ + dpkg -i python-im_*_all.deb ; \ + rm *.deb && \ + apt update && apt install -f -y && \ + rm -rf /var/lib/apt/lists/* # Expose the IM ports EXPOSE 8899 8800 From 2ae0f61dec7f5eff3ec8a8aaed2062db44d365e4 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 5 Jun 2017 10:44:52 +0200 Subject: [PATCH 496/509] Fix error in gitbook --- doc/gitbook/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gitbook/installation.md b/doc/gitbook/installation.md index 103d47d8a..5fe041e0b 100644 --- a/doc/gitbook/installation.md +++ b/doc/gitbook/installation.md @@ -93,7 +93,7 @@ The IM provides a script to install the IM in one single step. You only need to execute the following command: ```sh -$ wget -qO- https://raw.githubusercontent.com/grycap/im/master/install.sh | bash +$ wget -qO- https://raw.githubusercontent.com/indigo-dc/im/master/install.sh | bash ``` It works for the most recent version of the main Linux distributions (RHEL/CentOS 7, Ubuntu 14/16). From f411172e0a57d455286dd39252b2a3e7cbed4e76 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 5 Jun 2017 10:59:40 +0200 Subject: [PATCH 497/509] Update to use indigo 2 repos --- ansible_install.yaml | 10 +++++++--- doc/gitbook/installation.md | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ansible_install.yaml b/ansible_install.yaml index 3f5713503..093ce48ca 100644 --- a/ansible_install.yaml +++ b/ansible_install.yaml @@ -16,9 +16,13 @@ action: yum pkg=libselinux-python state=installed when: ansible_os_family == "RedHat" - - name: Ubuntu install indigo list + - name: Ubuntu 14 install indigo 1 list get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list dest=/etc/apt/sources.list.d/indigo1-ubuntu14_04.list - when: ansible_distribution == "Ubuntu" + when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "14" + + - name: Ubuntu 16 install indigo 2 list + get_url: url=http://repo.indigo-datacloud.eu/repos/2/indigo2-ubuntu16_04.list dest=/etc/apt/sources.list.d/indigo2-ubuntu16_04.list + when: ansible_distribution == "Ubuntu" and ansible_distribution_major_version == "16" - apt_key: url=http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc state=present when: ansible_distribution == "Ubuntu" @@ -32,7 +36,7 @@ when: ansible_distribution == "Ubuntu" - name: RH indigo repos - get_url: url=http://repo.indigo-datacloud.eu/repos/1/indigo1.repo dest=/etc/yum.repos.d/indigo1.repo + get_url: url=http://repo.indigo-datacloud.eu/repos/2/indigo2.repo dest=/etc/yum.repos.d/indigo2.repo when: ansible_os_family == "RedHat" - rpm_key: state=present key=http://repo.indigo-datacloud.eu/repository/RPM-GPG-KEY-indigodc diff --git a/doc/gitbook/installation.md b/doc/gitbook/installation.md index 5fe041e0b..790a92792 100644 --- a/doc/gitbook/installation.md +++ b/doc/gitbook/installation.md @@ -107,11 +107,11 @@ $ yum install epel-release ``` Then you have to enable the INDIGO - DataCloud packages repositories. See full instructions -[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the repo file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1.repo) in your /etc/yum.repos.d folder. +[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_2.html#id4). Briefly you have to download the repo file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/2/indigo2.repo) in your /etc/yum.repos.d folder. ```sh $ cd /etc/yum.repos.d -$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1.repo +$ wget http://repo.indigo-datacloud.eu/repos/2/indigo2.repo ``` And then install the GPG key for the INDIGO repository: @@ -129,11 +129,11 @@ $ yum install IM ### 1.3.3 FROM DEB You have to enable the INDIGO - DataCloud packages repositories. See full instructions -[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_1.html#id4). Briefly you have to download the list file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list) in your /etc/apt/sources.list.d folder. +[here](https://indigo-dc.gitbooks.io/indigo-datacloud-releases/content/generic_installation_and_configuration_guide_2.html#id4). Briefly you have to download the list file from [INDIGO SW Repository](http://repo.indigo-datacloud.eu/repos/2/indigo2-ubuntu16_04.list) in your /etc/apt/sources.list.d folder. ```sh $ cd /etc/apt/sources.list.d -$ wget http://repo.indigo-datacloud.eu/repos/1/indigo1-ubuntu14_04.list +$ wget http://repo.indigo-datacloud.eu/repos/2/indigo2-ubuntu16_04.list ``` And then install the GPG key for INDIGO the repository: From 97a28080be09612e8bcdaf154f67cefa86b25ce4 Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 6 Jun 2017 15:56:18 +0200 Subject: [PATCH 498/509] Update docs --- doc/gitbook/docker-image.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/gitbook/docker-image.md b/doc/gitbook/docker-image.md index 29904d3dd..33a8bacfe 100644 --- a/doc/gitbook/docker-image.md +++ b/doc/gitbook/docker-image.md @@ -9,6 +9,13 @@ How to launch the IM service using docker:: ```sh $ sudo docker run -d -p 8899:8899 -p 8800:8800 --name im indigodatacloud/im ``` + +To make the IM data persistent you also have to specify a persistent location for the IM database using the IM_DATA_DB environment variable and adding a volume:: + +```sh +$ sudo docker run -d -p 8899:8899 -p 8800:8800 -v "/some_local_path/db:/db" -e IM_DATA_DB=/db/inf.dat --name im indigodatacloud/im +``` + You can also specify an external MySQL server to store IM data using the IM_DATA_DB environment variable:: ```sh From 1d93b5e7ed83b1d41b2c3198c71c63b992efcd11 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 12 Jul 2017 08:21:19 +0200 Subject: [PATCH 499/509] Implements: #191 --- IM/tosca/Tosca.py | 58 ++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bd3719643..bbda12ecd 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -101,8 +101,10 @@ def to_radl(self, inf_info=None): else: num_instances = 1 - num_instances = num_instances - \ - self._get_num_instances(sys.name, inf_info) + current_num_instances = self._get_num_instances(sys.name, inf_info) + num_instances = num_instances - current_num_instances + Tosca.logger.debug("User requested %d instances of type %s and there" + " are %s" % (num_instances, sys.name, current_num_instances)) # TODO: Think about to check the IDs of the VMs if num_instances < 0: @@ -216,8 +218,9 @@ def _add_node_nets(self, node, radl, system, nodetemplates): system.setValue('net_interface.%d.ip' % num, ip) else: public_ip = False + private_ip = True - # This is the solution using the public_ip property + # This is the solution using the deprecated public_ip property node_props = node.get_properties() if node_props and "public_ip" in node_props: public_ip = self._final_function_result(node_props["public_ip"].value, node) @@ -244,35 +247,38 @@ def _add_node_nets(self, node, radl, system, nodetemplates): net_provider_id = ".".join(parts[:-1]) if cap_props and "dns_name" in cap_props: dns_name = self._final_function_result(cap_props["dns_name"].value, node) + if cap_props and "private_ip" in cap_props: + private_ip = self._final_function_result(cap_props["private_ip"].value, node) if cap_props and "ports" in cap_props: ports = self._final_function_result(cap_props["ports"].value, node) # The private net is always added - private_nets = [] - for net in radl.networks: - if not net.isPublic(): - private_nets.append(net) - - if private_nets: - private_net = None - for net in private_nets: - num_net = system.getNumNetworkWithConnection(net.id) - if num_net is not None: - private_net = net - break - - if not private_net: - # There are a public net but it has not been used in this - # VM - private_net = private_nets[0] + if not public_ip or private_ip: + private_nets = [] + for net in radl.networks: + if not net.isPublic(): + private_nets.append(net) + + if private_nets: + private_net = None + for net in private_nets: + num_net = system.getNumNetworkWithConnection(net.id) + if num_net is not None: + private_net = net + break + + if not private_net: + # There are a public net but it has not been used in this + # VM + private_net = private_nets[0] + num_net = system.getNumNetworkIfaces() + else: + # There no public net, create one + private_net = network.createNetwork("private_net", False) + radl.networks.append(private_net) num_net = system.getNumNetworkIfaces() - else: - # There no public net, create one - private_net = network.createNetwork("private_net", False) - radl.networks.append(private_net) - num_net = system.getNumNetworkIfaces() - system.setValue('net_interface.' + str(num_net) + '.connection', private_net.id) + system.setValue('net_interface.' + str(num_net) + '.connection', private_net.id) # If the node needs a public IP if public_ip: From 0071bec2e422816be974e1063ca5bc4596707d22 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 17 Jul 2017 11:35:27 +0200 Subject: [PATCH 500/509] Fix: #193 --- IM/tosca/Tosca.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bbda12ecd..11746cf8d 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -1241,8 +1241,8 @@ def _get_attached_disks(self, node, nodetemplates): device = value if trgt.type_definition.type == "tosca.nodes.BlockStorage": - size, unit = Tosca._get_size_and_unit( - trgt.get_property_value('size')) + full_size = self._final_function_result(trgt.get_property_value('size'), trgt) + size, unit = Tosca._get_size_and_unit(full_size) disks.append((size, unit, location, device, count, "ext4")) count += 1 else: From 6982519aa6c5fdf3d61c5dfd488a89fc3371fcda Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 5 Sep 2017 11:59:26 +0200 Subject: [PATCH 501/509] Increase timeout in test --- test/integration/TestIM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/TestIM.py b/test/integration/TestIM.py index 6fe8bd821..ef8302a2d 100755 --- a/test/integration/TestIM.py +++ b/test/integration/TestIM.py @@ -663,7 +663,7 @@ def test_90_create(self): self.__class__.inf_id = [inf_id] all_configured = self.wait_inf_state( - inf_id, VirtualMachine.CONFIGURED, 1000) + inf_id, VirtualMachine.CONFIGURED, 1200) self.assertTrue( all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") From 8008c3a4f8f485b7447f5316de9f86bbfd5915d2 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 8 Sep 2017 09:59:39 +0200 Subject: [PATCH 502/509] Implements #196 --- IM/tosca/Tosca.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 11746cf8d..75f4712eb 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -245,6 +245,9 @@ def _add_node_nets(self, node, radl, system, nodetemplates): elif network_name.endswith(".PRIVATE"): parts = network_name.split(".") net_provider_id = ".".join(parts[:-1]) + else: + # assume that is a private one + net_provider_id = network_name if cap_props and "dns_name" in cap_props: dns_name = self._final_function_result(cap_props["dns_name"].value, node) if cap_props and "private_ip" in cap_props: From 16670c1529cc8af204bd225ce5d2c90418a1b294 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Sep 2017 12:44:43 +0200 Subject: [PATCH 503/509] Fix error --- IM/tosca/Tosca.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 75f4712eb..bbd569400 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -245,7 +245,7 @@ def _add_node_nets(self, node, radl, system, nodetemplates): elif network_name.endswith(".PRIVATE"): parts = network_name.split(".") net_provider_id = ".".join(parts[:-1]) - else: + elif network_name != "PRIVATE": # assume that is a private one net_provider_id = network_name if cap_props and "dns_name" in cap_props: From ceb18b1219452d5ef5e8f94ee3209d2d5889fd0b Mon Sep 17 00:00:00 2001 From: micafer Date: Tue, 12 Sep 2017 12:38:04 +0200 Subject: [PATCH 504/509] Fix style --- IM/SSHRetry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IM/SSHRetry.py b/IM/SSHRetry.py index a45d3ae15..fbeb0c5d8 100755 --- a/IM/SSHRetry.py +++ b/IM/SSHRetry.py @@ -19,6 +19,7 @@ from IM.SSH import SSH, AuthenticationException import paramiko + class SSHRetry(SSH): """ SSH class decorated to perform a number of retries """ TRIES = 5 From f1bd982aafb6d19e04c29c2666fd60f38eedcb6e Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 14 Sep 2017 18:10:01 +0200 Subject: [PATCH 505/509] Update dockerfile --- docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9b81e993f..a9fb107d5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,9 +22,9 @@ RUN wget https://launchpad.net/ubuntu/+archive/primary/+files/python-msrest_0.4. rm -rf /var/lib/apt/lists/* # Install IM -RUN wget https://github.com/grycap/RADL/releases/download/v1.0.6/python-radl_1.0.6-1_all.deb && \ - wget https://github.com/indigo-dc/im/releases/download/v1.5.2/python-im_1.5.2-1_all.deb && \ - wget https://github.com/indigo-dc/tosca-parser/releases/download/0.7.2/python-tosca-parser_0.7.2-1_all.deb && \ +RUN wget https://github.com/grycap/RADL/releases/download/v1.1.0/python-radl_1.1.0-1_all.deb && \ + wget https://github.com/indigo-dc/im/releases/download/v1.6.0/python-im_1.6.0-1_all.deb && \ + wget https://github.com/indigo-dc/tosca-parser/releases/download/0.8.2/python-tosca-parser_0.8.2-1_all.deb && \ dpkg -i python-radl_*_all.deb ; \ dpkg -i python-tosca-parser_*_all.deb ; \ dpkg -i python-im_*_all.deb ; \ From 283cf4170bdf886dcfa05cb6d47843d89cf6ac66 Mon Sep 17 00:00:00 2001 From: micafer Date: Wed, 20 Sep 2017 17:16:12 +0200 Subject: [PATCH 506/509] Fix install.sh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index b93c80f54..aa4ccef66 100755 --- a/install.sh +++ b/install.sh @@ -94,7 +94,7 @@ then echo "ansible_install.yaml file present. Do not download." else echo "Downloading ansible_install.yaml file from github." - wget http://raw.githubusercontent.com/grycap/im/master/ansible_install.yaml + wget http://raw.githubusercontent.com/indigo-dc/im/master/ansible_install.yaml fi echo "Call Ansible playbook to install the IM." From 6f858dbef22a6559f015506ca5629de2f80efcb1 Mon Sep 17 00:00:00 2001 From: micafer Date: Thu, 2 Nov 2017 13:29:21 +0100 Subject: [PATCH 507/509] Fix error --- IM/REST.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IM/REST.py b/IM/REST.py index 747e6a1ab..09c31a17c 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -352,7 +352,7 @@ def RESTGetInfrastructureProperty(infid=None, prop=None): return return_error(415, "Unsupported Accept Media Types: %s" % accept) bottle.response.content_type = "application/json" auth = InfrastructureManager.check_auth_data(auth) - sel_inf = InfrastructureManager.get_infrastructure(id, auth) + sel_inf = InfrastructureManager.get_infrastructure(infid, auth) if "TOSCA" in sel_inf.extra_info: res = sel_inf.extra_info["TOSCA"].get_outputs(sel_inf) else: @@ -534,7 +534,7 @@ def RESTAddResource(infid=None): elif "text/yaml" in content_type: tosca_data = Tosca(radl_data) auth = InfrastructureManager.check_auth_data(auth) - sel_inf = InfrastructureManager.get_infrastructure(id, auth) + sel_inf = InfrastructureManager.get_infrastructure(infid, auth) # merge the current TOSCA with the new one if isinstance(sel_inf.extra_info['TOSCA'], Tosca): tosca_data = sel_inf.extra_info['TOSCA'].merge(tosca_data) @@ -551,7 +551,7 @@ def RESTAddResource(infid=None): # Replace the TOSCA document if tosca_data: - sel_inf = InfrastructureManager.get_infrastructure(id, auth) + sel_inf = InfrastructureManager.get_infrastructure(infid, auth) sel_inf.extra_info['TOSCA'] = tosca_data protocol = "http://" From ed97cedfb266d6ea1f94b74f616aa5890dd68e29 Mon Sep 17 00:00:00 2001 From: micafer Date: Fri, 3 Nov 2017 08:49:53 +0100 Subject: [PATCH 508/509] Fix conf-ansible to force install pyOpenSSL --- contextualization/conf-ansible.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contextualization/conf-ansible.yml b/contextualization/conf-ansible.yml index cda57640b..4317eee67 100644 --- a/contextualization/conf-ansible.yml +++ b/contextualization/conf-ansible.yml @@ -93,6 +93,12 @@ file: src=/usr/bin/python2.6 dest=/usr/bin/python_ansible state=link when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 6 + - name: Upgrade pip + pip: name=pip extra_args="-I" state=latest + + - name: Upgrade pyOpenSSL with Pip + pip: name=pyOpenSSL extra_args="-I" state=latest + - name: Upgrade setuptools with Pip pip: name=setuptools extra_args="-I" state=latest when: ansible_os_family == "Suse" or (ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 7) From 178e8653a7cce03c27a08e931d28d368e1faed65 Mon Sep 17 00:00:00 2001 From: micafer Date: Mon, 11 Dec 2017 16:44:31 +0100 Subject: [PATCH 509/509] Implements #216 --- IM/tosca/Tosca.py | 27 ++++++++++++++++++++++++++- test/files/tosca_long.yml | 18 ++++++++++++++---- test/unit/Tosca.py | 3 +++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index bbd569400..1754c2831 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -44,6 +44,30 @@ def serialize(self): def deserialize(str_data): return Tosca(str_data) + def _get_cloud_id(self, sys_name): + """ + Get the cloud ID of the deployment based on policies + """ + for policy in self.tosca.policies: + if policy.type_definition.type == "tosca.policies.Placement": + node_list = [] + if policy.targets_type == "node_templates": + node_list = policy.targets_list + elif policy.targets_type == "groups": + for group in policy.targets_list: + node_list.extend(group.member_nodes) + + for node in node_list: + if node.name == sys_name: + if 'cloud_id' in policy.properties: + Tosca.logger.debug("Set cloud id: %s to system: %s." % (policy.properties['cloud_id'], + sys_name)) + return policy.properties['cloud_id'] + else: + Tosca.logger.warn("Policy %s not supported. Ignoring it." % policy.type_definition.type) + + return None + def to_radl(self, inf_info=None): """ Converts the current ToscaTemplate object in a RADL object @@ -111,7 +135,8 @@ def to_radl(self, inf_info=None): all_removal_list.extend(removal_list[0:-num_instances]) if num_instances > 0: - dep = deploy(sys.name, num_instances) + cloud_id = self._get_cloud_id(sys.name) + dep = deploy(sys.name, num_instances, cloud_id) radl.deploys.append(dep) compute = node else: diff --git a/test/files/tosca_long.yml b/test/files/tosca_long.yml index 5a4de041e..9c9c792e0 100644 --- a/test/files/tosca_long.yml +++ b/test/files/tosca_long.yml @@ -123,7 +123,7 @@ topology_template: capabilities: scalable: properties: - count: 0 + count: 1 host: properties: num_cpus: 1 @@ -155,8 +155,18 @@ topology_template: galaxy_url: value: { concat: [ 'http://', get_attribute: [ lrms_server, public_address, 0 ], ':8080' ] } + groups: + my_placement_group: + type: tosca.groups.Root + members: [ lrms_server, lrms_wn ] + policies: - - deploy_on_aws: + - deploy_on_cloudid: + type: tosca.policies.Placement + properties: { cloud_id: cloudid } + targets: [ other_server ] + + - deploy_group_on_cloudid: type: tosca.policies.Placement - properties: { sla_id: SLA_provider-AWS-us-east-1_indigo-dc, username: {get_input: access_key}, password: {get_input: secret_key}} - targets: [ lrms_wn ] + properties: { cloud_id: cloudid } + targets: [ my_placement_group ] \ No newline at end of file diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index 0dda407d4..d8bb45d52 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -63,6 +63,9 @@ def test_tosca_to_radl(self): lrms_server = radl.get_system_by_name('lrms_server') self.assertEqual(lrms_server.getValue('memory.size'), 1000000000) self.assertEqual(lrms_server.getValue('net_interface.0.dns_name'), 'slurmserver') + self.assertEqual("cloudid", radl.deploys[0].cloud_id) + self.assertEqual("cloudid", radl.deploys[1].cloud_id) + self.assertEqual("cloudid", radl.deploys[2].cloud_id) def test_tosca_get_outputs(self): """Test TOSCA get_outputs function"""
- -
-
-
-
-
- {% block body %}{% endblock %} -
-
-© I3M-GRyCAP-UPV, - Universitat Politècnica de València - 46022, Valencia.
-Contact: micafer1 _at_ upv.es -
-
-
-