| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | ## |
| 3 | # Copyright 2018 University of Bristol - High Performance Networks Research |
| 4 | # Group |
| 5 | # All Rights Reserved. |
| 6 | # |
| 7 | # Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique |
| 8 | # Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou |
| 9 | # |
| 10 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 11 | # not use this file except in compliance with the License. You may obtain |
| 12 | # a copy of the License at |
| 13 | # |
| 14 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 15 | # |
| 16 | # Unless required by applicable law or agreed to in writing, software |
| 17 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 18 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 19 | # License for the specific language governing permissions and limitations |
| 20 | # under the License. |
| 21 | # |
| 22 | # For those usages not covered by the Apache License, Version 2.0 please |
| 23 | # contact with: <highperformance-networks@bristol.ac.uk> |
| 24 | # |
| 25 | # Neither the name of the University of Bristol nor the names of its |
| 26 | # contributors may be used to endorse or promote products derived from |
| 27 | # this software without specific prior written permission. |
| 28 | # |
| 29 | # This work has been performed in the context of DCMS UK 5G Testbeds |
| 30 | # & Trials Programme and in the framework of the Metro-Haul project - |
| 31 | # funded by the European Commission under Grant number 761727 through the |
| 32 | # Horizon 2020 and 5G-PPP programmes. |
| 33 | ## |
| 34 | |
| 35 | """This module works as an extension to the toplevel ``httpserver`` module, |
| 36 | implementing callbacks for the HTTP routes related to the WIM features of OSM. |
| 37 | |
| 38 | Acting as a front-end, it is responsible for converting the HTTP request |
| 39 | payload into native python objects, calling the correct engine methods |
| 40 | and converting back the response objects into strings to be send in the HTTP |
| 41 | response payload. |
| 42 | |
| 43 | Direct domain/persistence logic should be avoided in this file, instead |
| 44 | calls to other layers should be done. |
| 45 | """ |
| 46 | import logging |
| 47 | |
| 48 | from bottle import request |
| 49 | |
| 50 | from .. import utils |
| 51 | from ..http_tools.errors import ErrorHandler |
| 52 | from ..http_tools.handler import BaseHandler, route |
| 53 | from ..http_tools.request_processing import ( |
| 54 | filter_query_string, |
| 55 | format_in, |
| 56 | format_out |
| 57 | ) |
| 58 | from .engine import WimEngine |
| 59 | from .persistence import WimPersistence |
| 60 | from .schemas import ( |
| 61 | wim_account_schema, |
| 62 | wim_edit_schema, |
| 63 | wim_port_mapping_schema, |
| 64 | wim_schema |
| 65 | ) |
| 66 | |
| 67 | |
| 68 | class WimHandler(BaseHandler): |
| 69 | """HTTP route implementations for WIM related URLs |
| 70 | |
| 71 | Arguments: |
| 72 | db: instance of mydb [optional]. This argument must be provided |
| 73 | if not ``persistence`` is passed |
| 74 | persistence (WimPersistence): High-level data storage abstraction |
| 75 | [optional]. If this argument is not present, ``db`` must be. |
| 76 | engine (WimEngine): Implementation of the business logic |
| 77 | for the engine of WAN networks |
| 78 | logger (logging.Logger): logger object [optional] |
| 79 | url_base(str): Path fragment to be prepended to the routes [optional] |
| 80 | plugins(list): List of bottle plugins to be applied to routes |
| 81 | [optional] |
| 82 | """ |
| 83 | def __init__(self, db=None, persistence=None, engine=None, |
| 84 | url_base='', logger=None, plugins=()): |
| 85 | self.persist = persistence or WimPersistence(db) |
| 86 | self.engine = engine or WimEngine(self.persist) |
| 87 | self.url_base = url_base |
| 88 | self.logger = logger or logging.getLogger('openmano.wim.http') |
| 89 | error_handler = ErrorHandler(self.logger) |
| 90 | self.plugins = [error_handler] + list(plugins) |
| 91 | |
| 92 | @route('GET', '/<tenant_id>/wims') |
| 93 | def http_list_wims(self, tenant_id): |
| 94 | allowed_fields = ('uuid', 'name', 'wim_url', 'type', 'created_at') |
| 95 | select_, where_, limit_ = filter_query_string( |
| 96 | request.query, None, allowed_fields) |
| 97 | # ^ Since we allow the user to customize the db query using the HTTP |
| 98 | # query and it is quite difficult to re-use this query, let's just |
| 99 | # do a ad-hoc call to the db |
| 100 | |
| 101 | from_ = 'wims' |
| 102 | if tenant_id != 'any': |
| 103 | where_['nfvo_tenant_id'] = tenant_id |
| 104 | if 'created_at' in select_: |
| 105 | select_[select_.index('created_at')] = ( |
| 106 | 'w.created_at as created_at') |
| 107 | if 'created_at' in where_: |
| 108 | where_['w.created_at'] = where_.pop('created_at') |
| 109 | from_ = ('wims as w join wim_nfvo_tenants as wt ' |
| 110 | 'on w.uuid=wt.wim_id') |
| 111 | |
| 112 | wims = self.persist.query( |
| 113 | FROM=from_, SELECT=select_, WHERE=where_, LIMIT=limit_, |
| 114 | error_if_none=False) |
| 115 | |
| 116 | utils.convert_float_timestamp2str(wims) |
| 117 | return format_out({'wims': wims}) |
| 118 | |
| 119 | @route('GET', '/<tenant_id>/wims/<wim_id>') |
| 120 | def http_get_wim(self, tenant_id, wim_id): |
| 121 | tenant_id = None if tenant_id == 'any' else tenant_id |
| 122 | wim = self.engine.get_wim(wim_id, tenant_id) |
| Anderson Bravalheri | fed47b0 | 2018-12-16 20:44:08 +0000 | [diff] [blame^] | 123 | mappings = self.engine.get_wim_port_mappings(wim_id) |
| 124 | wim['config'] = utils.merge_dicts(wim.get('config', {}) or {}, |
| 125 | wim_port_mapping=mappings) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 126 | return format_out({'wim': wim}) |
| 127 | |
| 128 | @route('POST', '/wims') |
| 129 | def http_create_wim(self): |
| 130 | http_content, _ = format_in(wim_schema, confidential_data=True) |
| 131 | r = utils.remove_extra_items(http_content, wim_schema) |
| 132 | if r: |
| 133 | self.logger.debug("Remove extra items received %r", r) |
| 134 | data = self.engine.create_wim(http_content['wim']) |
| 135 | return self.http_get_wim('any', data) |
| 136 | |
| 137 | @route('PUT', '/wims/<wim_id>') |
| 138 | def http_update_wim(self, wim_id): |
| 139 | '''edit wim details, can use both uuid or name''' |
| 140 | # parse input data |
| 141 | http_content, _ = format_in(wim_edit_schema) |
| 142 | r = utils.remove_extra_items(http_content, wim_edit_schema) |
| 143 | if r: |
| 144 | self.logger.debug("Remove received extra items %s", r) |
| 145 | |
| 146 | wim_id = self.engine.update_wim(wim_id, http_content['wim']) |
| 147 | return self.http_get_wim('any', wim_id) |
| 148 | |
| 149 | @route('DELETE', '/wims/<wim_id>') |
| 150 | def http_delete_wim(self, wim_id): |
| 151 | """Delete a wim from a database, can use both uuid or name""" |
| 152 | data = self.engine.delete_wim(wim_id) |
| 153 | # TODO Remove WIM in orchestrator |
| 154 | return format_out({"result": "wim '" + data + "' deleted"}) |
| 155 | |
| 156 | @route('POST', '/<tenant_id>/wims/<wim_id>') |
| 157 | def http_create_wim_account(self, tenant_id, wim_id): |
| 158 | """Associate an existing wim to this tenant""" |
| 159 | # parse input data |
| 160 | http_content, _ = format_in( |
| 161 | wim_account_schema, confidential_data=True) |
| 162 | removed = utils.remove_extra_items(http_content, wim_account_schema) |
| 163 | removed and self.logger.debug("Remove extra items %r", removed) |
| 164 | account = self.engine.create_wim_account( |
| 165 | wim_id, tenant_id, http_content['wim_account']) |
| 166 | # check update succeeded |
| 167 | return format_out({"wim_account": account}) |
| 168 | |
| 169 | @route('PUT', '/<tenant_id>/wims/<wim_id>') |
| 170 | def http_update_wim_accounts(self, tenant_id, wim_id): |
| 171 | """Edit the association of an existing wim to this tenant""" |
| 172 | tenant_id = None if tenant_id == 'any' else tenant_id |
| 173 | # parse input data |
| 174 | http_content, _ = format_in( |
| 175 | wim_account_schema, confidential_data=True) |
| 176 | removed = utils.remove_extra_items(http_content, wim_account_schema) |
| 177 | removed and self.logger.debug("Remove extra items %r", removed) |
| 178 | accounts = self.engine.update_wim_accounts( |
| 179 | wim_id, tenant_id, http_content['wim_account']) |
| 180 | |
| 181 | if tenant_id: |
| 182 | return format_out({'wim_account': accounts[0]}) |
| 183 | |
| 184 | return format_out({'wim_accounts': accounts}) |
| 185 | |
| 186 | @route('DELETE', '/<tenant_id>/wims/<wim_id>') |
| 187 | def http_delete_wim_accounts(self, tenant_id, wim_id): |
| 188 | """Deassociate an existing wim to this tenant""" |
| 189 | tenant_id = None if tenant_id == 'any' else tenant_id |
| 190 | accounts = self.engine.delete_wim_accounts(wim_id, tenant_id, |
| 191 | error_if_none=True) |
| 192 | |
| 193 | properties = ( |
| 194 | (account['name'], wim_id, |
| 195 | utils.safe_get(account, 'association.nfvo_tenant_id', tenant_id)) |
| 196 | for account in accounts) |
| 197 | |
| 198 | return format_out({ |
| 199 | 'result': '\n'.join('WIM account `{}` deleted. ' |
| 200 | 'Tenant `{}` detached from WIM `{}`' |
| 201 | .format(*p) for p in properties) |
| 202 | }) |
| 203 | |
| 204 | @route('POST', '/<tenant_id>/wims/<wim_id>/port_mapping') |
| 205 | def http_create_wim_port_mappings(self, tenant_id, wim_id): |
| 206 | """Set the wim port mapping for a wim""" |
| 207 | # parse input data |
| 208 | http_content, _ = format_in(wim_port_mapping_schema) |
| 209 | |
| 210 | data = self.engine.create_wim_port_mappings( |
| 211 | wim_id, http_content['wim_port_mapping'], tenant_id) |
| 212 | return format_out({"wim_port_mapping": data}) |
| 213 | |
| 214 | @route('GET', '/<tenant_id>/wims/<wim_id>/port_mapping') |
| 215 | def http_get_wim_port_mappings(self, tenant_id, wim_id): |
| 216 | """Get wim port mapping details""" |
| 217 | # TODO: tenant_id is never used, so it should be removed |
| 218 | data = self.engine.get_wim_port_mappings(wim_id) |
| 219 | return format_out({"wim_port_mapping": data}) |
| 220 | |
| 221 | @route('DELETE', '/<tenant_id>/wims/<wim_id>/port_mapping') |
| 222 | def http_delete_wim_port_mappings(self, tenant_id, wim_id): |
| 223 | """Clean wim port mapping""" |
| 224 | # TODO: tenant_id is never used, so it should be removed |
| 225 | data = self.engine.delete_wim_port_mappings(wim_id) |
| 226 | return format_out({"result": data}) |