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