fe12f8b3d7c3fb506fb49f5136933f6f62a8b4bf
[osm/LCM.git] / osm_lcm / ROclient.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3
4 ##
5 # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U.
6 # This file is part of openmano
7 # All Rights Reserved.
8 #
9 # Licensed under the Apache License, Version 2.0 (the "License"); you may
10 # not use this file except in compliance with the License. You may obtain
11 # a copy of the License at
12 #
13 # http://www.apache.org/licenses/LICENSE-2.0
14 #
15 # Unless required by applicable law or agreed to in writing, software
16 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18 # License for the specific language governing permissions and limitations
19 # under the License.
20 #
21 # For those usages not covered by the Apache License, Version 2.0 please
22 # contact with: nfvlabs@tid.es
23 ##
24
25 """
26 asyncio RO python client to interact with RO-server
27 """
28
29 import asyncio
30 import aiohttp
31 import json
32 import yaml
33 import logging
34 from urllib.parse import quote
35 from uuid import UUID
36 from copy import deepcopy
37
38 __author__ = "Alfonso Tierno"
39 __date__ = "$09-Jan-2018 09:09:48$"
40 __version__ = "0.1.2"
41 version_date = "2018-05-16"
42 requests = None
43
44
45 class ROClientException(Exception):
46 def __init__(self, message, http_code=400):
47 """Common Exception for all RO client exceptions"""
48 self.http_code = http_code
49 Exception.__init__(self, message)
50
51
52 def remove_envelop(item, indata=None):
53 """
54 Obtain the useful data removing the envelop. It goes through the vnfd or nsd catalog and returns the
55 vnfd or nsd content
56 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
57 :param indata: Content to be inspected
58 :return: the useful part of indata (a reference, not a new dictionay)
59 """
60 clean_indata = indata
61 if not indata:
62 return {}
63 if item == "vnfd":
64 if clean_indata.get('vnfd:vnfd-catalog'):
65 clean_indata = clean_indata['vnfd:vnfd-catalog']
66 elif clean_indata.get('vnfd-catalog'):
67 clean_indata = clean_indata['vnfd-catalog']
68 if clean_indata.get('vnfd'):
69 if not isinstance(clean_indata['vnfd'], list) or len(clean_indata['vnfd']) != 1:
70 raise ROClientException("'vnfd' must be a list only one element")
71 clean_indata = clean_indata['vnfd'][0]
72 elif item == "nsd":
73 if clean_indata.get('nsd:nsd-catalog'):
74 clean_indata = clean_indata['nsd:nsd-catalog']
75 elif clean_indata.get('nsd-catalog'):
76 clean_indata = clean_indata['nsd-catalog']
77 if clean_indata.get('nsd'):
78 if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
79 raise ROClientException("'nsd' must be a list only one element")
80 clean_indata = clean_indata['nsd'][0]
81 elif item == "sdn":
82 if len(indata) == 1 and "sdn_controller" in indata:
83 clean_indata = indata["sdn_controller"]
84 elif item == "tenant":
85 if len(indata) == 1 and "tenant" in indata:
86 clean_indata = indata["tenant"]
87 elif item in ("vim", "vim_account", "datacenters"):
88 if len(indata) == 1 and "datacenter" in indata:
89 clean_indata = indata["datacenter"]
90 elif item == "ns" or item == "instances":
91 if len(indata) == 1 and "instance" in indata:
92 clean_indata = indata["instance"]
93 else:
94 assert False, "remove_envelop with unknown item {}".format(item)
95
96 return clean_indata
97
98
99 class ROClient:
100 headers_req = {'Accept': 'application/yaml', 'content-type': 'application/yaml'}
101 client_to_RO = {'tenant': 'tenants', 'vim': 'datacenters', 'vim_account': 'datacenters', 'sdn': 'sdn_controllers',
102 'vnfd': 'vnfs', 'nsd': 'scenarios',
103 'ns': 'instances'}
104 mandatory_for_create = {
105 'tenant': ("name", ),
106 'vnfd': ("name", "id", "connection-point", "vdu"),
107 'nsd': ("name", "id", "constituent-vnfd"),
108 'ns': ("name", "scenario", "datacenter"),
109 'vim': ("name", "vim_url"),
110 'vim_account': (),
111 'sdn': ("name", "port", 'ip', 'dpid', 'type'),
112 }
113 timeout_large = 120
114 timeout_short = 30
115
116 def __init__(self, loop, endpoint_url, **kwargs):
117 self.loop = loop
118 self.endpoint_url = endpoint_url
119
120 self.username = kwargs.get("username")
121 self.password = kwargs.get("password")
122 self.tenant_id_name = kwargs.get("tenant")
123 self.tenant = None
124 self.datacenter_id_name = kwargs.get("datacenter")
125 self.datacenter = None
126 logger_name = kwargs.get('logger_name', 'ROClient')
127 self.logger = logging.getLogger(logger_name)
128 if kwargs.get("loglevel"):
129 self.logger.setLevel(kwargs["loglevel"])
130 global requests
131 requests = kwargs.get("TODO remove")
132
133 def __getitem__(self, index):
134 if index == 'tenant':
135 return self.tenant_id_name
136 elif index == 'datacenter':
137 return self.datacenter_id_name
138 elif index == 'username':
139 return self.username
140 elif index == 'password':
141 return self.password
142 elif index == 'endpoint_url':
143 return self.endpoint_url
144 else:
145 raise KeyError("Invalid key '{}'".format(index))
146
147 def __setitem__(self, index, value):
148 if index == 'tenant':
149 self.tenant_id_name = value
150 elif index == 'datacenter' or index == 'vim':
151 self.datacenter_id_name = value
152 elif index == 'username':
153 self.username = value
154 elif index == 'password':
155 self.password = value
156 elif index == 'endpoint_url':
157 self.endpoint_url = value
158 else:
159 raise KeyError("Invalid key '{}'".format(index))
160 self.tenant = None # force to reload tenant with different credentials
161 self.datacenter = None # force to reload datacenter with different credentials
162
163 def _parse(self, descriptor, descriptor_format, response=False):
164 if descriptor_format and descriptor_format != "json" and descriptor_format != "yaml":
165 raise ROClientException("'descriptor_format' must be a 'json' or 'yaml' text")
166 if descriptor_format != "json":
167 try:
168 return yaml.load(descriptor)
169 except yaml.YAMLError as exc:
170 error_pos = ""
171 if hasattr(exc, 'problem_mark'):
172 mark = exc.problem_mark
173 error_pos = " at line:{} column:{}s".format(mark.line + 1, mark.column + 1)
174 error_text = "yaml format error" + error_pos
175 elif descriptor_format != "yaml":
176 try:
177 return json.loads(descriptor)
178 except Exception as e:
179 if response:
180 error_text = "json format error" + str(e)
181
182 if response:
183 raise ROClientException(error_text)
184 raise ROClientException(error_text)
185
186 def _parse_yaml(self, descriptor, response=False):
187 try:
188 return yaml.load(descriptor)
189 except yaml.YAMLError as exc:
190 error_pos = ""
191 if hasattr(exc, 'problem_mark'):
192 mark = exc.problem_mark
193 error_pos = " at line:{} column:{}s".format(mark.line + 1, mark.column + 1)
194 error_text = "yaml format error" + error_pos
195 if response:
196 raise ROClientException(error_text)
197 raise ROClientException(error_text)
198
199 @staticmethod
200 def check_if_uuid(uuid_text):
201 """
202 Check if text correspond to an uuid foramt
203 :param uuid_text:
204 :return: True if it is an uuid False if not
205 """
206 try:
207 UUID(uuid_text)
208 return True
209 except (ValueError, TypeError):
210 return False
211
212 @staticmethod
213 def _create_envelop(item, indata=None):
214 """
215 Returns a new dict that incledes indata with the expected envelop
216 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
217 :param indata: Content to be enveloped
218 :return: a new dic with {<envelop>: {indata} } where envelop can be e.g. tenant, datacenter, ...
219 """
220 if item == "vnfd":
221 return {'vnfd-catalog': {'vnfd': [indata]}}
222 elif item == "nsd":
223 return {'nsd-catalog': {'nsd': [indata]}}
224 elif item == "tenant":
225 return {'tenant': indata}
226 elif item in ("vim", "vim_account", "datacenter"):
227 return {'datacenter': indata}
228 elif item == "ns" or item == "instances":
229 return {'instance': indata}
230 elif item == "sdn":
231 return {'sdn_controller': indata}
232 else:
233 assert False, "_create_envelop with unknown item {}".format(item)
234
235 @staticmethod
236 def update_descriptor(desc, kwargs):
237 desc = deepcopy(desc) # do not modify original descriptor
238 try:
239 for k, v in kwargs.items():
240 update_content = desc
241 kitem_old = None
242 klist = k.split(".")
243 for kitem in klist:
244 if kitem_old is not None:
245 update_content = update_content[kitem_old]
246 if isinstance(update_content, dict):
247 kitem_old = kitem
248 elif isinstance(update_content, list):
249 kitem_old = int(kitem)
250 else:
251 raise ROClientException(
252 "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(k, kitem))
253 if v == "__DELETE__":
254 del update_content[kitem_old]
255 else:
256 update_content[kitem_old] = v
257 return desc
258 except KeyError:
259 raise ROClientException(
260 "Invalid query string '{}'. Descriptor does not contain '{}'".format(k, kitem_old))
261 except ValueError:
262 raise ROClientException("Invalid query string '{}'. Expected integer index list instead of '{}'".format(
263 k, kitem))
264 except IndexError:
265 raise ROClientException(
266 "Invalid query string '{}'. Index '{}' out of range".format(k, kitem_old))
267
268 @staticmethod
269 def check_ns_status(ns_descriptor):
270 """
271 Inspect RO instance descriptor and indicates the status
272 :param ns_descriptor: instance descriptor obtained with self.show("ns", )
273 :return: status, message: status can be BUILD,ACTIVE,ERROR, message is a text message
274 """
275 net_total = 0
276 vm_total = 0
277 net_done = 0
278 vm_done = 0
279
280 for net in ns_descriptor["nets"]:
281 net_total += 1
282 if net["status"] in ("ERROR", "VIM_ERROR"):
283 return "ERROR", net["error_msg"]
284 elif net["status"] == "ACTIVE":
285 net_done += 1
286 for vnf in ns_descriptor["vnfs"]:
287 for vm in vnf["vms"]:
288 vm_total += 1
289 if vm["status"] in ("ERROR", "VIM_ERROR"):
290 return "ERROR", vm["error_msg"]
291 elif vm["status"] == "ACTIVE":
292 vm_done += 1
293
294 if net_total == net_done and vm_total == vm_done:
295 return "ACTIVE", "VMs {}, networks: {}".format(vm_total, net_total)
296 else:
297 return "BUILD", "VMs: {}/{}, networks: {}/{}".format(vm_done, vm_total, net_done, net_total)
298
299 @staticmethod
300 def check_action_status(action_descriptor):
301 """
302 Inspect RO instance descriptor and indicates the status
303 :param action_descriptor: action instance descriptor obtained with self.show("ns", "action")
304 :return: status, message: status can be BUILD,ACTIVE,ERROR, message is a text message
305 """
306 net_total = 0
307 vm_total = 0
308 net_done = 0
309 vm_done = 0
310 other_total = 0
311 other_done = 0
312
313 for vim_action_set in action_descriptor["actions"]:
314 for vim_action in vim_action_set["vim_actions"]:
315 if vim_action["item"] == "instance_vms":
316 vm_total += 1
317 elif vim_action["item"] == "instance_nets":
318 net_total += 1
319 else:
320 other_total += 1
321 if vim_action["status"] == "FAILED":
322 return "ERROR", vim_action["error_msg"]
323 elif vim_action["status"] in ("DONE", "SUPERSEDED"):
324 if vim_action["item"] == "instance_vms":
325 vm_done += 1
326 elif vim_action["item"] == "instance_nets":
327 net_done += 1
328 else:
329 other_done += 1
330
331 if net_total == net_done and vm_total == vm_done and other_total == other_done:
332 return "ACTIVE", "VMs {}, networks: {}, other: {} ".format(vm_total, net_total, other_total)
333 else:
334 return "BUILD", "VMs: {}/{}, networks: {}/{}, other: {}/{}".format(vm_done, vm_total, net_done, net_total,
335 other_done, other_total)
336
337 @staticmethod
338 def get_ns_vnf_info(ns_descriptor):
339 """
340 Get a dict with the VIM_id, ip_addresses, mac_addresses of every vnf and vdu
341 :param ns_descriptor: instance descriptor obtained with self.show("ns", )
342 :return: dict with:
343 <member_vnf_index>:
344 ip_address: XXXX,
345 vdur:
346 <vdu_osm_id>:
347 ip_address: XXX
348 vim_id: XXXX
349 interfaces:
350 <name>:
351 ip_address: XXX
352 mac_address: XXX
353 """
354 ns_info = {}
355 for vnf in ns_descriptor["vnfs"]:
356 if not vnf.get("ip_address"):
357 raise ROClientException("ns member_vnf_index '{}' has no IP address".format(
358 vnf["member_vnf_index"]), http_code=409)
359 vnfr_info = {
360 "ip_address": vnf.get("ip_address"),
361 "vdur": {}
362 }
363 for vm in vnf["vms"]:
364 vdur = {
365 "vim_id": vm.get("vim_vm_id"),
366 "ip_address": vm.get("ip_address"),
367 "interfaces": {}
368 }
369 for iface in vm["interfaces"]:
370 if iface.get("type") == "mgmt" and not iface.get("ip_address"):
371 raise ROClientException("ns member_vnf_index '{}' vm '{}' management interface '{}' has no IP "
372 "address".format(vnf["member_vnf_index"], vm["vdu_osm_id"],
373 iface["external_name"]), http_code=409)
374 vdur["interfaces"][iface["internal_name"]] = {"ip_address": iface.get("ip_address"),
375 "mac_address": iface.get("mac_address"),
376 "vim_id": iface.get("vim_interface_id"),
377 }
378 vnfr_info["vdur"][vm["vdu_osm_id"]] = vdur
379 ns_info[str(vnf["member_vnf_index"])] = vnfr_info
380 return ns_info
381
382 async def _get_item_uuid(self, session, item, item_id_name, all_tenants=False):
383 if all_tenants:
384 tenant_text = "/any"
385 elif all_tenants is None:
386 tenant_text = ""
387 else:
388 if not self.tenant:
389 await self._get_tenant(session)
390 tenant_text = "/" + self.tenant
391
392 item_id = 0
393 url = "{}{}/{}".format(self.endpoint_url, tenant_text, item)
394 if self.check_if_uuid(item_id_name):
395 item_id = item_id_name
396 url += "/" + item_id_name
397 elif item_id_name and item_id_name.startswith("'") and item_id_name.endswith("'"):
398 item_id_name = item_id_name[1:-1]
399 self.logger.debug("RO GET %s", url)
400 with aiohttp.Timeout(self.timeout_short):
401 async with session.get(url, headers=self.headers_req) as response:
402 response_text = await response.read()
403 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
404 if response.status == 404: # NOT_FOUND
405 raise ROClientException("No {} found with id '{}'".format(item[:-1], item_id_name),
406 http_code=404)
407 if response.status >= 300:
408 raise ROClientException(response_text, http_code=response.status)
409 content = self._parse_yaml(response_text, response=True)
410
411 if item_id:
412 return item_id
413 desc = content[item]
414 assert isinstance(desc, list), "_get_item_uuid get a non dict with a list inside {}".format(type(desc))
415 uuid = None
416 for i in desc:
417 if item_id_name and i["name"] != item_id_name:
418 continue
419 if uuid: # found more than one
420 raise ROClientException(
421 "Found more than one {} with name '{}'. uuid must be used".format(item, item_id_name),
422 http_code=404)
423 uuid = i["uuid"]
424 if not uuid:
425 raise ROClientException("No {} found with name '{}'".format(item[:-1], item_id_name), http_code=404)
426 return uuid
427
428 async def _get_item(self, session, item, item_id_name, extra_item=None, extra_item_id=None, all_tenants=False):
429 if all_tenants:
430 tenant_text = "/any"
431 elif all_tenants is None:
432 tenant_text = ""
433 else:
434 if not self.tenant:
435 await self._get_tenant(session)
436 tenant_text = "/" + self.tenant
437
438 if self.check_if_uuid(item_id_name):
439 uuid = item_id_name
440 else:
441 # check that exist
442 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants)
443
444 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, uuid)
445 if extra_item:
446 url += "/" + extra_item
447 if extra_item_id:
448 url += "/" + extra_item_id
449 self.logger.debug("GET %s", url)
450 with aiohttp.Timeout(self.timeout_short):
451 async with session.get(url, headers=self.headers_req) as response:
452 response_text = await response.read()
453 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
454 if response.status >= 300:
455 raise ROClientException(response_text, http_code=response.status)
456
457 return self._parse_yaml(response_text, response=True)
458
459 async def _get_tenant(self, session):
460 if not self.tenant:
461 self.tenant = await self._get_item_uuid(session, "tenants", self.tenant_id_name, None)
462 return self.tenant
463
464 async def _get_datacenter(self, session):
465 if not self.tenant:
466 await self._get_tenant(session)
467 if not self.datacenter:
468 self.datacenter = await self._get_item_uuid(session, "datacenters", self.datacenter_id_name, True)
469 return self.datacenter
470
471 async def _create_item(self, session, item, descriptor, item_id_name=None, action=None, all_tenants=False):
472 if all_tenants:
473 tenant_text = "/any"
474 elif all_tenants is None:
475 tenant_text = ""
476 else:
477 if not self.tenant:
478 await self._get_tenant(session)
479 tenant_text = "/" + self.tenant
480 payload_req = yaml.safe_dump(descriptor)
481 # print payload_req
482
483 api_version_text = ""
484 if item == "vnfs":
485 # assumes version v3 only
486 api_version_text = "/v3"
487 item = "vnfd"
488 elif item == "scenarios":
489 # assumes version v3 only
490 api_version_text = "/v3"
491 item = "nsd"
492
493 if not item_id_name:
494 uuid = ""
495 elif self.check_if_uuid(item_id_name):
496 uuid = "/{}".format(item_id_name)
497 else:
498 # check that exist
499 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants)
500 uuid = "/{}".format(uuid)
501 if not action:
502 action = ""
503 else:
504 action = "/{}".format(action)
505
506 url = "{}{apiver}{tenant}/{item}{id}{action}".format(self.endpoint_url, apiver=api_version_text,
507 tenant=tenant_text, item=item, id=uuid, action=action)
508 self.logger.debug("RO POST %s %s", url, payload_req)
509 with aiohttp.Timeout(self.timeout_large):
510 async with session.post(url, headers=self.headers_req, data=payload_req) as response:
511 response_text = await response.read()
512 self.logger.debug("POST {} [{}] {}".format(url, response.status, response_text[:100]))
513 if response.status >= 300:
514 raise ROClientException(response_text, http_code=response.status)
515
516 return self._parse_yaml(response_text, response=True)
517
518 async def _del_item(self, session, item, item_id_name, all_tenants=False):
519 if all_tenants:
520 tenant_text = "/any"
521 elif all_tenants is None:
522 tenant_text = ""
523 else:
524 if not self.tenant:
525 await self._get_tenant(session)
526 tenant_text = "/" + self.tenant
527 if not self.check_if_uuid(item_id_name):
528 # check that exist
529 _all_tenants = all_tenants
530 if item == "datacenters":
531 _all_tenants = True
532 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants=_all_tenants)
533 else:
534 uuid = item_id_name
535
536 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, uuid)
537 self.logger.debug("DELETE %s", url)
538 with aiohttp.Timeout(self.timeout_short):
539 async with session.delete(url, headers=self.headers_req) as response:
540 response_text = await response.read()
541 self.logger.debug("DELETE {} [{}] {}".format(url, response.status, response_text[:100]))
542 if response.status >= 300:
543 raise ROClientException(response_text, http_code=response.status)
544 return self._parse_yaml(response_text, response=True)
545
546 async def _list_item(self, session, item, all_tenants=False, filter_dict=None):
547 if all_tenants:
548 tenant_text = "/any"
549 elif all_tenants is None:
550 tenant_text = ""
551 else:
552 if not self.tenant:
553 await self._get_tenant(session)
554 tenant_text = "/" + self.tenant
555
556 url = "{}{}/{}".format(self.endpoint_url, tenant_text, item)
557 separator = "?"
558 if filter_dict:
559 for k in filter_dict:
560 url += separator + quote(str(k)) + "=" + quote(str(filter_dict[k]))
561 separator = "&"
562 self.logger.debug("RO GET %s", url)
563 with aiohttp.Timeout(self.timeout_short):
564 async with session.get(url, headers=self.headers_req) as response:
565 response_text = await response.read()
566 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
567 if response.status >= 300:
568 raise ROClientException(response_text, http_code=response.status)
569 return self._parse_yaml(response_text, response=True)
570
571 async def _edit_item(self, session, item, item_id, descriptor, all_tenants=False):
572 if all_tenants:
573 tenant_text = "/any"
574 elif all_tenants is None:
575 tenant_text = ""
576 else:
577 if not self.tenant:
578 await self._get_tenant(session)
579 tenant_text = "/" + self.tenant
580
581 payload_req = yaml.safe_dump(descriptor)
582
583 # print payload_req
584 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, item_id)
585 self.logger.debug("RO PUT %s %s", url, payload_req)
586 with aiohttp.Timeout(self.timeout_large):
587 async with session.put(url, headers=self.headers_req, data=payload_req) as response:
588 response_text = await response.read()
589 self.logger.debug("PUT {} [{}] {}".format(url, response.status, response_text[:100]))
590 if response.status >= 300:
591 raise ROClientException(response_text, http_code=response.status)
592 return self._parse_yaml(response_text, response=True)
593
594 async def get_version(self):
595 """
596 Obtain RO server version.
597 :return: a list with integers ["major", "minor", "release"]. Raises ROClientException on Error,
598 """
599 try:
600 with aiohttp.ClientSession(loop=self.loop) as session:
601 url = "{}/version".format(self.endpoint_url)
602 self.logger.debug("RO GET %s", url)
603 with aiohttp.Timeout(self.timeout_short):
604 async with session.get(url, headers=self.headers_req) as response:
605 response_text = await response.read()
606 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
607 if response.status >= 300:
608 raise ROClientException(response_text, http_code=response.status)
609 for word in str(response_text).split(" "):
610 if "." in word:
611 version_text, _, _ = word.partition("-")
612 return list(map(int, version_text.split(".")))
613 raise ROClientException("Got invalid version text: '{}'".format(response_text), http_code=500)
614 except aiohttp.errors.ClientOSError as e:
615 raise ROClientException(e, http_code=504)
616 except asyncio.TimeoutError:
617 raise ROClientException("Timeout", http_code=504)
618 except Exception as e:
619 raise ROClientException("Got invalid version text: '{}'; causing exception {}".format(response_text, e),
620 http_code=500)
621
622 async def get_list(self, item, all_tenants=False, filter_by=None):
623 """
624 Obtain a list of items filtering by the specigy filter_by.
625 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
626 :param all_tenants: True if not filtering by tenant. Only allowed for admin
627 :param filter_by: dictionary with filtering
628 :return: a list of dict. It can be empty. Raises ROClientException on Error,
629 """
630 try:
631 if item not in self.client_to_RO:
632 raise ROClientException("Invalid item {}".format(item))
633 if item == 'tenant':
634 all_tenants = None
635 with aiohttp.ClientSession(loop=self.loop) as session:
636 content = await self._list_item(session, self.client_to_RO[item], all_tenants=all_tenants,
637 filter_dict=filter_by)
638 if isinstance(content, dict):
639 if len(content) == 1:
640 for _, v in content.items():
641 return v
642 return content.values()[0]
643 else:
644 raise ROClientException("Output not a list neither dict with len equal 1", http_code=500)
645 return content
646 except aiohttp.errors.ClientOSError as e:
647 raise ROClientException(e, http_code=504)
648 except asyncio.TimeoutError:
649 raise ROClientException("Timeout", http_code=504)
650
651 async def show(self, item, item_id_name=None, extra_item=None, extra_item_id=None, all_tenants=False):
652 """
653 Obtain the information of an item from its id or name
654 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
655 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
656 :param extra_item: if supplied, it is used to add to the URL.
657 Can be 'action' if item='ns'; 'networks' or'images' if item='vim'
658 :param extra_item_id: if supplied, it is used get details of a concrete extra_item.
659 :param all_tenants: True if not filtering by tenant. Only allowed for admin
660 :return: dictionary with the information or raises ROClientException on Error, NotFound, found several
661 """
662 try:
663 if item not in self.client_to_RO:
664 raise ROClientException("Invalid item {}".format(item))
665 if item == 'tenant':
666 all_tenants = None
667 elif item == 'vim':
668 all_tenants = True
669 elif item == 'vim_account':
670 all_tenants = False
671
672 with aiohttp.ClientSession(loop=self.loop) as session:
673 content = await self._get_item(session, self.client_to_RO[item], item_id_name, extra_item=extra_item,
674 extra_item_id=extra_item_id, all_tenants=all_tenants)
675 return remove_envelop(item, content)
676 except aiohttp.errors.ClientOSError as e:
677 raise ROClientException(e, http_code=504)
678 except asyncio.TimeoutError:
679 raise ROClientException("Timeout", http_code=504)
680
681 async def delete(self, item, item_id_name=None, all_tenants=False):
682 """
683 Delete the information of an item from its id or name
684 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
685 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
686 :param all_tenants: True if not filtering by tenant. Only allowed for admin
687 :return: dictionary with the information or raises ROClientException on Error, NotFound, found several
688 """
689 try:
690 if item not in self.client_to_RO:
691 raise ROClientException("Invalid item {}".format(item))
692 if item == 'tenant' or item == 'vim':
693 all_tenants = None
694
695 with aiohttp.ClientSession(loop=self.loop) as session:
696 return await self._del_item(session, self.client_to_RO[item], item_id_name, all_tenants=all_tenants)
697 except aiohttp.errors.ClientOSError as e:
698 raise ROClientException(e, http_code=504)
699 except asyncio.TimeoutError:
700 raise ROClientException("Timeout", http_code=504)
701
702 async def edit(self, item, item_id_name, descriptor=None, descriptor_format=None, **kwargs):
703 """ Edit an item
704 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns', 'vim'
705 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
706 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
707 :param descriptor_format: Can be 'json' or 'yaml'
708 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
709 keys can be a dot separated list to specify elements inside dict
710 :return: dictionary with the information or raises ROClientException on Error
711 """
712 try:
713 if isinstance(descriptor, str):
714 descriptor = self._parse(descriptor, descriptor_format)
715 elif descriptor:
716 pass
717 else:
718 descriptor = {}
719
720 if item not in self.client_to_RO:
721 raise ROClientException("Invalid item {}".format(item))
722 desc = remove_envelop(item, descriptor)
723
724 # Override descriptor with kwargs
725 if kwargs:
726 desc = self.update_descriptor(desc, kwargs)
727 all_tenants = False
728 if item in ('tenant', 'vim'):
729 all_tenants = None
730
731 create_desc = self._create_envelop(item, desc)
732
733 with aiohttp.ClientSession(loop=self.loop) as session:
734 _all_tenants = all_tenants
735 if item == 'vim':
736 _all_tenants = True
737 item_id = await self._get_item_uuid(session, self.client_to_RO[item], item_id_name,
738 all_tenants=_all_tenants)
739 if item == 'vim':
740 _all_tenants = None
741 # await self._get_tenant(session)
742 outdata = await self._edit_item(session, self.client_to_RO[item], item_id, create_desc,
743 all_tenants=_all_tenants)
744 return remove_envelop(item, outdata)
745 except aiohttp.errors.ClientOSError as e:
746 raise ROClientException(e, http_code=504)
747 except asyncio.TimeoutError:
748 raise ROClientException("Timeout", http_code=504)
749
750 async def create(self, item, descriptor=None, descriptor_format=None, **kwargs):
751 """
752 Creates an item from its descriptor
753 :param item: can be 'tenant', 'vnfd', 'nsd', 'ns', 'vim', 'vim_account', 'sdn'
754 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
755 :param descriptor_format: Can be 'json' or 'yaml'
756 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
757 keys can be a dot separated list to specify elements inside dict
758 :return: dictionary with the information or raises ROClientException on Error
759 """
760 try:
761 if isinstance(descriptor, str):
762 descriptor = self._parse(descriptor, descriptor_format)
763 elif descriptor:
764 pass
765 else:
766 descriptor = {}
767
768 if item not in self.client_to_RO:
769 raise ROClientException("Invalid item {}".format(item))
770 desc = remove_envelop(item, descriptor)
771
772 # Override descriptor with kwargs
773 if kwargs:
774 desc = self.update_descriptor(desc, kwargs)
775
776 for mandatory in self.mandatory_for_create[item]:
777 if mandatory not in desc:
778 raise ROClientException("'{}' is mandatory parameter for {}".format(mandatory, item))
779
780 all_tenants = False
781 if item in ('tenant', 'vim'):
782 all_tenants = None
783
784 create_desc = self._create_envelop(item, desc)
785
786 with aiohttp.ClientSession(loop=self.loop) as session:
787 outdata = await self._create_item(session, self.client_to_RO[item], create_desc,
788 all_tenants=all_tenants)
789 return remove_envelop(item, outdata)
790 except aiohttp.errors.ClientOSError as e:
791 raise ROClientException(e, http_code=504)
792 except asyncio.TimeoutError:
793 raise ROClientException("Timeout", http_code=504)
794
795 async def create_action(self, item, item_id_name, descriptor=None, descriptor_format=None, **kwargs):
796 """
797 Performs an action over an item
798 :param item: can be 'tenant', 'vnfd', 'nsd', 'ns', 'vim', 'vim_account', 'sdn'
799 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
800 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
801 :param descriptor_format: Can be 'json' or 'yaml'
802 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
803 keys can be a dot separated list to specify elements inside dict
804 :return: dictionary with the information or raises ROClientException on Error
805 """
806 try:
807 if isinstance(descriptor, str):
808 descriptor = self._parse(descriptor, descriptor_format)
809 elif descriptor:
810 pass
811 else:
812 descriptor = {}
813
814 if item not in self.client_to_RO:
815 raise ROClientException("Invalid item {}".format(item))
816 desc = remove_envelop(item, descriptor)
817
818 # Override descriptor with kwargs
819 if kwargs:
820 desc = self.update_descriptor(desc, kwargs)
821
822 all_tenants = False
823 if item in ('tenant', 'vim'):
824 all_tenants = None
825
826 action = None
827 if item == "vims":
828 action = "sdn_mapping"
829 elif item in ("vim_account", "ns"):
830 action = "action"
831
832 # create_desc = self._create_envelop(item, desc)
833 create_desc = desc
834
835 with aiohttp.ClientSession(loop=self.loop) as session:
836 _all_tenants = all_tenants
837 if item == 'vim':
838 _all_tenants = True
839 # item_id = await self._get_item_uuid(session, self.client_to_RO[item], item_id_name,
840 # all_tenants=_all_tenants)
841 outdata = await self._create_item(session, self.client_to_RO[item], create_desc,
842 item_id_name=item_id_name, # item_id_name=item_id
843 action=action, all_tenants=_all_tenants)
844 return remove_envelop(item, outdata)
845 except aiohttp.errors.ClientOSError as e:
846 raise ROClientException(e, http_code=504)
847 except asyncio.TimeoutError:
848 raise ROClientException("Timeout", http_code=504)
849
850 async def attach_datacenter(self, datacenter=None, descriptor=None, descriptor_format=None, **kwargs):
851
852 try:
853 if isinstance(descriptor, str):
854 descriptor = self._parse(descriptor, descriptor_format)
855 elif descriptor:
856 pass
857 else:
858 descriptor = {}
859 desc = remove_envelop("vim", descriptor)
860
861 # # check that exist
862 # uuid = self._get_item_uuid(session, "datacenters", uuid_name, all_tenants=True)
863 # tenant_text = "/" + self._get_tenant()
864 if kwargs:
865 desc = self.update_descriptor(desc, kwargs)
866
867 if not desc.get("vim_tenant_name") and not desc.get("vim_tenant_id"):
868 raise ROClientException("Wrong descriptor. At least vim_tenant_name or vim_tenant_id must be provided")
869 create_desc = self._create_envelop("vim", desc)
870 payload_req = yaml.safe_dump(create_desc)
871 with aiohttp.ClientSession(loop=self.loop) as session:
872 # check that exist
873 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=True)
874 await self._get_tenant(session)
875
876 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=self.tenant,
877 datacenter=item_id)
878 self.logger.debug("RO POST %s %s", url, payload_req)
879 with aiohttp.Timeout(self.timeout_large):
880 async with session.post(url, headers=self.headers_req, data=payload_req) as response:
881 response_text = await response.read()
882 self.logger.debug("POST {} [{}] {}".format(url, response.status, response_text[:100]))
883 if response.status >= 300:
884 raise ROClientException(response_text, http_code=response.status)
885
886 response_desc = self._parse_yaml(response_text, response=True)
887 desc = remove_envelop("vim", response_desc)
888 return desc
889 except aiohttp.errors.ClientOSError as e:
890 raise ROClientException(e, http_code=504)
891 except asyncio.TimeoutError:
892 raise ROClientException("Timeout", http_code=504)
893
894 async def detach_datacenter(self, datacenter=None):
895 # TODO replace the code with delete_item(vim_account,...)
896 try:
897 with aiohttp.ClientSession(loop=self.loop) as session:
898 # check that exist
899 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=False)
900 tenant = await self._get_tenant(session)
901
902 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=tenant,
903 datacenter=item_id)
904 self.logger.debug("RO DELETE %s", url)
905 with aiohttp.Timeout(self.timeout_large):
906 async with session.delete(url, headers=self.headers_req) as response:
907 response_text = await response.read()
908 self.logger.debug("DELETE {} [{}] {}".format(url, response.status, response_text[:100]))
909 if response.status >= 300:
910 raise ROClientException(response_text, http_code=response.status)
911
912 response_desc = self._parse_yaml(response_text, response=True)
913 desc = remove_envelop("vim", response_desc)
914 return desc
915 except aiohttp.errors.ClientOSError as e:
916 raise ROClientException(e, http_code=504)
917 except asyncio.TimeoutError:
918 raise ROClientException("Timeout", http_code=504)
919
920 # TODO convert to asyncio
921 # DATACENTERS
922
923 def edit_datacenter(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False,
924 **kwargs):
925 """Edit the parameters of a datacenter
926 Params: must supply a descriptor or/and a parameter to change
927 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
928 descriptor: with format {'datacenter':{params to change info}}
929 must be a dictionary or a json/yaml text.
930 parameters to change can be supplyied by the descriptor or as parameters:
931 new_name: the datacenter name
932 vim_url: the datacenter URL
933 vim_url_admin: the datacenter URL for administrative issues
934 vim_type: the datacenter type, can be openstack or openvim.
935 public: boolean, available to other tenants
936 description: datacenter description
937 Return: Raises an exception on error, not found or found several
938 Obtain a dictionary with format {'datacenter':{new_datacenter_info}}
939 """
940
941 if isinstance(descriptor, str):
942 descriptor = self.parse(descriptor, descriptor_format)
943 elif descriptor:
944 pass
945 elif kwargs:
946 descriptor = {"datacenter": {}}
947 else:
948 raise ROClientException("Missing descriptor")
949
950 if 'datacenter' not in descriptor or len(descriptor) != 1:
951 raise ROClientException("Descriptor must contain only one 'datacenter' field")
952 for param in kwargs:
953 if param == 'new_name':
954 descriptor['datacenter']['name'] = kwargs[param]
955 else:
956 descriptor['datacenter'][param] = kwargs[param]
957 return self._edit_item("datacenters", descriptor, uuid, name, all_tenants=None)
958
959 def edit_scenario(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False, **kwargs):
960 """Edit the parameters of a scenario
961 Params: must supply a descriptor or/and a parameters to change
962 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
963 descriptor: with format {'scenario':{params to change info}}
964 must be a dictionary or a json/yaml text.
965 parameters to change can be supplyied by the descriptor or as parameters:
966 new_name: the scenario name
967 public: boolean, available to other tenants
968 description: scenario description
969 tenant_id. Propietary tenant
970 Return: Raises an exception on error, not found or found several
971 Obtain a dictionary with format {'scenario':{new_scenario_info}}
972 """
973
974 if isinstance(descriptor, str):
975 descriptor = self.parse(descriptor, descriptor_format)
976 elif descriptor:
977 pass
978 elif kwargs:
979 descriptor = {"scenario": {}}
980 else:
981 raise ROClientException("Missing descriptor")
982
983 if 'scenario' not in descriptor or len(descriptor) > 2:
984 raise ROClientException("Descriptor must contain only one 'scenario' field")
985 for param in kwargs:
986 if param == 'new_name':
987 descriptor['scenario']['name'] = kwargs[param]
988 else:
989 descriptor['scenario'][param] = kwargs[param]
990 return self._edit_item("scenarios", descriptor, uuid, name, all_tenants=None)
991
992 # VIM ACTIONS
993 def vim_action(self, action, item, uuid=None, all_tenants=False, **kwargs):
994 """Perform an action over a vim
995 Params:
996 action: can be 'list', 'get'/'show', 'delete' or 'create'
997 item: can be 'tenants' or 'networks'
998 uuid: uuid of the tenant/net to show or to delete. Ignore otherwise
999 other parameters:
1000 datacenter_name, datacenter_id: datacenters to act on, if missing uses classes store datacenter
1001 descriptor, descriptor_format: descriptor needed on creation, can be a dict or a yaml/json str
1002 must be a dictionary or a json/yaml text.
1003 name: for created tenant/net Overwrite descriptor name if any
1004 description: tenant descriptor. Overwrite descriptor description if any
1005
1006 Return: Raises an exception on error
1007 Obtain a dictionary with format {'tenant':{new_tenant_info}}
1008 """
1009 session = None # TODO remove when changed to asyncio
1010 if item not in ("tenants", "networks", "images"):
1011 raise ROClientException("Unknown value for item '{}', must be 'tenants', 'nets' or "
1012 "images".format(str(item)))
1013
1014 image_actions = ['list', 'get', 'show', 'delete']
1015 if item == "images" and action not in image_actions:
1016 raise ROClientException("Only available actions for item '{}' are {}\n"
1017 "Requested action was '{}'".format(item, ', '.join(image_actions), action))
1018 if all_tenants:
1019 tenant_text = "/any"
1020 else:
1021 tenant_text = "/" + self._get_tenant()
1022
1023 if "datacenter_id" in kwargs or "datacenter_name" in kwargs:
1024 datacenter = self._get_item_uuid(session, "datacenters", kwargs.get("datacenter"), all_tenants=all_tenants)
1025 else:
1026 datacenter = self.get_datacenter(session)
1027
1028 if action == "list":
1029 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
1030 self.logger.debug("GET %s", url)
1031 mano_response = requests.get(url, headers=self.headers_req)
1032 self.logger.debug("RO response: %s", mano_response.text)
1033 content = self._parse_yaml(mano_response.text, response=True)
1034 if mano_response.status_code == 200:
1035 return content
1036 else:
1037 raise ROClientException(str(content), http_code=mano_response.status)
1038 elif action == "get" or action == "show":
1039 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
1040 self.logger.debug("GET %s", url)
1041 mano_response = requests.get(url, headers=self.headers_req)
1042 self.logger.debug("RO response: %s", mano_response.text)
1043 content = self._parse_yaml(mano_response.text, response=True)
1044 if mano_response.status_code == 200:
1045 return content
1046 else:
1047 raise ROClientException(str(content), http_code=mano_response.status)
1048 elif action == "delete":
1049 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
1050 self.logger.debug("DELETE %s", url)
1051 mano_response = requests.delete(url, headers=self.headers_req)
1052 self.logger.debug("RO response: %s", mano_response.text)
1053 content = self._parse_yaml(mano_response.text, response=True)
1054 if mano_response.status_code == 200:
1055 return content
1056 else:
1057 raise ROClientException(str(content), http_code=mano_response.status)
1058 elif action == "create":
1059 if "descriptor" in kwargs:
1060 if isinstance(kwargs["descriptor"], str):
1061 descriptor = self._parse(kwargs["descriptor"], kwargs.get("descriptor_format"))
1062 else:
1063 descriptor = kwargs["descriptor"]
1064 elif "name" in kwargs:
1065 descriptor = {item[:-1]: {"name": kwargs["name"]}}
1066 else:
1067 raise ROClientException("Missing descriptor")
1068
1069 if item[:-1] not in descriptor or len(descriptor) != 1:
1070 raise ROClientException("Descriptor must contain only one 'tenant' field")
1071 if "name" in kwargs:
1072 descriptor[item[:-1]]['name'] = kwargs["name"]
1073 if "description" in kwargs:
1074 descriptor[item[:-1]]['description'] = kwargs["description"]
1075 payload_req = yaml.safe_dump(descriptor)
1076 # print payload_req
1077 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
1078 self.logger.debug("RO POST %s %s", url, payload_req)
1079 mano_response = requests.post(url, headers=self.headers_req, data=payload_req)
1080 self.logger.debug("RO response: %s", mano_response.text)
1081 content = self._parse_yaml(mano_response.text, response=True)
1082 if mano_response.status_code == 200:
1083 return content
1084 else:
1085 raise ROClientException(str(content), http_code=mano_response.status)
1086 else:
1087 raise ROClientException("Unknown value for action '{}".format(str(action)))
1088
1089
1090 if __name__ == '__main__':
1091 RO_URL = "http://localhost:9090/openmano"
1092 TEST_TENANT = "myTenant"
1093 TEST_VIM1 = "myvim"
1094 TEST_URL1 = "https://localhost:5000/v1"
1095 TEST_TYPE1 = "openstack"
1096 TEST_CONFIG1 = {"use_floating_ip": True}
1097 TEST_VIM2 = "myvim2"
1098 TEST_URL2 = "https://localhost:5000/v2"
1099 TEST_TYPE2 = "openvim"
1100 TEST_CONFIG2 = {"config2": "config2", "config3": True}
1101
1102 streamformat = "%(asctime)s %(name)s %(levelname)s: %(message)s"
1103 logging.basicConfig(format=streamformat)
1104 logger = logging.getLogger("ROClient")
1105
1106 tenant_id = None
1107 vim_id = False
1108 loop = asyncio.get_event_loop()
1109 myClient = ROClient(endpoint_url=RO_URL, loop=loop, loglevel="DEBUG")
1110 try:
1111 # test tenant
1112 content = loop.run_until_complete(myClient.get_list("tenant"))
1113 print("tenants", content)
1114 content = loop.run_until_complete(myClient.create("tenant", name=TEST_TENANT))
1115 tenant_id = True
1116 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
1117 print("tenant", TEST_TENANT, content)
1118 content = loop.run_until_complete(myClient.edit("tenant", TEST_TENANT, description="another description"))
1119 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
1120 print("tenant edited", TEST_TENANT, content)
1121 myClient["tenant"] = TEST_TENANT
1122
1123 # test VIM
1124 content = loop.run_until_complete(myClient.create("vim", name=TEST_VIM1, type=TEST_TYPE1, vim_url=TEST_URL1,
1125 config=TEST_CONFIG1))
1126 vim_id = True
1127 content = loop.run_until_complete(myClient.get_list("vim"))
1128 print("vim", content)
1129 content = loop.run_until_complete(myClient.show("vim", TEST_VIM1))
1130 print("vim", TEST_VIM1, content)
1131 content = loop.run_until_complete(myClient.edit("vim", TEST_VIM1, description="another description",
1132 name=TEST_VIM2, type=TEST_TYPE2, vim_url=TEST_URL2,
1133 config=TEST_CONFIG2))
1134 content = loop.run_until_complete(myClient.show("vim", TEST_VIM2))
1135 print("vim edited", TEST_VIM2, content)
1136
1137 # test VIM_ACCOUNT
1138 content = loop.run_until_complete(myClient.attach_datacenter(TEST_VIM2, vim_username='user',
1139 vim_password='pass', vim_tenant_name='vimtenant1',
1140 config=TEST_CONFIG1))
1141 vim_id = True
1142 content = loop.run_until_complete(myClient.get_list("vim_account"))
1143 print("vim_account", content)
1144 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
1145 print("vim_account", TEST_VIM2, content)
1146 content = loop.run_until_complete(myClient.edit("vim_account", TEST_VIM2, vim_username='user2',
1147 vim_password='pass2', vim_tenant_name="vimtenant2",
1148 config=TEST_CONFIG2))
1149 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
1150 print("vim_account edited", TEST_VIM2, content)
1151
1152 myClient["vim"] = TEST_VIM2
1153
1154 except Exception as e:
1155 logger.error("Error {}".format(e), exc_info=True)
1156
1157 for item in (("vim_account", TEST_VIM1), ("vim", TEST_VIM1),
1158 ("vim_account", TEST_VIM2), ("vim", TEST_VIM2),
1159 ("tenant", TEST_TENANT)):
1160 try:
1161 content = loop.run_until_complete(myClient.delete(item[0], item[1]))
1162 print("{} {} deleted; {}".format(item[0], item[1], content))
1163 except Exception as e:
1164 if e.http_code == 404:
1165 print("{} {} not present or already deleted".format(item[0], item[1]))
1166 else:
1167 logger.error("Error {}".format(e), exc_info=True)
1168
1169 loop.close()