LCM Providing VDU IP address information
[osm/RO.git] / lcm / 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
32 import json
33 import yaml
34 import logging
35 import sys
36 from urllib.parse import quote
37 from uuid import UUID
38 from copy import deepcopy
39
40 __author__ = "Alfonso Tierno, Pablo Montes"
41 __date__ = "$09-Jan-2018 09:09:48$"
42 __version__ = "0.1.0-r470"
43 version_date = "Jan 2018"
44 requests = None
45
46 class ROClientException(Exception):
47 def __init__(self, message, http_code=400):
48 self.http_code = http_code
49 Exception.__init__(self, message)
50 """Common Exception for all openmano client exceptions"""
51
52
53 def remove_envelop(item, indata=None):
54 """
55 Obtain the useful data removing the envelop. It goes through the vnfd or nsd catalog and returns the
56 vnfd or nsd content
57 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
58 :param indata: Content to be inspected
59 :return: the useful part of indata (a reference, not a new dictionay)
60 """
61 clean_indata = indata
62 if not indata:
63 return {}
64 if item == "vnfd":
65 if clean_indata.get('vnfd:vnfd-catalog'):
66 clean_indata = clean_indata['vnfd:vnfd-catalog']
67 elif clean_indata.get('vnfd-catalog'):
68 clean_indata = clean_indata['vnfd-catalog']
69 if clean_indata.get('vnfd'):
70 if not isinstance(clean_indata['vnfd'], list) or len(clean_indata['vnfd']) != 1:
71 raise ROClientException("'vnfd' must be a list only one element")
72 clean_indata = clean_indata['vnfd'][0]
73 elif item == "nsd":
74 if clean_indata.get('nsd:nsd-catalog'):
75 clean_indata = clean_indata['nsd:nsd-catalog']
76 elif clean_indata.get('nsd-catalog'):
77 clean_indata = clean_indata['nsd-catalog']
78 if clean_indata.get('nsd'):
79 if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
80 raise ROClientException("'nsd' must be a list only one element")
81 clean_indata = clean_indata['nsd'][0]
82 elif item == "sdn":
83 if len(indata) == 1 and "sdn_controller" in indata:
84 clean_indata = indata["sdn_controller"]
85 elif item == "tenant":
86 if len(indata) == 1 and "tenant" in indata:
87 clean_indata = indata["tenant"]
88 elif item in ("vim", "vim_account", "datacenters"):
89 if len(indata) == 1 and "datacenter" in indata:
90 clean_indata = indata["datacenter"]
91 elif item == "ns" or item == "instances":
92 if len(indata) == 1 and "instance" in indata:
93 clean_indata = indata["instance"]
94 else:
95 assert False, "remove_envelop with unknown item {}".format(item)
96
97 return clean_indata
98
99
100 class ROClient:
101 headers_req = {'Accept': 'application/yaml', 'content-type': 'application/yaml'}
102 client_to_RO = {'tenant': 'tenants', 'vim': 'datacenters', 'vim_account': 'datacenters', 'sdn': 'sdn_controllers',
103 'vnfd': 'vnfs', 'nsd': 'scenarios',
104 'ns': 'instances'}
105 mandatory_for_create = {
106 'tenant': ("name", ),
107 'vnfd': ("name", "id", "connection-point", "vdu"),
108 'nsd': ("name", "id", "constituent-vnfd"),
109 'ns': ("name", "scenario", "datacenter"),
110 'vim': ("name", "vim_url"),
111 'vim_account': (),
112 'sdn': ("name", "port", 'ip', 'dpid', 'type'),
113 }
114 timeout_large = 120
115 timeout_short = 30
116
117 def __init__(self, loop, endpoint_url, **kwargs):
118 self.loop = loop
119 self.endpoint_url = endpoint_url
120
121 self.username = kwargs.get("username")
122 self.password = kwargs.get("password")
123 self.tenant_id_name = kwargs.get("tenant")
124 self.tenant = None
125 self.datacenter_id_name = kwargs.get("datacenter")
126 self.datacenter = None
127 logger_name = kwargs.get('logger_name', 'ROClient')
128 self.logger = logging.getLogger(logger_name)
129 if kwargs.get("loglevel"):
130 self.logger.setLevel(kwargs["loglevel"])
131 global requests
132 requests = kwargs.get("TODO remove")
133
134 def __getitem__(self, index):
135 if index == 'tenant':
136 return self.tenant_id_name
137 elif index == 'datacenter':
138 return self.datacenter_id_name
139 elif index == 'username':
140 return self.username
141 elif index == 'password':
142 return self.password
143 elif index == 'endpoint_url':
144 return self.endpoint_url
145 else:
146 raise KeyError("Invalid key '%s'" %str(index))
147
148 def __setitem__(self,index, value):
149 if index == 'tenant':
150 self.tenant_id_name = value
151 elif index == 'datacenter' or index == 'vim':
152 self.datacenter_id_name = value
153 elif index == 'username':
154 self.username = value
155 elif index == 'password':
156 self.password = value
157 elif index == 'endpoint_url':
158 self.endpoint_url = value
159 else:
160 raise KeyError("Invalid key '{}'".format(index))
161 self.tenant = None # force to reload tenant with different credentials
162 self.datacenter = None # force to reload datacenter with different credentials
163
164 def _parse(self, descriptor, descriptor_format, response=False):
165 #try yaml
166 if descriptor_format and descriptor_format != "json" and descriptor_format != "yaml":
167 raise ROClientException("'descriptor_format' must be a 'json' or 'yaml' text")
168 if descriptor_format != "json":
169 try:
170 return yaml.load(descriptor)
171 except yaml.YAMLError as exc:
172 error_pos = ""
173 if hasattr(exc, 'problem_mark'):
174 mark = exc.problem_mark
175 error_pos = " at line:{} column:{}s".format(mark.line+1, mark.column+1)
176 error_text = "yaml format error" + error_pos
177 elif descriptor_format != "yaml":
178 try:
179 return json.loads(descriptor)
180 except Exception as e:
181 if response:
182 error_text = "json format error" + str(e)
183
184 if response:
185 raise ROClientException(error_text)
186 raise ROClientException(error_text)
187
188 def _parse_yaml(self, descriptor, response=False):
189 try:
190 return yaml.load(descriptor)
191 except yaml.YAMLError as exc:
192 error_pos = ""
193 if hasattr(exc, 'problem_mark'):
194 mark = exc.problem_mark
195 error_pos = " at line:{} column:{}s".format(mark.line+1, mark.column+1)
196 error_text = "yaml format error" + error_pos
197 if response:
198 raise ROClientException(error_text)
199 raise ROClientException(error_text)
200
201 @staticmethod
202 def check_if_uuid(uuid_text):
203 """
204 Check if text correspond to an uuid foramt
205 :param uuid_text:
206 :return: True if it is an uuid False if not
207 """
208 try:
209 UUID(uuid_text)
210 return True
211 except (ValueError, TypeError):
212 return False
213
214 @staticmethod
215 def _create_envelop(item, indata=None):
216 """
217 Returns a new dict that incledes indata with the expected envelop
218 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
219 :param indata: Content to be enveloped
220 :return: a new dic with {<envelop>: {indata} } where envelop can be e.g. tenant, datacenter, ...
221 """
222 if item == "vnfd":
223 return {'vnfd-catalog': {'vnfd': [indata]}}
224 elif item == "nsd":
225 return {'nsd-catalog': {'nsd': [indata]}}
226 elif item == "tenant":
227 return {'tenant': indata}
228 elif item in ("vim", "vim_account", "datacenter"):
229 return {'datacenter': indata}
230 elif item == "ns" or item == "instances":
231 return {'instance': 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 get_ns_vnf_ip(ns_descriptor):
301 """
302 Get a dict with the IPs of every vnf and vdu
303 :param ns_descriptor: instance descriptor obtained with self.show("ns", )
304 :return: dict with {member_vnf_index: ip_address, ... member_vnf_index.vdu_id: ip_address ...}
305 """
306 ns_ip = {"vnf": {}, "vdu": {}}
307 for vnf in ns_descriptor["vnfs"]:
308 ns_ip["vnf"][str(vnf["member_vnf_index"])] = vnf["ip_address"]
309 ns_ip["vdu"][str(vnf["member_vnf_index"])] = {}
310 for vm in vnf["vms"]:
311 if vm.get("ip_address"):
312 ns_ip["vdu"][str(vnf["member_vnf_index"])][vm["vdu_osm_id"]] = vm["ip_address"]
313 return ns_ip
314
315 async def _get_item_uuid(self, session, item, item_id_name, all_tenants=False):
316 if all_tenants:
317 tenant_text = "/any"
318 elif all_tenants is None:
319 tenant_text = ""
320 else:
321 if not self.tenant:
322 await self._get_tenant(session)
323 tenant_text = "/" + self.tenant
324
325 item_id = 0
326 url = "{}{}/{}".format(self.endpoint_url, tenant_text, item)
327 if self.check_if_uuid(item_id_name):
328 item_id = item_id_name
329 url += "/" + item_id_name
330 elif item_id_name and item_id_name.startswith("'") and item_id_name.endswith("'"):
331 item_id_name = item_id_name[1:-1]
332 self.logger.debug("openmano GET %s", url)
333 with aiohttp.Timeout(self.timeout_short):
334 async with session.get(url, headers=self.headers_req) as response:
335 response_text = await response.read()
336 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
337 if response.status == 404: # NOT_FOUND
338 raise ROClientException("No {} found with id '{}'".format(item[:-1], item_id_name),
339 http_code=404)
340 if response.status >= 300:
341 raise ROClientException(response_text, http_code=response.status)
342 content = self._parse_yaml(response_text, response=True)
343
344 if item_id:
345 return item_id
346 desc = content[item]
347 assert isinstance(desc, list), "_get_item_uuid get a non dict with a list inside {}".format(type(desc))
348 uuid = None
349 for i in desc:
350 if item_id_name and i["name"] != item_id_name:
351 continue
352 if uuid: # found more than one
353 raise ROClientException(
354 "Found more than one {} with name '{}'. uuid must be used".format(item, item_id_name),
355 http_code=404)
356 uuid = i["uuid"]
357 if not uuid:
358 raise ROClientException("No {} found with name '{}'".format(item[:-1], item_id_name), http_code=404)
359 return uuid
360
361 async def _get_item(self, session, item, item_id_name, all_tenants=False):
362 if all_tenants:
363 tenant_text = "/any"
364 elif all_tenants is None:
365 tenant_text = ""
366 else:
367 if not self.tenant:
368 await self._get_tenant(session)
369 tenant_text = "/" + self.tenant
370
371 if self.check_if_uuid(item_id_name):
372 uuid = item_id_name
373 else:
374 # check that exist
375 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants)
376
377 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, uuid)
378 self.logger.debug("GET %s", url )
379 with aiohttp.Timeout(self.timeout_short):
380 async with session.get(url, headers=self.headers_req) as response:
381 response_text = await response.read()
382 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
383 if response.status >= 300:
384 raise ROClientException(response_text, http_code=response.status)
385
386 return self._parse_yaml(response_text, response=True)
387
388 async def _get_tenant(self, session):
389 if not self.tenant:
390 self.tenant = await self._get_item_uuid(session, "tenants", self.tenant_id_name, None)
391 return self.tenant
392
393 async def _get_datacenter(self, session):
394 if not self.tenant:
395 await self._get_tenant(session)
396 if not self.datacenter:
397 self.datacenter = await self._get_item_uuid(session, "datacenters", self.datacenter_id_name, True)
398 return self.datacenter
399
400 async def _create_item(self, session, item, descriptor, item_id_name=None, action=None, all_tenants=False):
401 if all_tenants:
402 tenant_text = "/any"
403 elif all_tenants is None:
404 tenant_text = ""
405 else:
406 if not self.tenant:
407 await self._get_tenant(session)
408 tenant_text = "/" + self.tenant
409 payload_req = yaml.safe_dump(descriptor)
410 #print payload_req
411
412 api_version_text = ""
413 if item == "vnfs":
414 # assumes version v3 only
415 api_version_text = "/v3"
416 item = "vnfd"
417 elif item == "scenarios":
418 # assumes version v3 only
419 api_version_text = "/v3"
420 item = "nsd"
421
422 if not item_id_name:
423 uuid=""
424 elif self.check_if_uuid(item_id_name):
425 uuid = "/{}".format(item_id_name)
426 else:
427 # check that exist
428 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants)
429 uuid = "/{}".format(uuid)
430 if not action:
431 action = ""
432 else:
433 action = "/".format(action)
434
435 url = "{}{apiver}{tenant}/{item}{id}{action}".format(self.endpoint_url, apiver=api_version_text, tenant=tenant_text,
436 item=item, id=uuid, action=action)
437 self.logger.debug("openmano POST %s %s", url, payload_req)
438 with aiohttp.Timeout(self.timeout_large):
439 async with session.post(url, headers=self.headers_req, data=payload_req) as response:
440 response_text = await response.read()
441 self.logger.debug("POST {} [{}] {}".format(url, response.status, response_text[:100]))
442 if response.status >= 300:
443 raise ROClientException(response_text, http_code=response.status)
444
445 return self._parse_yaml(response_text, response=True)
446
447 async def _del_item(self, session, item, item_id_name, all_tenants=False):
448 if all_tenants:
449 tenant_text = "/any"
450 elif all_tenants is None:
451 tenant_text = ""
452 else:
453 if not self.tenant:
454 await self._get_tenant(session)
455 tenant_text = "/" + self.tenant
456 if not self.check_if_uuid(item_id_name):
457 # check that exist
458 _all_tenants = all_tenants
459 if item == "datacenters":
460 _all_tenants = True
461 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants=_all_tenants)
462 else:
463 uuid = item_id_name
464
465 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, uuid)
466 self.logger.debug("DELETE %s", url)
467 with aiohttp.Timeout(self.timeout_short):
468 async with session.delete(url, headers=self.headers_req) as response:
469 response_text = await response.read()
470 self.logger.debug("DELETE {} [{}] {}".format(url, response.status, response_text[:100]))
471 if response.status >= 300:
472 raise ROClientException(response_text, http_code=response.status)
473 return self._parse_yaml(response_text, response=True)
474
475 async def _list_item(self, session, item, all_tenants=False, filter_dict=None):
476 if all_tenants:
477 tenant_text = "/any"
478 elif all_tenants is None:
479 tenant_text = ""
480 else:
481 if not self.tenant:
482 await self._get_tenant(session)
483 tenant_text = "/" + self.tenant
484
485 url = "{}{}/{}".format(self.endpoint_url, tenant_text, item)
486 separator = "?"
487 if filter_dict:
488 for k in filter_dict:
489 url += separator + quote(str(k)) + "=" + quote(str(filter_dict[k]))
490 separator = "&"
491 self.logger.debug("openmano GET %s", url)
492 with aiohttp.Timeout(self.timeout_short):
493 async with session.get(url, headers=self.headers_req) as response:
494 response_text = await response.read()
495 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
496 if response.status >= 300:
497 raise ROClientException(response_text, http_code=response.status)
498 return self._parse_yaml(response_text, response=True)
499
500 async def _edit_item(self, session, item, item_id, descriptor, all_tenants=False):
501 if all_tenants:
502 tenant_text = "/any"
503 elif all_tenants is None:
504 tenant_text = ""
505 else:
506 if not self.tenant:
507 await self._get_tenant(session)
508 tenant_text = "/" + self.tenant
509
510 payload_req = yaml.safe_dump(descriptor)
511
512 #print payload_req
513
514 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, item_id)
515 self.logger.debug("openmano PUT %s %s", url, payload_req)
516 with aiohttp.Timeout(self.timeout_large):
517 async with session.put(url, headers=self.headers_req, data=payload_req) as response:
518 response_text = await response.read()
519 self.logger.debug("PUT {} [{}] {}".format(url, response.status, response_text[:100]))
520 if response.status >= 300:
521 raise ROClientException(response_text, http_code=response.status)
522 return self._parse_yaml(response_text, response=True)
523
524 async def get_list(self, item, all_tenants=False, filter_by=None):
525 """
526 Obtain a list of items filtering by the specigy filter_by.
527 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
528 :param all_tenants: True if not filtering by tenant. Only allowed for admin
529 :param filter_by: dictionary with filtering
530 :return: a list of dict. It can be empty. Raises ROClientException on Error,
531 """
532 try:
533 if item not in self.client_to_RO:
534 raise ROClientException("Invalid item {}".format(item))
535 if item == 'tenant':
536 all_tenants = None
537 with aiohttp.ClientSession(loop=self.loop) as session:
538 content = await self._list_item(session, self.client_to_RO[item], all_tenants=all_tenants,
539 filter_dict=filter_by)
540 if isinstance(content, dict):
541 if len(content) == 1:
542 for _, v in content.items():
543 return v
544 return content.values()[0]
545 else:
546 raise ROClientException("Output not a list neither dict with len equal 1", http_code=500)
547 return content
548 except aiohttp.errors.ClientOSError as e:
549 raise ROClientException(e, http_code=504)
550
551 async def show(self, item, item_id_name=None, all_tenants=False):
552 """
553 Obtain the information of an item from its id or name
554 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
555 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
556 :param all_tenants: True if not filtering by tenant. Only allowed for admin
557 :return: dictionary with the information or raises ROClientException on Error, NotFound, found several
558 """
559 try:
560 if item not in self.client_to_RO:
561 raise ROClientException("Invalid item {}".format(item))
562 if item == 'tenant':
563 all_tenants = None
564 elif item == 'vim':
565 all_tenants = True
566 elif item == 'vim_account':
567 all_tenants = False
568
569 with aiohttp.ClientSession(loop=self.loop) as session:
570 content = await self._get_item(session, self.client_to_RO[item], item_id_name, all_tenants=all_tenants)
571 return remove_envelop(item, content)
572 except aiohttp.errors.ClientOSError as e:
573 raise ROClientException(e, http_code=504)
574
575 async def delete(self, item, item_id_name=None, all_tenants=False):
576 """
577 Delete the information of an item from its id or name
578 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
579 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
580 :param all_tenants: True if not filtering by tenant. Only allowed for admin
581 :return: dictionary with the information or raises ROClientException on Error, NotFound, found several
582 """
583 try:
584 if item not in self.client_to_RO:
585 raise ROClientException("Invalid item {}".format(item))
586 if item == 'tenant' or item == 'vim':
587 all_tenants = None
588
589 with aiohttp.ClientSession(loop=self.loop) as session:
590 return await self._del_item(session, self.client_to_RO[item], item_id_name, all_tenants=all_tenants)
591 except aiohttp.errors.ClientOSError as e:
592 raise ROClientException(e, http_code=504)
593
594 async def edit(self, item, item_id_name, descriptor=None, descriptor_format=None, **kwargs):
595 """ Edit an item
596 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns', 'vim'
597 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
598 :param descriptor_format: Can be 'json' or 'yaml'
599 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
600 keys can be a dot separated list to specify elements inside dict
601 :return: dictionary with the information or raises ROClientException on Error
602 """
603 try:
604 if isinstance(descriptor, str):
605 descriptor = self._parse(descriptor, descriptor_format)
606 elif descriptor:
607 pass
608 else:
609 descriptor = {}
610
611 if item not in self.client_to_RO:
612 raise ROClientException("Invalid item {}".format(item))
613 desc = remove_envelop(item, descriptor)
614
615 # Override descriptor with kwargs
616 if kwargs:
617 desc = self.update_descriptor(desc, kwargs)
618 all_tenants = False
619 if item in ('tenant', 'vim'):
620 all_tenants = None
621
622 create_desc = self._create_envelop(item, desc)
623
624 with aiohttp.ClientSession(loop=self.loop) as session:
625 _all_tenants = all_tenants
626 if item == 'vim':
627 _all_tenants = True
628 item_id = await self._get_item_uuid(session, self.client_to_RO[item], item_id_name, all_tenants=_all_tenants)
629 # await self._get_tenant(session)
630 outdata = await self._edit_item(session, self.client_to_RO[item], item_id, create_desc, all_tenants=all_tenants)
631 return remove_envelop(item, outdata)
632 except aiohttp.errors.ClientOSError as e:
633 raise ROClientException(e, http_code=504)
634
635 async def create(self, item, descriptor=None, descriptor_format=None, **kwargs):
636 """
637 Creates an item from its descriptor
638 :param item: can be 'tenant', 'vnfd', 'nsd', 'ns', 'vim', 'vim_account', 'sdn'
639 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
640 :param descriptor_format: Can be 'json' or 'yaml'
641 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
642 keys can be a dot separated list to specify elements inside dict
643 :return: dictionary with the information or raises ROClientException on Error
644 """
645 try:
646 if isinstance(descriptor, str):
647 descriptor = self._parse(descriptor, descriptor_format)
648 elif descriptor:
649 pass
650 else:
651 descriptor = {}
652
653 if item not in self.client_to_RO:
654 raise ROClientException("Invalid item {}".format(item))
655 desc = remove_envelop(item, descriptor)
656
657 # Override descriptor with kwargs
658 if kwargs:
659 desc = self.update_descriptor(desc, kwargs)
660
661 for mandatory in self.mandatory_for_create[item]:
662 if mandatory not in desc:
663 raise ROClientException("'{}' is mandatory parameter for {}".format(mandatory, item))
664
665 all_tenants = False
666 if item in ('tenant', 'vim'):
667 all_tenants = None
668
669 create_desc = self._create_envelop(item, desc)
670
671 with aiohttp.ClientSession(loop=self.loop) as session:
672 outdata = await self._create_item(session, self.client_to_RO[item], create_desc,
673 all_tenants=all_tenants)
674 return remove_envelop(item, outdata)
675 except aiohttp.errors.ClientOSError as e:
676 raise ROClientException(e, http_code=504)
677
678 async def attach_datacenter(self, datacenter=None, descriptor=None, descriptor_format=None, **kwargs):
679
680 if isinstance(descriptor, str):
681 descriptor = self._parse(descriptor, descriptor_format)
682 elif descriptor:
683 pass
684 else:
685 descriptor = {}
686 desc = remove_envelop("vim", descriptor)
687
688 # # check that exist
689 # uuid = self._get_item_uuid(session, "datacenters", uuid_name, all_tenants=True)
690 # tenant_text = "/" + self._get_tenant()
691 if kwargs:
692 desc = self.update_descriptor(desc, kwargs)
693
694 if not desc.get("vim_tenant_name") and not desc.get("vim_tenant_id"):
695 raise ROClientException("Wrong descriptor. At least vim_tenant_name or vim_tenant_id must be provided")
696 create_desc = self._create_envelop("vim", desc)
697 payload_req = yaml.safe_dump(create_desc)
698 with aiohttp.ClientSession(loop=self.loop) as session:
699 # check that exist
700 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=True)
701 await self._get_tenant(session)
702
703 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=self.tenant,
704 datacenter=item_id)
705 self.logger.debug("openmano POST %s %s", url, payload_req)
706 with aiohttp.Timeout(self.timeout_large):
707 async with session.post(url, headers=self.headers_req, data=payload_req) as response:
708 response_text = await response.read()
709 self.logger.debug("POST {} [{}] {}".format(url, response.status, response_text[:100]))
710 if response.status >= 300:
711 raise ROClientException(response_text, http_code=response.status)
712
713 response_desc = self._parse_yaml(response_text, response=True)
714 desc = remove_envelop("vim", response_desc)
715 return desc
716
717 async def detach_datacenter(self, datacenter=None):
718 #TODO replace the code with delete_item(vim_account,...)
719 with aiohttp.ClientSession(loop=self.loop) as session:
720 # check that exist
721 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=False)
722 tenant = await self._get_tenant(session)
723
724 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=tenant,
725 datacenter=item_id)
726 self.logger.debug("openmano DELETE %s", url)
727 with aiohttp.Timeout(self.timeout_large):
728 async with session.delete(url, headers=self.headers_req) as response:
729 response_text = await response.read()
730 self.logger.debug("DELETE {} [{}] {}".format(url, response.status, response_text[:100]))
731 if response.status >= 300:
732 raise ROClientException(response_text, http_code=response.status)
733
734 response_desc = self._parse_yaml(response_text, response=True)
735 desc = remove_envelop("vim", response_desc)
736 return desc
737
738
739 # TODO convert to asyncio
740
741 #DATACENTERS
742
743 def edit_datacenter(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False, **kwargs):
744 """Edit the parameters of a datacenter
745 Params: must supply a descriptor or/and a parameter to change
746 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
747 descriptor: with format {'datacenter':{params to change info}}
748 must be a dictionary or a json/yaml text.
749 parameters to change can be supplyied by the descriptor or as parameters:
750 new_name: the datacenter name
751 vim_url: the datacenter URL
752 vim_url_admin: the datacenter URL for administrative issues
753 vim_type: the datacenter type, can be openstack or openvim.
754 public: boolean, available to other tenants
755 description: datacenter description
756 Return: Raises an exception on error, not found or found several
757 Obtain a dictionary with format {'datacenter':{new_datacenter_info}}
758 """
759
760 if isinstance(descriptor, str):
761 descriptor = self.parse(descriptor, descriptor_format)
762 elif descriptor:
763 pass
764 elif kwargs:
765 descriptor={"datacenter": {}}
766 else:
767 raise ROClientException("Missing descriptor")
768
769 if 'datacenter' not in descriptor or len(descriptor)!=1:
770 raise ROClientException("Descriptor must contain only one 'datacenter' field")
771 for param in kwargs:
772 if param=='new_name':
773 descriptor['datacenter']['name'] = kwargs[param]
774 else:
775 descriptor['datacenter'][param] = kwargs[param]
776 return self._edit_item("datacenters", descriptor, uuid, name, all_tenants=None)
777
778
779 def edit_scenario(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False, **kwargs):
780 """Edit the parameters of a scenario
781 Params: must supply a descriptor or/and a parameters to change
782 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
783 descriptor: with format {'scenario':{params to change info}}
784 must be a dictionary or a json/yaml text.
785 parameters to change can be supplyied by the descriptor or as parameters:
786 new_name: the scenario name
787 public: boolean, available to other tenants
788 description: scenario description
789 tenant_id. Propietary tenant
790 Return: Raises an exception on error, not found or found several
791 Obtain a dictionary with format {'scenario':{new_scenario_info}}
792 """
793
794 if isinstance(descriptor, str):
795 descriptor = self.parse(descriptor, descriptor_format)
796 elif descriptor:
797 pass
798 elif kwargs:
799 descriptor={"scenario": {}}
800 else:
801 raise ROClientException("Missing descriptor")
802
803 if 'scenario' not in descriptor or len(descriptor)>2:
804 raise ROClientException("Descriptor must contain only one 'scenario' field")
805 for param in kwargs:
806 if param=='new_name':
807 descriptor['scenario']['name'] = kwargs[param]
808 else:
809 descriptor['scenario'][param] = kwargs[param]
810 return self._edit_item("scenarios", descriptor, uuid, name, all_tenants=None)
811
812 #VIM ACTIONS
813 def vim_action(self, action, item, uuid=None, all_tenants=False, **kwargs):
814 """Perform an action over a vim
815 Params:
816 action: can be 'list', 'get'/'show', 'delete' or 'create'
817 item: can be 'tenants' or 'networks'
818 uuid: uuid of the tenant/net to show or to delete. Ignore otherwise
819 other parameters:
820 datacenter_name, datacenter_id: datacenters to act on, if missing uses classes store datacenter
821 descriptor, descriptor_format: descriptor needed on creation, can be a dict or a yaml/json str
822 must be a dictionary or a json/yaml text.
823 name: for created tenant/net Overwrite descriptor name if any
824 description: tenant descriptor. Overwrite descriptor description if any
825
826 Return: Raises an exception on error
827 Obtain a dictionary with format {'tenant':{new_tenant_info}}
828 """
829 if item not in ("tenants", "networks", "images"):
830 raise ROClientException("Unknown value for item '{}', must be 'tenants', 'nets' or "
831 "images".format(str(item)))
832
833 image_actions = ['list','get','show','delete']
834 if item == "images" and action not in image_actions:
835 raise ROClientException("Only available actions for item '{}' are {}\n"
836 "Requested action was '{}'".format(item, ', '.join(image_actions), action))
837 if all_tenants:
838 tenant_text = "/any"
839 else:
840 tenant_text = "/"+self._get_tenant()
841
842 if "datacenter_id" in kwargs or "datacenter_name" in kwargs:
843 datacenter = self._get_item_uuid(session, "datacenters", kwargs.get("datacenter"), all_tenants=all_tenants)
844 else:
845 datacenter = self.get_datacenter(session)
846
847 if action=="list":
848 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
849 self.logger.debug("GET %s", url )
850 mano_response = requests.get(url, headers=self.headers_req)
851 self.logger.debug("openmano response: %s", mano_response.text )
852 content = self._parse_yaml(mano_response.text, response=True)
853 if mano_response.status_code==200:
854 return content
855 else:
856 raise ROClientException(str(content), http_code=mano_response.status)
857 elif action=="get" or action=="show":
858 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
859 self.logger.debug("GET %s", url )
860 mano_response = requests.get(url, headers=self.headers_req)
861 self.logger.debug("openmano response: %s", mano_response.text )
862 content = self._parse_yaml(mano_response.text, response=True)
863 if mano_response.status_code==200:
864 return content
865 else:
866 raise ROClientException(str(content), http_code=mano_response.status)
867 elif action=="delete":
868 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
869 self.logger.debug("DELETE %s", url )
870 mano_response = requests.delete(url, headers=self.headers_req)
871 self.logger.debug("openmano response: %s", mano_response.text )
872 content = self._parse_yaml(mano_response.text, response=True)
873 if mano_response.status_code==200:
874 return content
875 else:
876 raise ROClientException(str(content), http_code=mano_response.status)
877 elif action=="create":
878 if "descriptor" in kwargs:
879 if isinstance(kwargs["descriptor"], str):
880 descriptor = self._parse(kwargs["descriptor"], kwargs.get("descriptor_format") )
881 else:
882 descriptor = kwargs["descriptor"]
883 elif "name" in kwargs:
884 descriptor={item[:-1]: {"name": kwargs["name"]}}
885 else:
886 raise ROClientException("Missing descriptor")
887
888 if item[:-1] not in descriptor or len(descriptor)!=1:
889 raise ROClientException("Descriptor must contain only one 'tenant' field")
890 if "name" in kwargs:
891 descriptor[ item[:-1] ]['name'] = kwargs["name"]
892 if "description" in kwargs:
893 descriptor[ item[:-1] ]['description'] = kwargs["description"]
894 payload_req = yaml.safe_dump(descriptor)
895 #print payload_req
896 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
897 self.logger.debug("openmano POST %s %s", url, payload_req)
898 mano_response = requests.post(url, headers = self.headers_req, data=payload_req)
899 self.logger.debug("openmano response: %s", mano_response.text )
900 content = self._parse_yaml(mano_response.text, response=True)
901 if mano_response.status_code==200:
902 return content
903 else:
904 raise ROClientException(str(content), http_code=mano_response.status)
905 else:
906 raise ROClientException("Unknown value for action '{}".format(str(action)))
907
908
909 if __name__ == '__main__':
910 RO_URL = "http://localhost:9090/openmano"
911 TEST_TENANT = "myTenant"
912 TEST_VIM1 = "myvim"
913 TEST_URL1 = "https://localhost:5000/v1"
914 TEST_TYPE1 = "openstack"
915 TEST_CONFIG1 = {"use_floating_ip": True}
916 TEST_VIM2 = "myvim2"
917 TEST_URL2 = "https://localhost:5000/v2"
918 TEST_TYPE2 = "openvim"
919 TEST_CONFIG2 = {"config2": "config2", "config3": True}
920
921 streamformat = "%(asctime)s %(name)s %(levelname)s: %(message)s"
922 logging.basicConfig(format=streamformat)
923 logger = logging.getLogger("ROClient")
924
925 tenant_id = None
926 vim_id = False
927 loop = asyncio.get_event_loop()
928 myClient = ROClient(endpoint_url=RO_URL, loop=loop, loglevel="DEBUG")
929 try:
930 # test tenant
931 content = loop.run_until_complete(myClient.get_list("tenant"))
932 print("tenants", content)
933 content = loop.run_until_complete(myClient.create("tenant", name=TEST_TENANT))
934 tenant_id = True
935 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
936 print("tenant", TEST_TENANT, content)
937 content = loop.run_until_complete(myClient.edit("tenant", TEST_TENANT, description="another description"))
938 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
939 print("tenant edited", TEST_TENANT, content)
940 myClient["tenant"] = TEST_TENANT
941
942
943 # test VIM
944 content = loop.run_until_complete(myClient.create("vim", name=TEST_VIM1, type=TEST_TYPE1, vim_url=TEST_URL1, config=TEST_CONFIG1))
945 vim_id = True
946 content = loop.run_until_complete(myClient.get_list("vim"))
947 print("vim", content)
948 content = loop.run_until_complete(myClient.show("vim", TEST_VIM1))
949 print("vim", TEST_VIM1, content)
950 content = loop.run_until_complete(myClient.edit("vim", TEST_VIM1, description="another description",
951 name=TEST_VIM2, type=TEST_TYPE2, vim_url=TEST_URL2,
952 config=TEST_CONFIG2))
953 content = loop.run_until_complete(myClient.show("vim", TEST_VIM2))
954 print("vim edited", TEST_VIM2, content)
955
956 # test VIM_ACCOUNT
957 content = loop.run_until_complete(myClient.attach_datacenter(TEST_VIM2, vim_username='user',
958 vim_password='pass', vim_tenant_name='vimtenant1', config=TEST_CONFIG1))
959 vim_id = True
960 content = loop.run_until_complete(myClient.get_list("vim_account"))
961 print("vim_account", content)
962 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
963 print("vim_account", TEST_VIM2, content)
964 content = loop.run_until_complete(myClient.edit("vim_account", TEST_VIM2, vim_username='user2', vim_password='pass2',
965 vim_tenant_name="vimtenant2", config=TEST_CONFIG2))
966 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
967 print("vim_account edited", TEST_VIM2, content)
968
969 myClient["vim"] = TEST_VIM2
970
971 except Exception as e:
972 logger.error("Error {}".format(e), exc_info=True)
973
974 for item in (("vim_account", TEST_VIM1), ("vim", TEST_VIM1),
975 ("vim_account", TEST_VIM2), ("vim", TEST_VIM2),
976 ("tenant", TEST_TENANT)):
977 try:
978 content = loop.run_until_complete(myClient.delete(item[0], item[1]))
979 print("{} {} deleted; {}".format(item[0], item[1], content))
980 except Exception as e:
981 if e.http_code == 404:
982 print("{} {} not present or already deleted".format(item[0], item[1]))
983 else:
984 logger.error("Error {}".format(e), exc_info=True)
985
986 loop.close()
987
988