73a1b13006b9fb8946b0a034887c602baa49b54d
[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"),
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 Exception:
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 result = await self._del_item(session, self.client_to_RO[item], item_id_name, all_tenants=all_tenants)
697 # in case of ns delete, get the action_id embeded in text
698 if item == "ns" and result.get("result"):
699 _, _, action_id = result["result"].partition("action_id=")
700 action_id, _, _ = action_id.partition(" ")
701 if action_id:
702 result["action_id"] = action_id
703 return result
704 except aiohttp.errors.ClientOSError as e:
705 raise ROClientException(e, http_code=504)
706 except asyncio.TimeoutError:
707 raise ROClientException("Timeout", http_code=504)
708
709 async def edit(self, item, item_id_name, descriptor=None, descriptor_format=None, **kwargs):
710 """ Edit an item
711 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns', 'vim'
712 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
713 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
714 :param descriptor_format: Can be 'json' or 'yaml'
715 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
716 keys can be a dot separated list to specify elements inside dict
717 :return: dictionary with the information or raises ROClientException on Error
718 """
719 try:
720 if isinstance(descriptor, str):
721 descriptor = self._parse(descriptor, descriptor_format)
722 elif descriptor:
723 pass
724 else:
725 descriptor = {}
726
727 if item not in self.client_to_RO:
728 raise ROClientException("Invalid item {}".format(item))
729 desc = remove_envelop(item, descriptor)
730
731 # Override descriptor with kwargs
732 if kwargs:
733 desc = self.update_descriptor(desc, kwargs)
734 all_tenants = False
735 if item in ('tenant', 'vim'):
736 all_tenants = None
737
738 create_desc = self._create_envelop(item, desc)
739
740 with aiohttp.ClientSession(loop=self.loop) as session:
741 _all_tenants = all_tenants
742 if item == 'vim':
743 _all_tenants = True
744 item_id = await self._get_item_uuid(session, self.client_to_RO[item], item_id_name,
745 all_tenants=_all_tenants)
746 if item == 'vim':
747 _all_tenants = None
748 # await self._get_tenant(session)
749 outdata = await self._edit_item(session, self.client_to_RO[item], item_id, create_desc,
750 all_tenants=_all_tenants)
751 return remove_envelop(item, outdata)
752 except aiohttp.errors.ClientOSError as e:
753 raise ROClientException(e, http_code=504)
754 except asyncio.TimeoutError:
755 raise ROClientException("Timeout", http_code=504)
756
757 async def create(self, item, descriptor=None, descriptor_format=None, **kwargs):
758 """
759 Creates an item from its descriptor
760 :param item: can be 'tenant', 'vnfd', 'nsd', 'ns', 'vim', 'vim_account', 'sdn'
761 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
762 :param descriptor_format: Can be 'json' or 'yaml'
763 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
764 keys can be a dot separated list to specify elements inside dict
765 :return: dictionary with the information or raises ROClientException on Error
766 """
767 try:
768 if isinstance(descriptor, str):
769 descriptor = self._parse(descriptor, descriptor_format)
770 elif descriptor:
771 pass
772 else:
773 descriptor = {}
774
775 if item not in self.client_to_RO:
776 raise ROClientException("Invalid item {}".format(item))
777 desc = remove_envelop(item, descriptor)
778
779 # Override descriptor with kwargs
780 if kwargs:
781 desc = self.update_descriptor(desc, kwargs)
782
783 for mandatory in self.mandatory_for_create[item]:
784 if mandatory not in desc:
785 raise ROClientException("'{}' is mandatory parameter for {}".format(mandatory, item))
786
787 all_tenants = False
788 if item in ('tenant', 'vim'):
789 all_tenants = None
790
791 create_desc = self._create_envelop(item, desc)
792
793 with aiohttp.ClientSession(loop=self.loop) as session:
794 outdata = await self._create_item(session, self.client_to_RO[item], create_desc,
795 all_tenants=all_tenants)
796 return remove_envelop(item, outdata)
797 except aiohttp.errors.ClientOSError as e:
798 raise ROClientException(e, http_code=504)
799 except asyncio.TimeoutError:
800 raise ROClientException("Timeout", http_code=504)
801
802 async def create_action(self, item, item_id_name, descriptor=None, descriptor_format=None, **kwargs):
803 """
804 Performs an action over an item
805 :param item: can be 'tenant', 'vnfd', 'nsd', 'ns', 'vim', 'vim_account', 'sdn'
806 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
807 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
808 :param descriptor_format: Can be 'json' or 'yaml'
809 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
810 keys can be a dot separated list to specify elements inside dict
811 :return: dictionary with the information or raises ROClientException on Error
812 """
813 try:
814 if isinstance(descriptor, str):
815 descriptor = self._parse(descriptor, descriptor_format)
816 elif descriptor:
817 pass
818 else:
819 descriptor = {}
820
821 if item not in self.client_to_RO:
822 raise ROClientException("Invalid item {}".format(item))
823 desc = remove_envelop(item, descriptor)
824
825 # Override descriptor with kwargs
826 if kwargs:
827 desc = self.update_descriptor(desc, kwargs)
828
829 all_tenants = False
830 if item in ('tenant', 'vim'):
831 all_tenants = None
832
833 action = None
834 if item == "vims":
835 action = "sdn_mapping"
836 elif item in ("vim_account", "ns"):
837 action = "action"
838
839 # create_desc = self._create_envelop(item, desc)
840 create_desc = desc
841
842 with aiohttp.ClientSession(loop=self.loop) as session:
843 _all_tenants = all_tenants
844 if item == 'vim':
845 _all_tenants = True
846 # item_id = await self._get_item_uuid(session, self.client_to_RO[item], item_id_name,
847 # all_tenants=_all_tenants)
848 outdata = await self._create_item(session, self.client_to_RO[item], create_desc,
849 item_id_name=item_id_name, # item_id_name=item_id
850 action=action, all_tenants=_all_tenants)
851 return remove_envelop(item, outdata)
852 except aiohttp.errors.ClientOSError as e:
853 raise ROClientException(e, http_code=504)
854 except asyncio.TimeoutError:
855 raise ROClientException("Timeout", http_code=504)
856
857 async def attach_datacenter(self, datacenter=None, descriptor=None, descriptor_format=None, **kwargs):
858
859 try:
860 if isinstance(descriptor, str):
861 descriptor = self._parse(descriptor, descriptor_format)
862 elif descriptor:
863 pass
864 else:
865 descriptor = {}
866 desc = remove_envelop("vim", descriptor)
867
868 # # check that exist
869 # uuid = self._get_item_uuid(session, "datacenters", uuid_name, all_tenants=True)
870 # tenant_text = "/" + self._get_tenant()
871 if kwargs:
872 desc = self.update_descriptor(desc, kwargs)
873
874 if not desc.get("vim_tenant_name") and not desc.get("vim_tenant_id"):
875 raise ROClientException("Wrong descriptor. At least vim_tenant_name or vim_tenant_id must be provided")
876 create_desc = self._create_envelop("vim", desc)
877 payload_req = yaml.safe_dump(create_desc)
878 with aiohttp.ClientSession(loop=self.loop) as session:
879 # check that exist
880 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=True)
881 await self._get_tenant(session)
882
883 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=self.tenant,
884 datacenter=item_id)
885 self.logger.debug("RO POST %s %s", url, payload_req)
886 with aiohttp.Timeout(self.timeout_large):
887 async with session.post(url, headers=self.headers_req, data=payload_req) as response:
888 response_text = await response.read()
889 self.logger.debug("POST {} [{}] {}".format(url, response.status, response_text[:100]))
890 if response.status >= 300:
891 raise ROClientException(response_text, http_code=response.status)
892
893 response_desc = self._parse_yaml(response_text, response=True)
894 desc = remove_envelop("vim", response_desc)
895 return desc
896 except aiohttp.errors.ClientOSError as e:
897 raise ROClientException(e, http_code=504)
898 except asyncio.TimeoutError:
899 raise ROClientException("Timeout", http_code=504)
900
901 async def detach_datacenter(self, datacenter=None):
902 # TODO replace the code with delete_item(vim_account,...)
903 try:
904 with aiohttp.ClientSession(loop=self.loop) as session:
905 # check that exist
906 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=False)
907 tenant = await self._get_tenant(session)
908
909 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=tenant,
910 datacenter=item_id)
911 self.logger.debug("RO DELETE %s", url)
912 with aiohttp.Timeout(self.timeout_large):
913 async with session.delete(url, headers=self.headers_req) as response:
914 response_text = await response.read()
915 self.logger.debug("DELETE {} [{}] {}".format(url, response.status, response_text[:100]))
916 if response.status >= 300:
917 raise ROClientException(response_text, http_code=response.status)
918
919 response_desc = self._parse_yaml(response_text, response=True)
920 desc = remove_envelop("vim", response_desc)
921 return desc
922 except aiohttp.errors.ClientOSError as e:
923 raise ROClientException(e, http_code=504)
924 except asyncio.TimeoutError:
925 raise ROClientException("Timeout", http_code=504)
926
927 # TODO convert to asyncio
928 # DATACENTERS
929
930 def edit_datacenter(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False,
931 **kwargs):
932 """Edit the parameters of a datacenter
933 Params: must supply a descriptor or/and a parameter to change
934 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
935 descriptor: with format {'datacenter':{params to change info}}
936 must be a dictionary or a json/yaml text.
937 parameters to change can be supplyied by the descriptor or as parameters:
938 new_name: the datacenter name
939 vim_url: the datacenter URL
940 vim_url_admin: the datacenter URL for administrative issues
941 vim_type: the datacenter type, can be openstack or openvim.
942 public: boolean, available to other tenants
943 description: datacenter description
944 Return: Raises an exception on error, not found or found several
945 Obtain a dictionary with format {'datacenter':{new_datacenter_info}}
946 """
947
948 if isinstance(descriptor, str):
949 descriptor = self.parse(descriptor, descriptor_format)
950 elif descriptor:
951 pass
952 elif kwargs:
953 descriptor = {"datacenter": {}}
954 else:
955 raise ROClientException("Missing descriptor")
956
957 if 'datacenter' not in descriptor or len(descriptor) != 1:
958 raise ROClientException("Descriptor must contain only one 'datacenter' field")
959 for param in kwargs:
960 if param == 'new_name':
961 descriptor['datacenter']['name'] = kwargs[param]
962 else:
963 descriptor['datacenter'][param] = kwargs[param]
964 return self._edit_item("datacenters", descriptor, uuid, name, all_tenants=None)
965
966 def edit_scenario(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False, **kwargs):
967 """Edit the parameters of a scenario
968 Params: must supply a descriptor or/and a parameters to change
969 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
970 descriptor: with format {'scenario':{params to change info}}
971 must be a dictionary or a json/yaml text.
972 parameters to change can be supplyied by the descriptor or as parameters:
973 new_name: the scenario name
974 public: boolean, available to other tenants
975 description: scenario description
976 tenant_id. Propietary tenant
977 Return: Raises an exception on error, not found or found several
978 Obtain a dictionary with format {'scenario':{new_scenario_info}}
979 """
980
981 if isinstance(descriptor, str):
982 descriptor = self.parse(descriptor, descriptor_format)
983 elif descriptor:
984 pass
985 elif kwargs:
986 descriptor = {"scenario": {}}
987 else:
988 raise ROClientException("Missing descriptor")
989
990 if 'scenario' not in descriptor or len(descriptor) > 2:
991 raise ROClientException("Descriptor must contain only one 'scenario' field")
992 for param in kwargs:
993 if param == 'new_name':
994 descriptor['scenario']['name'] = kwargs[param]
995 else:
996 descriptor['scenario'][param] = kwargs[param]
997 return self._edit_item("scenarios", descriptor, uuid, name, all_tenants=None)
998
999 # VIM ACTIONS
1000 def vim_action(self, action, item, uuid=None, all_tenants=False, **kwargs):
1001 """Perform an action over a vim
1002 Params:
1003 action: can be 'list', 'get'/'show', 'delete' or 'create'
1004 item: can be 'tenants' or 'networks'
1005 uuid: uuid of the tenant/net to show or to delete. Ignore otherwise
1006 other parameters:
1007 datacenter_name, datacenter_id: datacenters to act on, if missing uses classes store datacenter
1008 descriptor, descriptor_format: descriptor needed on creation, can be a dict or a yaml/json str
1009 must be a dictionary or a json/yaml text.
1010 name: for created tenant/net Overwrite descriptor name if any
1011 description: tenant descriptor. Overwrite descriptor description if any
1012
1013 Return: Raises an exception on error
1014 Obtain a dictionary with format {'tenant':{new_tenant_info}}
1015 """
1016 session = None # TODO remove when changed to asyncio
1017 if item not in ("tenants", "networks", "images"):
1018 raise ROClientException("Unknown value for item '{}', must be 'tenants', 'nets' or "
1019 "images".format(str(item)))
1020
1021 image_actions = ['list', 'get', 'show', 'delete']
1022 if item == "images" and action not in image_actions:
1023 raise ROClientException("Only available actions for item '{}' are {}\n"
1024 "Requested action was '{}'".format(item, ', '.join(image_actions), action))
1025 if all_tenants:
1026 tenant_text = "/any"
1027 else:
1028 tenant_text = "/" + self._get_tenant()
1029
1030 if "datacenter_id" in kwargs or "datacenter_name" in kwargs:
1031 datacenter = self._get_item_uuid(session, "datacenters", kwargs.get("datacenter"), all_tenants=all_tenants)
1032 else:
1033 datacenter = self.get_datacenter(session)
1034
1035 if action == "list":
1036 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
1037 self.logger.debug("GET %s", url)
1038 mano_response = requests.get(url, headers=self.headers_req)
1039 self.logger.debug("RO response: %s", mano_response.text)
1040 content = self._parse_yaml(mano_response.text, response=True)
1041 if mano_response.status_code == 200:
1042 return content
1043 else:
1044 raise ROClientException(str(content), http_code=mano_response.status)
1045 elif action == "get" or action == "show":
1046 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
1047 self.logger.debug("GET %s", url)
1048 mano_response = requests.get(url, headers=self.headers_req)
1049 self.logger.debug("RO response: %s", mano_response.text)
1050 content = self._parse_yaml(mano_response.text, response=True)
1051 if mano_response.status_code == 200:
1052 return content
1053 else:
1054 raise ROClientException(str(content), http_code=mano_response.status)
1055 elif action == "delete":
1056 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
1057 self.logger.debug("DELETE %s", url)
1058 mano_response = requests.delete(url, headers=self.headers_req)
1059 self.logger.debug("RO response: %s", mano_response.text)
1060 content = self._parse_yaml(mano_response.text, response=True)
1061 if mano_response.status_code == 200:
1062 return content
1063 else:
1064 raise ROClientException(str(content), http_code=mano_response.status)
1065 elif action == "create":
1066 if "descriptor" in kwargs:
1067 if isinstance(kwargs["descriptor"], str):
1068 descriptor = self._parse(kwargs["descriptor"], kwargs.get("descriptor_format"))
1069 else:
1070 descriptor = kwargs["descriptor"]
1071 elif "name" in kwargs:
1072 descriptor = {item[:-1]: {"name": kwargs["name"]}}
1073 else:
1074 raise ROClientException("Missing descriptor")
1075
1076 if item[:-1] not in descriptor or len(descriptor) != 1:
1077 raise ROClientException("Descriptor must contain only one 'tenant' field")
1078 if "name" in kwargs:
1079 descriptor[item[:-1]]['name'] = kwargs["name"]
1080 if "description" in kwargs:
1081 descriptor[item[:-1]]['description'] = kwargs["description"]
1082 payload_req = yaml.safe_dump(descriptor)
1083 # print payload_req
1084 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
1085 self.logger.debug("RO POST %s %s", url, payload_req)
1086 mano_response = requests.post(url, headers=self.headers_req, data=payload_req)
1087 self.logger.debug("RO response: %s", mano_response.text)
1088 content = self._parse_yaml(mano_response.text, response=True)
1089 if mano_response.status_code == 200:
1090 return content
1091 else:
1092 raise ROClientException(str(content), http_code=mano_response.status)
1093 else:
1094 raise ROClientException("Unknown value for action '{}".format(str(action)))
1095
1096
1097 if __name__ == '__main__':
1098 RO_URL = "http://localhost:9090/openmano"
1099 TEST_TENANT = "myTenant"
1100 TEST_VIM1 = "myvim"
1101 TEST_URL1 = "https://localhost:5000/v1"
1102 TEST_TYPE1 = "openstack"
1103 TEST_CONFIG1 = {"use_floating_ip": True}
1104 TEST_VIM2 = "myvim2"
1105 TEST_URL2 = "https://localhost:5000/v2"
1106 TEST_TYPE2 = "openvim"
1107 TEST_CONFIG2 = {"config2": "config2", "config3": True}
1108
1109 streamformat = "%(asctime)s %(name)s %(levelname)s: %(message)s"
1110 logging.basicConfig(format=streamformat)
1111 logger = logging.getLogger("ROClient")
1112
1113 tenant_id = None
1114 vim_id = False
1115 loop = asyncio.get_event_loop()
1116 myClient = ROClient(endpoint_url=RO_URL, loop=loop, loglevel="DEBUG")
1117 try:
1118 # test tenant
1119 content = loop.run_until_complete(myClient.get_list("tenant"))
1120 print("tenants", content)
1121 content = loop.run_until_complete(myClient.create("tenant", name=TEST_TENANT))
1122 tenant_id = True
1123 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
1124 print("tenant", TEST_TENANT, content)
1125 content = loop.run_until_complete(myClient.edit("tenant", TEST_TENANT, description="another description"))
1126 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
1127 print("tenant edited", TEST_TENANT, content)
1128 myClient["tenant"] = TEST_TENANT
1129
1130 # test VIM
1131 content = loop.run_until_complete(myClient.create("vim", name=TEST_VIM1, type=TEST_TYPE1, vim_url=TEST_URL1,
1132 config=TEST_CONFIG1))
1133 vim_id = True
1134 content = loop.run_until_complete(myClient.get_list("vim"))
1135 print("vim", content)
1136 content = loop.run_until_complete(myClient.show("vim", TEST_VIM1))
1137 print("vim", TEST_VIM1, content)
1138 content = loop.run_until_complete(myClient.edit("vim", TEST_VIM1, description="another description",
1139 name=TEST_VIM2, type=TEST_TYPE2, vim_url=TEST_URL2,
1140 config=TEST_CONFIG2))
1141 content = loop.run_until_complete(myClient.show("vim", TEST_VIM2))
1142 print("vim edited", TEST_VIM2, content)
1143
1144 # test VIM_ACCOUNT
1145 content = loop.run_until_complete(myClient.attach_datacenter(TEST_VIM2, vim_username='user',
1146 vim_password='pass', vim_tenant_name='vimtenant1',
1147 config=TEST_CONFIG1))
1148 vim_id = True
1149 content = loop.run_until_complete(myClient.get_list("vim_account"))
1150 print("vim_account", content)
1151 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
1152 print("vim_account", TEST_VIM2, content)
1153 content = loop.run_until_complete(myClient.edit("vim_account", TEST_VIM2, vim_username='user2',
1154 vim_password='pass2', vim_tenant_name="vimtenant2",
1155 config=TEST_CONFIG2))
1156 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
1157 print("vim_account edited", TEST_VIM2, content)
1158
1159 myClient["vim"] = TEST_VIM2
1160
1161 except Exception as e:
1162 logger.error("Error {}".format(e), exc_info=True)
1163
1164 for item in (("vim_account", TEST_VIM1), ("vim", TEST_VIM1),
1165 ("vim_account", TEST_VIM2), ("vim", TEST_VIM2),
1166 ("tenant", TEST_TENANT)):
1167 try:
1168 content = loop.run_until_complete(myClient.delete(item[0], item[1]))
1169 print("{} {} deleted; {}".format(item[0], item[1], content))
1170 except Exception as e:
1171 if e.http_code == 404:
1172 print("{} {} not present or already deleted".format(item[0], item[1]))
1173 else:
1174 logger.error("Error {}".format(e), exc_info=True)
1175
1176 loop.close()