diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index e9329e7e6..3e042fd04 100755 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -31,7 +31,7 @@ import InfrastructureInfo from IM.radl import radl_parse -from IM.radl.radl import Feature +from IM.radl.radl import Feature, RADL from IM.recipe import Recipe from IM.db import DataBase @@ -259,7 +259,10 @@ def Reconfigure(inf_id, radl_data, auth, vm_list = None): """ InfrastructureManager.logger.info("Reconfiguring the inf: " + str(inf_id)) - radl = radl_parse.parse_radl(radl_data) + if isinstance(radl_data, RADL): + radl = radl_data + else: + radl = radl_parse.parse_radl(radl_data) InfrastructureManager.logger.debug(radl) sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) @@ -356,10 +359,13 @@ 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() + if isinstance(radl_data, RADL): + radl = radl_data + else: + radl = radl_parse.parse_radl(radl_data) InfrastructureManager.logger.debug(radl) + radl.check() sel_inf = InfrastructureManager.get_infrastructure(inf_id, auth) @@ -604,13 +610,7 @@ def GetVMProperty(inf_id, vm_id, property_name, auth): Return: a str with the property value """ - radl_data = InfrastructureManager.GetVMInfo(inf_id, vm_id, auth) - - try: - radl = radl_parse.parse_radl(radl_data) - except Exception, ex: - InfrastructureManager.logger.exception("Error parsing the RADL: " + radl_data) - raise ex + radl = InfrastructureManager.GetVMInfo(inf_id, vm_id, auth) res = None if radl.systems: @@ -689,7 +689,10 @@ def AlterVM(inf_id, vm_id, radl_data, auth): InfrastructureManager.logger.info("VM does not exist or Access Error") raise Exception("VM does not exist or Access Error") - radl = radl_parse.parse_radl(radl_data) + if isinstance(radl_data, RADL): + radl = radl_data + else: + radl = radl_parse.parse_radl(radl_data) exception = None try: @@ -706,7 +709,7 @@ def AlterVM(inf_id, vm_id, radl_data, auth): vm.update_status(auth) InfrastructureManager.save_data(inf_id) - return str(vm.info) + return vm.info @staticmethod def GetInfrastructureRADL(inf_id, auth): diff --git a/IM/REST.py b/IM/REST.py index 0fba58e6f..549c2e940 100644 --- a/IM/REST.py +++ b/IM/REST.py @@ -21,6 +21,8 @@ import bottle import json from config import Config +from radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json +from bottle import HTTPError AUTH_LINE_SEPARATOR = '\\n' @@ -95,6 +97,19 @@ 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) + if accept: + pos = accept.find(";") + if pos != -1: + accept = accept[:pos] + return accept.strip() + else: + return accept + @app.route('/infrastructures/:id', method='DELETE') def RESTDestroyInfrastructure(id=None): try: @@ -105,6 +120,7 @@ def RESTDestroyInfrastructure(id=None): try: InfrastructureManager.DestroyInfrastructure(id, auth) + bottle.response.content_type = "text/plain" return "" except DeletedInfrastructureException, ex: bottle.abort(404, "Error Destroying Inf: " + str(ex)) @@ -166,6 +182,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 @@ -209,11 +227,22 @@ def RESTCreateInfrastructure(): bottle.abort(401, "No authentication data provided") try: + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() + + if content_type: + if content_type == "application/json": + radl_data = parse_radl_json(radl_data) + elif content_type != "text/plain": + bottle.abort(415, "Unsupported Media Type %s" % content_type) + return False + inf_id = InfrastructureManager.CreateInfrastructure(radl_data, auth) 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 @@ -230,9 +259,27 @@ def RESTGetVMInfo(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: - info = InfrastructureManager.GetVMInfo(infid, vmid, auth) - bottle.response.content_type = "text/plain" + accept = get_media_type('Accept') + + radl = InfrastructureManager.GetVMInfo(infid, vmid, auth) + + if accept: + if accept == "application/json": + bottle.response.content_type = accept + info = dump_radl_json(radl, enter="", indent="") + elif accept == "text/plain": + info = str(radl) + bottle.response.content_type = accept + else: + bottle.abort(404, "Unsupported Accept Media Type: %s" % accept) + return False + else: + info = str(radl) + 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 @@ -262,8 +309,24 @@ def RESTGetVMProperty(infid=None, vmid=None, prop=None): info = InfrastructureManager.GetVMContMsg(infid, vmid, auth) else: info = InfrastructureManager.GetVMProperty(infid, vmid, prop, auth) - bottle.response.content_type = "text/plain" + + accept = get_media_type('Accept') + if accept: + if accept == "application/json": + bottle.response.content_type = accept + if isinstance(info, str) or isinstance(info, unicode): + info = '"' + info + '"' + elif accept == "text/plain": + bottle.response.content_type = accept + else: + bottle.abort(404, "Unsupported Accept Media Type: %s" % accept) + return False + else: + 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 @@ -298,8 +361,17 @@ def RESTAddResource(id=None): context = False else: bottle.abort(400, "Incorrect value in context parameter") - + + content_type = get_media_type('Content-Type') radl_data = bottle.request.body.read() + + if content_type: + if content_type == "application/json": + radl_data = parse_radl_json(radl_data) + elif content_type != "text/plain": + bottle.abort(415, "Unsupported Media Type %s" % content_type) + return False + vm_ids = InfrastructureManager.AddResource(id, radl_data, auth, context) res = "" @@ -310,6 +382,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 @@ -340,7 +414,10 @@ def RESTRemoveResource(infid=None, vmid=None): bottle.abort(400, "Incorrect value in context parameter") 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 @@ -366,10 +443,35 @@ def RESTAlterVM(infid=None, vmid=None): bottle.abort(401, "No authentication data provided") try: + content_type = get_media_type('Content-Type') + accept = get_media_type('Accept') radl_data = bottle.request.body.read() - bottle.response.content_type = "text/plain" - return InfrastructureManager.AlterVM(infid, vmid, radl_data, auth) + if content_type: + if content_type == "application/json": + radl_data = parse_radl_json(radl_data) + elif content_type != "text/plain": + bottle.abort(415, "Unsupported Media Type %s" % content_type) + return False + + vm_info = InfrastructureManager.AlterVM(infid, vmid, radl_data, auth) + + if accept: + if accept == "application/json": + bottle.response.content_type = accept + res = dump_radl_json(vm_info, enter="", indent="") + elif accept == "text/plain": + res = str(vm_info) + bottle.response.content_type = accept + else: + bottle.abort(404, "Unsupported Accept Media Type: %s" % accept) + return False + else: + 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 @@ -403,11 +505,21 @@ def RESTReconfigureInfrastructure(id=None): except: bottle.abort(400, "Incorrect vm_list format.") - if 'radl' in bottle.request.forms.keys(): - radl_data = bottle.request.forms.get('radl') + content_type = get_media_type('Content-Type') + radl_data = bottle.request.body.read() + + if radl_data: + if content_type: + if content_type == "application/json": + radl_data = parse_radl_json(radl_data) + elif content_type != "text/plain": + bottle.abort(415, "Unsupported Media Type %s" % content_type) + return False 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 diff --git a/IM/ServiceRequests.py b/IM/ServiceRequests.py index c399bc26a..0a7fd98a4 100644 --- a/IM/ServiceRequests.py +++ b/IM/ServiceRequests.py @@ -152,7 +152,7 @@ class Request_GetVMInfo(IMBaseRequest): def _call_function(self): self._error_mesage = "Error Getting VM Info." (inf_id, vm_id, auth_data) = self.arguments - return InfrastructureManager.InfrastructureManager.GetVMInfo(inf_id, vm_id, Authentication(auth_data)) + return str(InfrastructureManager.InfrastructureManager.GetVMInfo(inf_id, vm_id, Authentication(auth_data))) class Request_GetVMProperty(IMBaseRequest): """ @@ -170,7 +170,7 @@ class Request_AlterVM(IMBaseRequest): def _call_function(self): self._error_mesage = "Error Changing VM Info." (inf_id, vm_id, radl, auth_data) = self.arguments - return InfrastructureManager.InfrastructureManager.AlterVM(inf_id, vm_id, radl, Authentication(auth_data)) + return str(InfrastructureManager.InfrastructureManager.AlterVM(inf_id, vm_id, radl, Authentication(auth_data))) class Request_DestroyInfrastructure(IMBaseRequest): """ diff --git a/IM/VirtualMachine.py b/IM/VirtualMachine.py index 893407fc6..d4dc6df0b 100644 --- a/IM/VirtualMachine.py +++ b/IM/VirtualMachine.py @@ -730,7 +730,7 @@ def get_vm_info(self): res = RADL() res.networks = self.info.networks res.systems = self.info.systems - return str(res) + return res def get_ssh_ansible_master(self): ansible_host = None diff --git a/IM/__init__.py b/IM/__init__.py index 9e9ca5724..132412d54 100644 --- a/IM/__init__.py +++ b/IM/__init__.py @@ -16,6 +16,6 @@ __all__ = ['auth','bottle','CloudManager','config','ConfManager','db','ganglia','HTTPHeaderTransport','ImageManager','InfrastructureInfo','InfrastructureManager','parsetab','radl','recipe','request','REST', 'ServiceRequests','SSH','timedcall','uriparse','VMRC','xmlobject'] -__version__ = '1.4.1' +__version__ = '1.4.2' __author__ = 'Miguel Caballer' diff --git a/IM/ansible/ansible_launcher.py b/IM/ansible/ansible_launcher.py index a7ee01ece..328b34581 100755 --- a/IM/ansible/ansible_launcher.py +++ b/IM/ansible/ansible_launcher.py @@ -37,7 +37,7 @@ from ansible.cli import CLI from ansible.parsing.dataloader import DataLoader from ansible.vars import VariableManager - from ansible.inventory import Inventory + import ansible.inventory from ansible_executor_v2 import IMPlaybookExecutor @@ -137,10 +137,15 @@ def launch_playbook_v2(self): options.forks = self.threads loader = DataLoader() - inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=options.inventory) + # Add this to avoid the Ansible bug: no host vars as host is not in inventory + # In version 2.0.1 it must be fixed + ansible.inventory.HOSTS_PATTERNS_CACHE = {} + + inventory = ansible.inventory.Inventory(loader=loader, variable_manager=variable_manager, host_list=options.inventory) variable_manager.set_inventory(inventory) - inventory.subset(self.host) + if self.host: + inventory.subset(self.host) # let inventory know which playbooks are using so it can know the basedirs inventory.set_playbook_basedir(os.path.dirname(self.playbook_file)) @@ -217,8 +222,9 @@ def launch_playbook_v1(self): inventory = ansible.inventory.Inventory(self.inventory_file) else: inventory = ansible.inventory.Inventory(options.inventory) - - inventory.subset(self.host) + + if self.host: + inventory.subset(self.host) # let inventory know which playbooks are using so it can know the basedirs inventory.set_playbook_basedir(os.path.dirname(self.playbook_file)) diff --git a/IM/radl/__init__.py b/IM/radl/__init__.py index dc055b705..ea76271ff 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/radl/radl.py b/IM/radl/radl.py index b64019f2b..b36704178 100644 --- a/IM/radl/radl.py +++ b/IM/radl/radl.py @@ -1322,6 +1322,11 @@ def __init__(self, name, features, line=None): def __str__(self): return "ansible %s (%s)" % (self.id, Features.__str__(self)) + def getId(self): + """Return the id of the aspect.""" + + return self.id + def check(self, radl): """Check the features in this network.""" diff --git a/IM/radl/radl_json.py b/IM/radl/radl_json.py new file mode 100644 index 000000000..d81564744 --- /dev/null +++ b/IM/radl/radl_json.py @@ -0,0 +1,229 @@ +# 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 sys + +try: + import json +except ImportError: + import simplejson as json +try: + import yaml +except ImportError: + yaml = None + +from radl import Feature, Features, Aspect, RADL, configure, contextualize, contextualize_item, deploy, SoftFeatures, RADLParseException +import radl + +def encode_simple(d): + """Encode strings in basic python objects.""" + if isinstance(d, unicode): return d.encode() + if isinstance(d, list): return map(encode_simple, d) + if isinstance(d, dict): return dict([ (encode_simple(k), encode_simple(v)) for k, v in d.items() ]) + return d + +def parse_radl(data): + """ + Parse a RADL document in JSON. + + Args.: + - data(str or list): document to parse. + + Return(RADL): RADL object. + """ + if not isinstance(data, list): + if os.path.isfile(data): + f = open(data) + data = "".join(f.readlines()) + f.close() + data = json.loads(data) + data = encode_simple(data) + res = RADL() + for aspect in [ p_aspect(a) for a in data ]: + res.add(aspect) + return res + +def p_aspect(a): + assert "class" in a + if a["class"] == "configure": + return p_configure(a) + elif a["class"] == "contextualize": + return p_contextualize(a) + elif a["class"] == "deploy": + return p_deploy(a) + else: + return p_cfeatures(a) + +def p_configure(a): + assert a["class"] == "configure" + if a.get("reference", False): + return configure(a["id"], reference=True) + recipes = a["recipes"] + if isinstance(recipes, str) and yaml: + try: + yaml.safe_load(recipes) + except Exception, e: + raise RADLParseException("Error parsing YAML: %s" % str(e)) + return configure(a["id"], recipes) + +def p_contextualize(a): + assert a["class"] == "contextualize" + return contextualize([ p_contextualize_item(i) for i in a.get("items", []) ], + max_time=a.get("max_time", 0)) + +def p_contextualize_item(a): + return contextualize_item(a["system"], a["configure"], a.get("step", 0), a.get("ctxt_tool", None)) + +def p_deploy(a): + assert a["class"] == "deploy" + return deploy(a["system"], a["vm_number"], a.get("cloud", None)) + +def p_cfeatures(a): + assert a["class"] and a["id"] + cls = getattr(radl, a["class"]) + if a.get("reference", False): + return cls(a["id"], reference=True) + return cls(a["id"], p_features(a)) + +def p_features(a): + assert isinstance(a, dict) + def val(k, v): + if k == "softs": + return [ SoftFeatures(i.get("weight", 0), p_features(i.get("items", {}))) for i in v ] + elif k.endswith("_min") and isinstance(v, (int, float)): + return [ Feature(k[0:-4], ">=", v) ] + elif k.endswith("_max") and isinstance(v, (int, float)): + return [ Feature(k[0:-4], "<=", v) ] + elif isinstance(v, list): + return [ Feature(k, "contains", p_feature(i)) for i in v ] + else: + return [ Feature(k, "=", p_feature(v)) ] + return [ i for k, v in a.items() if k != "class" and k != "id" for i in val(k, v) ] + +def p_feature(a): + if isinstance(a, (int, float, str)): + return a + elif isinstance(a, unicode): + return str(a) + elif isinstance(a, dict) and "class" in a: + return p_cfeatures(a) + elif isinstance(a, dict): + return Features(p_features(a)) + assert False + +def dump_radl(radl, enter="\n", indent=" "): + """Dump a RADL document.""" + + indent = len(indent) if enter else None + sort_keys = indent is not None + separators = (",", ":" if indent is None else ": ") + return json.dumps(radlToSimple(radl), indent=indent, sort_keys=sort_keys, separators=separators) + +def radlToSimple(radl): + """ + Return a list of maps whose values are only other maps or lists. + """ + + aspects = radl.ansible_hosts + radl.networks + radl.systems + radl.configures + radl.deploys + if radl.contextualize.items is not None: + aspects.append(radl.contextualize) + return [ aspectToSimple(a) for a in aspects ] + +def aspectToSimple(a): + if isinstance(a, Features): + return cfeaturesToSimple(a) + elif isinstance(a, configure): + return configureToSimple(a) + elif isinstance(a, contextualize): + return contextualizeToSimple(a) + elif isinstance(a, deploy): + return deployToSimple(a) + assert False + +def configureToSimple(a): + assert isinstance(a, configure) + if a.reference or not a.recipes: + return { "class": "configure", "id": a.name, "reference": True } + else: + return { "class": "configure", "id": a.name, "recipes": a.recipes } + +def contextualizeToSimple(a): + assert isinstance(a, contextualize) + r = {"class": "contextualize"} + if a.max_time: r["max_time"] = a.max_time + if a.items: + r["items"] = [ contextualizeItemToSimple(i) for i in a.items.values() ] + return r + +def contextualizeItemToSimple(a): + assert isinstance(a, contextualize_item) + r = {"system": a.system, "configure": a.configure} + if a.num: r["step"] = a.num + if a.ctxt_tool: r["ctxt_tool"] = a.ctxt_tool + return r + +def deployToSimple(a): + assert isinstance(a, deploy) + r = {"class": "deploy", "system": a.id, "vm_number": a.vm_number} + if a.cloud_id: r["cloud"] = a.cloud_id + return r + +def cfeaturesToSimple(a): + assert isinstance(a, Features) + r = { "class": a.__class__.__name__, "id": a.getId() } + if a.reference: + r["reference"] = True + return r + r.update(featuresToSimple(a)) + return r + +def featuresToSimple(a): + assert isinstance(a, Features) + r = {} + for k, v in a.props.items(): + if k == SoftFeatures.SOFT: + r["softs"] = [ {"weight": i.soft, "items": featuresToSimple(i)} + for i in a.props[SoftFeatures.SOFT] ] + elif isinstance(v, tuple): + if v[0] and v[1] and v[0].value == v[1].value: + r[k] = v[0].value + elif v[0]: + r[k + "_min"] = v[0].value + elif v[1]: + r[k + "_max"] = v[1].value + elif isinstance(v, (set, list)): + r[k] = [ featureToSimple(i.value) for i in v ] + elif isinstance(v, dict): + r[k] = [ featureToSimple(i.value) for i in v.values() ] + else: + r[k] = featureToSimple(v.value) + return r + +def featureToSimple(a): + if isinstance(a, (int, float, str)): + return a + elif isinstance(a, unicode): + return str(a) + elif isinstance(a, Aspect): + return referenceToSimple(a) + elif isinstance(a, Features): + return featuresToSimple(a) + assert False + +def referenceToSimple(a): + assert isinstance(a, Aspect) + return { "class": a.__class__.__name__, "id": a.getId(), + "reference": True } diff --git a/README b/README index af797df30..6cfffebdb 100644 --- a/README +++ b/README @@ -14,9 +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 =============== @@ -41,6 +40,10 @@ 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/). * Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. @@ -49,9 +52,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 @@ -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. @@ -184,4 +186,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 -p 8800:8800 --name im grycap/im \ No newline at end of file diff --git a/README.md b/README.md index b220301a4..7710799fe 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 @@ -43,6 +43,12 @@ 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/). + Ansible (http://www.ansibleworks.com/) to configure nodes in the infrastructures. In particular, Ansible 1.4.2+ must be installed. @@ -52,9 +58,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 @@ -71,12 +82,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. @@ -204,5 +209,5 @@ default configuration. Information about this image can be found here: https://r 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 -p 8800:8800 --name im grycap/im ``` diff --git a/changelog b/changelog index 67c126d25..c6f4838a2 100644 --- a/changelog +++ b/changelog @@ -166,3 +166,10 @@ IM 1.4.1 * Add support for Ansible v2.X * Add supoort for using an external ansible master node * Bugfix in incorrects links inside containers + +IM 1.4.2 + * Add support for new RADL JSON format + * Change in Auth Header in new version of FogBow and support for requirements + * Code improvements in OpenStack, OpenNebula and FogBow connectors + * Added workaround to problems in ansible_launcher with HOSTS_PATTERNS_CACHE + * Bugfixes in REST API diff --git a/connectors/FogBow.py b/connectors/FogBow.py index d8b0459df..f40c68118 100644 --- a/connectors/FogBow.py +++ b/connectors/FogBow.py @@ -15,12 +15,9 @@ # along with this program. If not, see . import json -import subprocess -import shutil import os import sys import httplib -import tempfile from IM.uriparse import uriparse from IM.VirtualMachine import VirtualMachine from CloudConnector import CloudConnector @@ -34,7 +31,7 @@ class FogBowCloudConnector(CloudConnector): type = "FogBow" """str with the name of the provider.""" - INSTANCE_TYPE = 'small' + INSTANCE_TYPE = 'fogbow_small' """str with the name of the default instance type to launch.""" VM_STATE_MAP = { @@ -54,25 +51,33 @@ class FogBowCloudConnector(CloudConnector): } """Dictionary with a map with the FogBow Request states to the IM states.""" + def __init__(self, cloud_info): + # check if the user has specified the http protocol in the host and remove it + pos = cloud_info.server.find('://') + if pos != -1: + cloud_info.server = cloud_info.server[pos+3:] + CloudConnector.__init__(self, cloud_info) + def get_auth_headers(self, auth_data): """ Generate the auth header needed to contact with the FogBow server. """ auth = auth_data.getAuthInfo(FogBowCloudConnector.type) + if not auth: + raise Exception("No correct auth data has been specified to FogBow.") - if auth and 'token_type' in auth[0]: + if 'token_type' in auth[0]: token_type = auth[0]['token_type'] - plugin = IdentityPlugin.getIdentityPlugin(token_type) - token = plugin.create_token(auth[0]).replace("\n", "").replace("\r", "") - - auth_headers = {'X-Federation-Auth-Token' : token} - #auth_headers = {'X-Auth-Token' : token, 'X-Local-Auth-Token' : token, 'Authorization' : token} - - return auth_headers else: - raise Exception("Incorrect auth data") - self.logger.error("Incorrect auth data") + # If not token_type supplied, we assume that is VOMS one + token_type = 'VOMS' + plugin = IdentityPlugin.getIdentityPlugin(token_type) + token = plugin.create_token(auth[0]).replace("\n", "").replace("\r", "") + + auth_headers = {'X-Auth-Token' : token} + + return auth_headers def concreteSystem(self, radl_system, auth_data): image_urls = radl_system.getValue("disk.0.image.url") @@ -88,9 +93,6 @@ def concreteSystem(self, radl_system, auth_data): protocol = url[0] if protocol in ['fbw']: res_system = radl_system.clone() - - if not res_system.hasFeature('instance_type'): - res_system.addFeature(Feature("instance_type", "=", self.INSTANCE_TYPE), conflict="me", missing="other") res_system.addFeature(Feature("disk.0.image.url", "=", str_url), conflict="other", missing="other") @@ -164,12 +166,15 @@ def updateVMInfo(self, vm, auth_data): elif resp.status != 200: return (False, resp.reason + "\n" + output) else: + providing_member = self.get_occi_attribute_value(output,'org.fogbowcloud.request.providing-member') + if providing_member == "null": + providing_member = None instance_id = self.get_occi_attribute_value(output,'org.fogbowcloud.request.instance-id') if instance_id == "null": instance_id = None if not instance_id: - vm.state = self.VM_REQ_STATE_MAP.get(self.get_occi_attribute_value(output, 'org.fogbowcloud.request.state'), VirtualMachine.UNKNOWN) + vm.state = VirtualMachine.PENDING return (True, vm) else: # Now get the instance info @@ -201,41 +206,19 @@ def updateVMInfo(self, vm, auth_data): if len(parts) > 1: vm.setSSHPort(int(parts[1])) + 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") + + vm.info.systems[0].setValue('instance_id', instance_id) + vm.info.systems[0].setValue('availability_zone', providing_member) + return (True, vm) except Exception, ex: self.logger.exception("Error connecting with FogBow Manager") return (False, "Error connecting with FogBow Manager: " + str(ex)) - def keygen(self): - """ - Generates a keypair using the ssh-keygen command and returns a tuple (public, private) - """ - tmp_dir = tempfile.mkdtemp() - pk_file = tmp_dir + "/occi-key" - command = 'ssh-keygen -t rsa -b 2048 -q -N "" -f ' + pk_file - p=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - (out, err) = p.communicate() - if p.returncode!=0: - shutil.rmtree(tmp_dir, ignore_errors=True) - self.logger.error("Error executing ssh-keygen: " + out + err) - return (None, None) - else: - public = None - private = None - try: - with open(pk_file) as f: private = f.read() - except: - self.logger.exception("Error reading private_key file.") - - try: - with open(pk_file + ".pub") as f: public = f.read() - except: - self.logger.exception("Error reading public_key file.") - - shutil.rmtree(tmp_dir, ignore_errors=True) - return (public, private) - def launch(self, inf, radl, requested_radl, num_vm, auth_data): system = radl.systems[0] auth_headers = self.get_auth_headers(auth_data) @@ -273,16 +256,35 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): conn.putheader(k, v) conn.putheader('Category', 'fogbow_request; scheme="http://schemas.fogbowcloud.org/request#"; class="kind"') - + conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.request.instance-count=1') conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.request.type="one-time"') - conn.putheader('Category', 'fogbow_' + system.getValue('instance_type') + '; scheme="http://schemas.fogbowcloud.org/template/resource#"; class="mixin"') + requirements = "" + if system.getValue('instance_type'): + conn.putheader('Category', system.getValue('instance_type') + '; scheme="http://schemas.fogbowcloud.org/template/resource#"; class="mixin"') + else: + cpu = system.getValue('cpu.count') + memory = system.getFeature('memory.size').getValue('M') + if cpu: + requirements += "Glue2vCPU >= %d" % cpu + if memory: + if requirements: + requirements += " && " + requirements += "Glue2RAM >= %d" % memory + conn.putheader('Category', os_tpl + '; scheme="http://schemas.fogbowcloud.org/template/os#"; class="mixin"') conn.putheader('Category', 'fogbow_public_key; scheme="http://schemas.fogbowcloud/credentials#"; class="mixin"') - conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.credentials.publickey.data="' + public_key.strip() + '"') + if system.getValue('availability_zone'): + if requirements: + requirements += ' && ' + requirements += 'Glue2CloudComputeManagerID == "%s"' % system.getValue('availability_zone') + + if requirements: + conn.putheader('X-OCCI-Attribute', 'org.fogbowcloud.request.requirements=' + requirements) + conn.endheaders() resp = conn.getresponse() @@ -389,19 +391,28 @@ class OpenNebulaIdentityPlugin(IdentityPlugin): @staticmethod def create_token(params): - return params['username'] + ":" + params['password'] + if 'username' in params and 'password' in params: + return params['username'] + ":" + params['password'] + else: + raise Exception("Incorrect auth data, username and password must be specified") class X509IdentityPlugin(IdentityPlugin): @staticmethod def create_token(params): - return params['proxy'] + if 'proxy' in params: + return params['proxy'] + else: + raise Exception("Incorrect auth data, proxy must be specified") class VOMSIdentityPlugin(IdentityPlugin): @staticmethod def create_token(params): - return params['proxy'] + if 'proxy' in params: + return params['proxy'] + else: + raise Exception("Incorrect auth data, no proxy has been specified") class KeyStoneIdentityPlugin(IdentityPlugin): """ @@ -413,32 +424,35 @@ def create_token(params): """ Contact the specified keystone server to return the token """ - try: - keystone_uri = params['auth_url'] - uri = uriparse(keystone_uri) - server = uri[1].split(":")[0] - port = int(uri[1].split(":")[1]) - - conn = httplib.HTTPSConnection(server, port) - conn.putrequest('POST', "/v2.0/tokens") - conn.putheader('Accept', 'application/json') - conn.putheader('Content-Type', 'application/json') - conn.putheader('Connection', 'close') - - body = '{"auth":{"passwordCredentials":{"username": "' + params['username'] + '","password": "' + params['password'] + '"},"tenantName": "' + params['tenant'] + '"}}' - - conn.putheader('Content-Length', len(body)) - conn.endheaders(body) + if 'username' in params and 'password' in params and 'auth_url' in params and 'tenant' in params: + try: + keystone_uri = params['auth_url'] + uri = uriparse(keystone_uri) + server = uri[1].split(":")[0] + port = int(uri[1].split(":")[1]) - resp = conn.getresponse() - - # format: -> "{\"access\": {\"token\": {\"issued_at\": \"2014-12-29T17:10:49.609894\", \"expires\": \"2014-12-30T17:10:49Z\", \"id\": \"c861ab413e844d12a61d09b23dc4fb9c\"}, \"serviceCatalog\": [], \"user\": {\"username\": \"/DC=es/DC=irisgrid/O=upv/CN=miguel-caballer\", \"roles_links\": [], \"id\": \"475ce4978fb042e49ce0391de9bab49b\", \"roles\": [], \"name\": \"/DC=es/DC=irisgrid/O=upv/CN=miguel-caballer\"}, \"metadata\": {\"is_admin\": 0, \"roles\": []}}}" - output = json.loads(resp.read()) - token_id = output['access']['token']['id'] - - if conn.cert_file and os.path.isfile(conn.cert_file): - os.unlink(conn.cert_file) + conn = httplib.HTTPSConnection(server, port) + conn.putrequest('POST', "/v2.0/tokens") + conn.putheader('Accept', 'application/json') + conn.putheader('Content-Type', 'application/json') + conn.putheader('Connection', 'close') + + body = '{"auth":{"passwordCredentials":{"username": "' + params['username'] + '","password": "' + params['password'] + '"},"tenantName": "' + params['tenant'] + '"}}' + + conn.putheader('Content-Length', len(body)) + conn.endheaders(body) - return token_id - except: - return None \ No newline at end of file + resp = conn.getresponse() + + # format: -> "{\"access\": {\"token\": {\"issued_at\": \"2014-12-29T17:10:49.609894\", \"expires\": \"2014-12-30T17:10:49Z\", \"id\": \"c861ab413e844d12a61d09b23dc4fb9c\"}, \"serviceCatalog\": [], \"user\": {\"username\": \"/DC=es/DC=irisgrid/O=upv/CN=miguel-caballer\", \"roles_links\": [], \"id\": \"475ce4978fb042e49ce0391de9bab49b\", \"roles\": [], \"name\": \"/DC=es/DC=irisgrid/O=upv/CN=miguel-caballer\"}, \"metadata\": {\"is_admin\": 0, \"roles\": []}}}" + output = json.loads(resp.read()) + token_id = output['access']['token']['id'] + + if conn.cert_file and os.path.isfile(conn.cert_file): + os.unlink(conn.cert_file) + + return token_id + except: + return None + else: + raise Exception("Incorrect auth data, auth_url, username, password and tenant must be specified") \ No newline at end of file diff --git a/connectors/OpenNebula.py b/connectors/OpenNebula.py index 386003850..378b2d673 100644 --- a/connectors/OpenNebula.py +++ b/connectors/OpenNebula.py @@ -103,6 +103,10 @@ class OpenNebulaCloudConnector(CloudConnector): """str with the name of the provider.""" def __init__(self, cloud_info): + # check if the user has specified the http protocol in the host and remove it + pos = cloud_info.server.find('://') + if pos != -1: + cloud_info.server = cloud_info.server[pos+3:] CloudConnector.__init__(self, cloud_info) self.server_url = "http://%s:%d/RPC2" % (self.cloud.server, self.cloud.port) @@ -217,7 +221,7 @@ def updateVMInfo(self, vm, auth_data): session_id = self.getSessionID(auth_data) if session_id == None: - return (False, "Incorrect auth data") + return (False, "Incorrect auth data, username and password must be specified for OpenNebula provider.") func_res = server.one.vm.info(session_id, int(vm.id)) if len(func_res) == 2: @@ -272,7 +276,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): server = xmlrpclib.ServerProxy(self.server_url,allow_none=True) session_id = self.getSessionID(auth_data) if session_id == None: - return [(False, "Incorrect auth data")] + return [(False, "Incorrect auth data, username and password must be specified for OpenNebula provider.")] system = radl.systems[0] # Currently ONE plugin prioritizes user-password credentials @@ -305,7 +309,7 @@ def finalize(self, vm, auth_data): server = xmlrpclib.ServerProxy(self.server_url,allow_none=True) session_id = self.getSessionID(auth_data) if session_id == None: - return (False, "Incorrect auth data") + 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)) if len(func_res) == 1: @@ -324,7 +328,7 @@ def stop(self, vm, auth_data): server = xmlrpclib.ServerProxy(self.server_url,allow_none=True) session_id = self.getSessionID(auth_data) if session_id == None: - return (False, "Incorrect auth data") + return (False, "Incorrect auth data, username and password must be specified for OpenNebula provider.") func_res = server.one.vm.action(session_id, 'suspend', int(vm.id)) if len(func_res) == 1: @@ -343,7 +347,7 @@ def start(self, vm, auth_data): server = xmlrpclib.ServerProxy(self.server_url,allow_none=True) session_id = self.getSessionID(auth_data) if session_id == None: - return (False, "Incorrect auth data") + return (False, "Incorrect auth data, username and password must be specified for OpenNebula provider.") func_res = server.one.vm.action(session_id, 'resume', int(vm.id)) if len(func_res) == 1: @@ -729,7 +733,7 @@ def poweroff(self, vm, auth_data, timeout = 60): server = xmlrpclib.ServerProxy(self.server_url,allow_none=True) session_id = self.getSessionID(auth_data) if session_id == None: - return (False, "Incorrect auth data") + return (False, "Incorrect auth data, username and password must be specified for OpenNebula provider.") func_res = server.one.vm.action(session_id, 'poweroff', int(vm.id)) if len(func_res) == 1: @@ -771,7 +775,7 @@ def alterVM(self, vm, radl, auth_data): server = xmlrpclib.ServerProxy(self.server_url,allow_none=True) session_id = self.getSessionID(auth_data) if session_id == None: - return (False, "Incorrect auth data") + return (False, "Incorrect auth data, username and password must be specified for OpenNebula provider.") if self.checkResize(): if not radl.systems: diff --git a/connectors/OpenStack.py b/connectors/OpenStack.py index 14fcc5410..80e58d632 100644 --- a/connectors/OpenStack.py +++ b/connectors/OpenStack.py @@ -32,6 +32,13 @@ class OpenStackCloudConnector(LibCloudCloudConnector): type = "OpenStack" """str with the name of the provider.""" + def __init__(self, cloud_info): + # check if the user has specified the http protocol in the host and remove it + pos = cloud_info.server.find('://') + if pos != -1: + cloud_info.server = cloud_info.server[pos+3:] + LibCloudCloudConnector.__init__(self, cloud_info) + def get_driver(self, auth_data): """ Get the driver from the auth data diff --git a/doc/source/REST.rst b/doc/source/REST.rst index 5a8e751e7..9443a675a 100644 --- a/doc/source/REST.rst +++ b/doc/source/REST.rst @@ -53,7 +53,7 @@ Next tables summaries the resources and the HTTP methods available. +-------------+--------------------------------------------+---------------------------------------------+ GET ``http://imserver.com/infrastructures`` - :Content-type: text/uri-list + :Response Content-type: text/uri-list :ok response: 200 OK :fail response: 401, 400 @@ -62,23 +62,24 @@ GET ``http://imserver.com/infrastructures`` POST ``http://imserver.com/infrastructures`` :body: ``RADL document`` - :Content-type: text/uri-list + :body Content-type: text/plain or application/json + :Response Content-type: text/uri-list :ok response: 200 OK - :fail response: 401, 400 + :fail response: 401, 400, 415 Create and configure an infrastructure with the requirements specified in - the RADL document of the body contents. If success, it is returned the - URI of the new infrastructure. + 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. GET ``http://imserver.com/infrastructures/`` - :Content-type: text/uri-list + :Response Content-type: text/uri-list :ok response: 200 OK :fail response: 401, 404, 400 Return a list of URIs referencing the virtual machines associated to the infrastructure with ID ``infId``. GET ``http://imserver.com/infrastructures//`` - :Content-type: application/json + :Response Content-type: text/plain or application/json :ok response: 200 OK :fail response: 401, 404, 400, 403 @@ -92,67 +93,75 @@ GET ``http://imserver.com/infrastructures//`` POST ``http://imserver.com/infrastructures/`` :body: ``RADL document`` + :body Content-type: text/plain or application/json :input fields: ``context`` (optional) - :Content-type: text/uri-list + :Response Content-type: text/uri-list :ok response: 200 OK - :fail response: 401, 404, 400 + :fail response: 401, 404, 400, 415 - Add the resources specified in the body contents to the infrastructure with ID - ``infId``. The RADL restrictions are the same as in + 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 :ref:`RPC-XML AddResource `. 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. PUT ``http://imserver.com/infrastructures//stop`` - :Content-type: text/uri-list + :Response Content-type: text/plain :ok response: 200 OK :fail response: 401, 404, 400 Perform the ``stop`` action in all the virtual machines in the - the infrastructure with ID ``infID``: + 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`` - :Content-type: text/uri-list + :Response Content-type: text/plain :ok response: 200 OK :fail response: 401, 404, 400 Perform the ``start`` action in all the virtual machines in the - the infrastructure with ID ``infID``: + 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`` - :input fields: ``radl`` (compulsory), ``vm_list`` (optional) - :Content-type: text/uri-list + :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, 404, 400 + :fail response: 401, 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 ``radl``. The RADL restrictions - are the same as in :ref:`RPC-XML Reconfigure `. If no + of the infrastructure as indicated in the body contents (in plain RADL or in JSON formats). + The RADL restrictions are the same as in :ref:`RPC-XML Reconfigure `. If no RADL are specified, the contextualization process is stated again. - The last ``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. + 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 :ok response: 200 OK :fail response: 401, 404, 400 Undeploy the virtual machines associated to the infrastructure with ID - ``infId``. + ``infId``. If the operation has been performed successfully + the return value is an empty string. GET ``http://imserver.com/infrastructures//vms/`` - :Content-type: text/plain + :Response Content-type: text/plain or application/json :ok response: 200 OK :fail response: 401, 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. + 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 :ref:`GetVMInfo `. GET ``http://imserver.com/infrastructures//vms//`` - :Content-type: text/plain + :Response Content-type: text/plain :ok response: 200 OK :fail response: 401, 404, 400 @@ -161,35 +170,43 @@ GET ``http://imserver.com/infrastructures//vms//`` 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, 404, 400 + :fail response: 401, 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 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). DELETE ``http://imserver.com/infrastructures//vms/`` :input fields: ``context`` (optional) + :Response Content-type: text/plain :ok response: 200 OK :fail response: 401, 404, 400 Undeploy the virtual machine with ID ``vmId`` associated to the infrastructure with ID ``infId``. 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. + 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`` - :Content-type: text/plain + :Response Content-type: text/plain :ok response: 200 OK :fail response: 401, 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`` - :Content-type: text/plain + :Response Content-type: text/plain :ok response: 200 OK :fail response: 401, 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. diff --git a/doc/source/radl.rst b/doc/source/radl.rst index 6f5136e75..2c0518047 100644 --- a/doc/source/radl.rst +++ b/doc/source/radl.rst @@ -607,3 +607,64 @@ The next RADL deploys a single node that will be configured using Cloud-Init ins It depends on the Cloud provider to process correctly the cloud-init recipes of the configure section. In some cases (EGI FedCloud) it uses the cloud-init language (see `Cloud-Init documentation `_). In other cases as Amazon EC2 or OpenStack it must be a script to be executed in the instance. + +JSON Version +------------ + +There is a JSON version of the RADL language. It has the same semantics that the original RADL but +using JSON syntax to describe the objects. This is a complete example of the JSON format:: + + [ + { + "class": "ansible", + "id": "ansible_jost", + "credentials.username": "user", + "credentials.password": "pass", + "host": "server" + }, + { + "class": "network", + "id": "publica", + "outbound": "yes" + }, + { + "class": "system", + "cpu.arch": "x86_64", + "cpu.count_min": 1, + "disk.0.os.name": "linux", + "id": "front", + "memory.size_min": 536870912, + "net_interface.0.connection": "publica" + }, + { + "class": "configure", + "id": "front", + "recipes": "\\n---\\n- roles:\\n- { role: 'micafer.hadoop', hadoop_master: 'hadoopmaster', hadoop_type_of_node: 'master' }" + }, + { + "class": "deploy", + "system": "front", + "vm_number": 1, + "cloud": "cloud_id" + }, + { + "class": "contextualize", + "items": [ + { + "configure": "front", + "system": "front", + "ctxt_tool": "Ansible" + } + ] + } + ] + +The RADL JSON document is described as a list of objects. Each main object has a field named ``class`` that +described the type of RADL object (ansible, network, system, configure, contextualize or deploy). In case of +ansible, network, system and configure, the must also have and ``id`` field. Then the other fields correspond +to the features described in the RADL object. A particularity of the JSON format is that it does not uses +the comparators (``<=`` or ``>=``) so it is expressed using the ``_min`` and ``_max`` suffixes as show in the +example in ``cpu.count_min`` and ``memory.size_min``. Also the JSON format does not use units in the amount of +memory or disk size, so all these quantities are expresed in bytes. + +Currently this format is only supported in the REST API (not in the native XML-RPC one). \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index a1febc08f..855a6d3e0 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.4.1" +LABEL version="1.4.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 diff --git a/docker/ansible.cfg b/docker/ansible.cfg index 3cfba7837..2ae9bcc53 100644 --- a/docker/ansible.cfg +++ b/docker/ansible.cfg @@ -1,8 +1,8 @@ [defaults] transport = smart host_key_checking = False -sudo_user = root -sudo_exe = sudo +become_user = root +become_method = sudo [paramiko_connection] 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/TestRADL.py b/test/TestRADL.py index 240974c40..86f050abd 100755 --- a/test/TestRADL.py +++ b/test/TestRADL.py @@ -26,6 +26,7 @@ from IM.radl.radl_parse import parse_radl from IM.radl.radl import RADL, Features, Feature, RADLParseException, system +from IM.radl.radl_json import parse_radl as parse_radl_json, dump_radl as dump_radl_json import unittest class TestRADL(unittest.TestCase): @@ -49,6 +50,13 @@ def test_basic(self): self.assertIsInstance(s, system) self.assertEqual(len(s.features), 17) self.assertEqual(s.getValue("disk.0.os.name"), "linux") + + radl_json = dump_radl_json(r) + r = parse_radl_json(radl_json) + s = r.get_system_by_name("cursoaws") + self.assertIsInstance(s, system) + self.assertEqual(len(s.features), 17) + self.assertEqual(s.getValue("disk.0.os.name"), "linux") def test_basic0(self): @@ -57,11 +65,22 @@ def test_basic0(self): s = r.get_system_by_name("main") self.assertEqual(s.getValue("cpu.arch"), "x86_64") self.assertEqual(s.getValue("net_interface.0.connection"), "publica") + + radl_json = dump_radl_json(r) + r = parse_radl_json(radl_json) + self.radl_check(r, [2, 2, 0, 0, 0]) + s = r.get_system_by_name("main") + self.assertEqual(s.getValue("cpu.arch"), "x86_64") + self.assertEqual(s.getValue("net_interface.0.connection"), "publica") def test_references(self): r = parse_radl(TESTS_PATH + "/test_radl_ref.radl") self.radl_check(r, [2, 2, 0, 2, 2]) + + radl_json = dump_radl_json(r) + r = parse_radl_json(radl_json) + self.radl_check(r, [2, 2, 0, 2, 2]) def test_logic0(self): @@ -135,6 +154,15 @@ def test_concrete(self): self.assertIsInstance(concrete_s, system) self.assertEqual(score, 201) + radl_json = dump_radl_json(r) + r = parse_radl_json(radl_json) + self.radl_check(r) + s = r.get_system_by_name("main") + self.assertIsInstance(s, system) + concrete_s, score = s.concrete() + self.assertIsInstance(concrete_s, system) + self.assertEqual(score, 201) + def test_outports(self): @@ -194,6 +222,11 @@ def test_empty_contextualize(self): r.check() self.assertEqual(r.contextualize.items, {}) + radl_json = dump_radl_json(r) + r = parse_radl_json(radl_json) + r.check() + self.assertEqual(r.contextualize.items, {}) + radl = """ system test ( cpu.count>=1 @@ -204,6 +237,11 @@ def test_empty_contextualize(self): r = parse_radl(radl) r.check() self.assertEqual(r.contextualize.items, None) + + radl_json = dump_radl_json(r) + r = parse_radl_json(radl_json) + r.check() + self.assertEqual(r.contextualize.items, None) def test_ansible_host(self): @@ -217,6 +255,10 @@ def test_ansible_host(self): ) """ r = parse_radl(radl) self.radl_check(r) + + radl_json = dump_radl_json(r) + r = parse_radl_json(radl_json) + self.radl_check(r) radl = """ ansible ansible_master (host = 'host' and credentials.username = 'user' and credentials.password = 'pass') diff --git a/test/TestREST.py b/test/TestREST.py index ea06f8259..894406416 100755 --- a/test/TestREST.py +++ b/test/TestREST.py @@ -166,14 +166,14 @@ def test_32_get_vm_contmsg(self): resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR getting VM contmsg:" + output) - self.assertGreater(len(output), 100, msg="Incorrect VM contextualization message: " + output) + self.assertEqual(len(output), 0, msg="Incorrect VM contextualization message: " + output) def test_33_get_contmsg(self): self.server.request('GET', "/infrastructures/" + self.inf_id + "/contmsg", 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) - self.assertGreater(len(output), 100, msg="Incorrect contextualization message: " + output) + self.assertGreater(len(output), 30, msg="Incorrect contextualization message: " + output) def test_34_get_radl(self): self.server.request('GET', "/infrastructures/" + self.inf_id + "/radl", headers = {'AUTHORIZATION' : self.auth_data}) @@ -225,14 +225,8 @@ 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_addresource_noconfig(self): - self.server.request('POST', "/infrastructures/" + self.inf_id + "?context=0", body = RADL_ADD, headers = {'AUTHORIZATION' : self.auth_data}) - resp = self.server.getresponse() - output = str(resp.read()) - self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) - - def test_47_removeresource_noconfig(self): - self.server.request('GET', "/infrastructures/" + self.inf_id + "?context=0", headers = {'AUTHORIZATION' : self.auth_data}) + def test_46_removeresource(self): + 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) @@ -249,10 +243,19 @@ def test_47_removeresource_noconfig(self): 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), 2, msg="ERROR getting infrastructure info: Incorrect number of VMs(" + str(len(vm_ids)) + "). It must be 1") + 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_50_removeresource(self): - self.server.request('GET', "/infrastructures/" + self.inf_id, headers = {'AUTHORIZATION' : self.auth_data}) + 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): + self.server.request('POST', "/infrastructures/" + self.inf_id + "?context=0", body = RADL_ADD, headers = {'AUTHORIZATION' : self.auth_data}) + resp = self.server.getresponse() + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR adding resources:" + output) + + def test_50_removeresource_noconfig(self): + self.server.request('GET', "/infrastructures/" + self.inf_id + "?context=0", 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) @@ -271,9 +274,6 @@ def test_50_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_55_reconfigure(self): self.server.request('PUT', "/infrastructures/" + self.inf_id + "/reconfigure", headers = {'AUTHORIZATION' : self.auth_data}) resp = self.server.getresponse() @@ -293,10 +293,12 @@ def test_57_reconfigure_list(self): self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be configured (timeout).") def test_60_stop(self): + time.sleep(10) self.server.request('PUT', "/infrastructures/" + self.inf_id + "/stop", headers = {"Content-type": "application/x-www-form-urlencoded", 'AUTHORIZATION' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR stopping the infrastructure:" + output) + time.sleep(10) all_stopped = self.wait_inf_state(VirtualMachine.STOPPED, 120, [VirtualMachine.RUNNING]) self.assertTrue(all_stopped, msg="ERROR waiting the infrastructure to be stopped (timeout).") @@ -308,15 +310,18 @@ def test_70_start(self): resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR starting the infrastructure:" + output) + time.sleep(10) all_configured = self.wait_inf_state(VirtualMachine.CONFIGURED, 120, [VirtualMachine.RUNNING]) self.assertTrue(all_configured, msg="ERROR waiting the infrastructure to be started (timeout).") def test_80_stop_vm(self): + time.sleep(10) self.server.request('PUT', "/infrastructures/" + self.inf_id + "/vms/0/stop", headers = {"Content-type": "application/x-www-form-urlencoded", 'AUTHORIZATION' : self.auth_data}) resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR stopping the vm:" + output) + time.sleep(10) all_stopped = self.wait_inf_state(VirtualMachine.STOPPED, 120, [VirtualMachine.RUNNING], ["/infrastructures/" + self.inf_id + "/vms/0"]) self.assertTrue(all_stopped, msg="ERROR waiting the infrastructure to be stopped (timeout).") @@ -328,6 +333,7 @@ def test_90_start_vm(self): resp = self.server.getresponse() output = str(resp.read()) self.assertEqual(resp.status, 200, msg="ERROR starting the vm:" + output) + time.sleep(10) 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).") diff --git a/test/TestREST_JSON.py b/test/TestREST_JSON.py new file mode 100755 index 000000000..18478f116 --- /dev/null +++ b/test/TestREST_JSON.py @@ -0,0 +1,188 @@ +#! /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 httplib +import time +import sys +import json + +sys.path.append("..") +sys.path.append(".") + +from IM.VirtualMachine import VirtualMachine +from IM.uriparse import uriparse +from IM.radl.radl_json import parse_radl as parse_radl_json + +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 + '/test_simple.json' +AUTH_FILE = TESTS_PATH + '/auth.dat' + +HOSTNAME = "localhost" +TEST_PORT = 8800 + +class TestIM(unittest.TestCase): + + server = None + auth_data = None + inf_id = 0 + + @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.inf_id = "0" + + @classmethod + def tearDownClass(cls): + # Assure that the infrastructure is destroyed + try: + cls.server.request('DELETE', "/infrastructures/" + cls.inf_id, headers = {'Authorization' : cls.auth_data}) + cls.server.getresponse() + except Exception: + pass + + def wait_inf_state(self, state, timeout, incorrect_states = [], vm_ids = None): + """ + Wait for an infrastructure to have a specific state + """ + if not vm_ids: + 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 infrastructure info:" + output) + + vm_ids = output.split("\n") + else: + pass + + err_states = [VirtualMachine.FAILED, VirtualMachine.OFF, VirtualMachine.UNCONFIGURED] + err_states.extend(incorrect_states) + + wait = 0 + all_ok = False + while not all_ok and wait < timeout: + all_ok = True + for vm_id in vm_ids: + vm_uri = uriparse(vm_id) + self.server.request('GET', vm_uri[2] + "/state", headers = {'AUTHORIZATION' : self.auth_data}) + resp = self.server.getresponse() + vm_state = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting VM info:" + vm_state) + + 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, vm_uri)) + + if vm_state in err_states: + return False + elif vm_state != state: + all_ok = False + + if not all_ok: + wait += 5 + time.sleep(5) + + return all_ok + + def test_20_create(self): + f = open(RADL_FILE) + radl = "" + for line in f.readlines(): + radl += line + f.close() + + self.server.request('POST', "/infrastructures", body = radl, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'application/json'}) + 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_30_get_vm_info(self): + 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") + + vm_uri = uriparse(vm_ids[0]) + self.server.request('GET', vm_uri[2], headers = {'AUTHORIZATION' : self.auth_data, 'Accept':'application/json'}) + resp = self.server.getresponse() + ct = resp.getheader('Content-type') + output = str(resp.read()) + self.assertEqual(resp.status, 200, msg="ERROR getting VM info:" + output) + self.assertEqual(ct, "application/json", msg="ERROR getting VM info: Incorrect Content-type: %s" % ct) + parse_radl_json(output) + + def test_40_addresource(self): + self.server.request('POST', "/infrastructures/" + self.inf_id, body = RADL_ADD, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'application/json'}) + 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), 2, 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_55_reconfigure(self): + new_config = """ +[ +{ +"class": "configure", +"id": "front", +"recipes": "---\\n - tasks:\\n - debug: msg=RECONTEXTUALIZAMOS!\\n\\n" +} +] + """ + + self.server.request('PUT', "/infrastructures/" + self.inf_id + "/reconfigure", body = new_config, headers = {'AUTHORIZATION' : self.auth_data, 'Content-Type':'application/json'}) + resp = self.server.getresponse() + output = str(resp.read()) + 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).") + + self.server.request('GET', "/infrastructures/" + self.inf_id + "/contmsg", 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) + self.assertNotEqual(output.find("RECONTEXTUALIZAMOS"), -1, msg="Incorrect contextualization message: " + output) + + def test_95_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() diff --git a/test/test_simple.json b/test/test_simple.json new file mode 100644 index 000000000..e52cf6f6b --- /dev/null +++ b/test/test_simple.json @@ -0,0 +1,24 @@ +[ + { + "class": "network", + "id": "publica", + "outbound": "yes" + }, + { + "class": "system", + "cpu.arch": "x86_64", + "cpu.count_min": 1, + "disk.0.image.url": "one://ramses.i3m.upv.es/95", + "disk.0.os.credentials.password": "yoyoyo", + "disk.0.os.credentials.username": "ubuntu", + "disk.0.os.name": "linux", + "id": "front", + "memory.size_min": 536870912, + "net_interface.0.connection": "publica" + }, + { + "class": "deploy", + "system": "front", + "vm_number": 1 + } +] \ No newline at end of file diff --git a/test/test_simple.radl b/test/test_simple.radl index 2a5aef9e7..4b17b5f24 100644 --- a/test/test_simple.radl +++ b/test/test_simple.radl @@ -11,4 +11,6 @@ disk.0.os.credentials.password = 'yoyoyo' and disk.0.os.name = 'linux' ) +contextualize () + deploy front 1 \ No newline at end of file