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