040eb17eaaf473aff78a8933c17e84002328bff5
[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.
303 :param ns_descriptor: instance descriptor obtained with self.show("ns", )
304 :return: dict iwth key member_vnf_index, value ip_address
305 """
306 ns_ip={}
307 for vnf in ns_descriptor["vnfs"]:
308 ns_ip[str(vnf["member_vnf_index"])] = vnf["ip_address"]
309 #uuid sce_vnf_id
310 # vnf[mgmt_access]: '{interface_id: cf3cbf00-385c-49b4-9a3f-b400b7b15dc6, vm_id: d0dd22a9-91ef-46f1-8e8f-8cf4b2d5b2d7}'
311 # vnf[vms]
312 return ns_ip
313
314 async def _get_item_uuid(self, session, item, item_id_name, all_tenants=False):
315 if all_tenants:
316 tenant_text = "/any"
317 elif all_tenants is None:
318 tenant_text = ""
319 else:
320 if not self.tenant:
321 await self._get_tenant(session)
322 tenant_text = "/" + self.tenant
323
324 item_id = 0
325 url = "{}{}/{}".format(self.endpoint_url, tenant_text, item)
326 if self.check_if_uuid(item_id_name):
327 item_id = item_id_name
328 url += "/" + item_id_name
329 elif item_id_name and item_id_name.startswith("'") and item_id_name.endswith("'"):
330 item_id_name = item_id_name[1:-1]
331 self.logger.debug("openmano GET %s", url)
332 with aiohttp.Timeout(self.timeout_short):
333 async with session.get(url, headers=self.headers_req) as response:
334 response_text = await response.read()
335 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
336 if response.status == 404: # NOT_FOUND
337 raise ROClientException("No {} found with id '{}'".format(item[:-1], item_id_name),
338 http_code=404)
339 if response.status >= 300:
340 raise ROClientException(response_text, http_code=response.status)
341 content = self._parse_yaml(response_text, response=True)
342
343 if item_id:
344 return item_id
345 desc = content[item]
346 assert isinstance(desc, list), "_get_item_uuid get a non dict with a list inside {}".format(type(desc))
347 uuid = None
348 for i in desc:
349 if item_id_name and i["name"] != item_id_name:
350 continue
351 if uuid: # found more than one
352 raise ROClientException(
353 "Found more than one {} with name '{}'. uuid must be used".format(item, item_id_name),
354 http_code=404)
355 uuid = i["uuid"]
356 if not uuid:
357 raise ROClientException("No {} found with name '{}'".format(item[:-1], item_id_name), http_code=404)
358 return uuid
359
360 async def _get_item(self, session, item, item_id_name, all_tenants=False):
361 if all_tenants:
362 tenant_text = "/any"
363 elif all_tenants is None:
364 tenant_text = ""
365 else:
366 if not self.tenant:
367 await self._get_tenant(session)
368 tenant_text = "/" + self.tenant
369
370 if self.check_if_uuid(item_id_name):
371 uuid = item_id_name
372 else:
373 # check that exist
374 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants)
375
376 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, uuid)
377 self.logger.debug("GET %s", url )
378 with aiohttp.Timeout(self.timeout_short):
379 async with session.get(url, headers=self.headers_req) as response:
380 response_text = await response.read()
381 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
382 if response.status >= 300:
383 raise ROClientException(response_text, http_code=response.status)
384
385 return self._parse_yaml(response_text, response=True)
386
387 async def _get_tenant(self, session):
388 if not self.tenant:
389 self.tenant = await self._get_item_uuid(session, "tenants", self.tenant_id_name, None)
390 return self.tenant
391
392 async def _get_datacenter(self, session):
393 if not self.tenant:
394 await self._get_tenant(session)
395 if not self.datacenter:
396 self.datacenter = await self._get_item_uuid(session, "datacenters", self.datacenter_id_name, True)
397 return self.datacenter
398
399 async def _create_item(self, session, item, descriptor, item_id_name=None, action=None, all_tenants=False):
400 if all_tenants:
401 tenant_text = "/any"
402 elif all_tenants is None:
403 tenant_text = ""
404 else:
405 if not self.tenant:
406 await self._get_tenant(session)
407 tenant_text = "/" + self.tenant
408 payload_req = yaml.safe_dump(descriptor)
409 #print payload_req
410
411 api_version_text = ""
412 if item == "vnfs":
413 # assumes version v3 only
414 api_version_text = "/v3"
415 item = "vnfd"
416 elif item == "scenarios":
417 # assumes version v3 only
418 api_version_text = "/v3"
419 item = "nsd"
420
421 if not item_id_name:
422 uuid=""
423 elif self.check_if_uuid(item_id_name):
424 uuid = "/{}".format(item_id_name)
425 else:
426 # check that exist
427 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants)
428 uuid = "/{}".format(uuid)
429 if not action:
430 action = ""
431 else:
432 action = "/".format(action)
433
434 url = "{}{apiver}{tenant}/{item}{id}{action}".format(self.endpoint_url, apiver=api_version_text, tenant=tenant_text,
435 item=item, id=uuid, action=action)
436 self.logger.debug("openmano POST %s %s", url, payload_req)
437 with aiohttp.Timeout(self.timeout_large):
438 async with session.post(url, headers=self.headers_req, data=payload_req) as response:
439 response_text = await response.read()
440 self.logger.debug("POST {} [{}] {}".format(url, response.status, response_text[:100]))
441 if response.status >= 300:
442 raise ROClientException(response_text, http_code=response.status)
443
444 return self._parse_yaml(response_text, response=True)
445
446 async def _del_item(self, session, item, item_id_name, all_tenants=False):
447 if all_tenants:
448 tenant_text = "/any"
449 elif all_tenants is None:
450 tenant_text = ""
451 else:
452 if not self.tenant:
453 await self._get_tenant(session)
454 tenant_text = "/" + self.tenant
455 if not self.check_if_uuid(item_id_name):
456 # check that exist
457 _all_tenants = all_tenants
458 if item == "datacenters":
459 _all_tenants = True
460 uuid = await self._get_item_uuid(session, item, item_id_name, all_tenants=_all_tenants)
461 else:
462 uuid = item_id_name
463
464 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, uuid)
465 self.logger.debug("DELETE %s", url)
466 with aiohttp.Timeout(self.timeout_short):
467 async with session.delete(url, headers=self.headers_req) as response:
468 response_text = await response.read()
469 self.logger.debug("DELETE {} [{}] {}".format(url, response.status, response_text[:100]))
470 if response.status >= 300:
471 raise ROClientException(response_text, http_code=response.status)
472 return self._parse_yaml(response_text, response=True)
473
474 async def _list_item(self, session, item, all_tenants=False, filter_dict=None):
475 if all_tenants:
476 tenant_text = "/any"
477 elif all_tenants is None:
478 tenant_text = ""
479 else:
480 if not self.tenant:
481 await self._get_tenant(session)
482 tenant_text = "/" + self.tenant
483
484 url = "{}{}/{}".format(self.endpoint_url, tenant_text, item)
485 separator = "?"
486 if filter_dict:
487 for k in filter_dict:
488 url += separator + quote(str(k)) + "=" + quote(str(filter_dict[k]))
489 separator = "&"
490 self.logger.debug("openmano GET %s", url)
491 with aiohttp.Timeout(self.timeout_short):
492 async with session.get(url, headers=self.headers_req) as response:
493 response_text = await response.read()
494 self.logger.debug("GET {} [{}] {}".format(url, response.status, response_text[:100]))
495 if response.status >= 300:
496 raise ROClientException(response_text, http_code=response.status)
497 return self._parse_yaml(response_text, response=True)
498
499 async def _edit_item(self, session, item, item_id, descriptor, all_tenants=False):
500 if all_tenants:
501 tenant_text = "/any"
502 elif all_tenants is None:
503 tenant_text = ""
504 else:
505 if not self.tenant:
506 await self._get_tenant(session)
507 tenant_text = "/" + self.tenant
508
509 payload_req = yaml.safe_dump(descriptor)
510
511 #print payload_req
512
513 url = "{}{}/{}/{}".format(self.endpoint_url, tenant_text, item, item_id)
514 self.logger.debug("openmano PUT %s %s", url, payload_req)
515 with aiohttp.Timeout(self.timeout_large):
516 async with session.put(url, headers=self.headers_req, data=payload_req) as response:
517 response_text = await response.read()
518 self.logger.debug("PUT {} [{}] {}".format(url, response.status, response_text[:100]))
519 if response.status >= 300:
520 raise ROClientException(response_text, http_code=response.status)
521 return self._parse_yaml(response_text, response=True)
522
523 async def get_list(self, item, all_tenants=False, filter_by=None):
524 """
525 Obtain a list of items filtering by the specigy filter_by.
526 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
527 :param all_tenants: True if not filtering by tenant. Only allowed for admin
528 :param filter_by: dictionary with filtering
529 :return: a list of dict. It can be empty. Raises ROClientException on Error,
530 """
531 try:
532 if item not in self.client_to_RO:
533 raise ROClientException("Invalid item {}".format(item))
534 if item == 'tenant':
535 all_tenants = None
536 with aiohttp.ClientSession(loop=self.loop) as session:
537 content = await self._list_item(session, self.client_to_RO[item], all_tenants=all_tenants,
538 filter_dict=filter_by)
539 if isinstance(content, dict):
540 if len(content) == 1:
541 for _, v in content.items():
542 return v
543 return content.values()[0]
544 else:
545 raise ROClientException("Output not a list neither dict with len equal 1", http_code=500)
546 return content
547 except aiohttp.errors.ClientOSError as e:
548 raise ROClientException(e, http_code=504)
549
550 async def show(self, item, item_id_name=None, all_tenants=False):
551 """
552 Obtain the information of an item from its id or name
553 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
554 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
555 :param all_tenants: True if not filtering by tenant. Only allowed for admin
556 :return: dictionary with the information or raises ROClientException on Error, NotFound, found several
557 """
558 try:
559 if item not in self.client_to_RO:
560 raise ROClientException("Invalid item {}".format(item))
561 if item == 'tenant':
562 all_tenants = None
563 elif item == 'vim':
564 all_tenants = True
565 elif item == 'vim_account':
566 all_tenants = False
567
568 with aiohttp.ClientSession(loop=self.loop) as session:
569 content = await self._get_item(session, self.client_to_RO[item], item_id_name, all_tenants=all_tenants)
570 return remove_envelop(item, content)
571 except aiohttp.errors.ClientOSError as e:
572 raise ROClientException(e, http_code=504)
573
574 async def delete(self, item, item_id_name=None, all_tenants=False):
575 """
576 Delete the information of an item from its id or name
577 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns'
578 :param item_id_name: RO id or name of the item. Raise and exception if more than one found
579 :param all_tenants: True if not filtering by tenant. Only allowed for admin
580 :return: dictionary with the information or raises ROClientException on Error, NotFound, found several
581 """
582 try:
583 if item not in self.client_to_RO:
584 raise ROClientException("Invalid item {}".format(item))
585 if item == 'tenant' or item == 'vim':
586 all_tenants = None
587
588 with aiohttp.ClientSession(loop=self.loop) as session:
589 return await self._del_item(session, self.client_to_RO[item], item_id_name, all_tenants=all_tenants)
590 except aiohttp.errors.ClientOSError as e:
591 raise ROClientException(e, http_code=504)
592
593 async def edit(self, item, item_id_name, descriptor=None, descriptor_format=None, **kwargs):
594 """ Edit an item
595 :param item: can be 'tenant', 'vim', 'vnfd', 'nsd', 'ns', 'vim'
596 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
597 :param descriptor_format: Can be 'json' or 'yaml'
598 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
599 keys can be a dot separated list to specify elements inside dict
600 :return: dictionary with the information or raises ROClientException on Error
601 """
602 try:
603 if isinstance(descriptor, str):
604 descriptor = self._parse(descriptor, descriptor_format)
605 elif descriptor:
606 pass
607 else:
608 descriptor = {}
609
610 if item not in self.client_to_RO:
611 raise ROClientException("Invalid item {}".format(item))
612 desc = remove_envelop(item, descriptor)
613
614 # Override descriptor with kwargs
615 if kwargs:
616 desc = self.update_descriptor(desc, kwargs)
617 all_tenants = False
618 if item in ('tenant', 'vim'):
619 all_tenants = None
620
621 create_desc = self._create_envelop(item, desc)
622
623 with aiohttp.ClientSession(loop=self.loop) as session:
624 _all_tenants = all_tenants
625 if item == 'vim':
626 _all_tenants = True
627 item_id = await self._get_item_uuid(session, self.client_to_RO[item], item_id_name, all_tenants=_all_tenants)
628 # await self._get_tenant(session)
629 outdata = await self._edit_item(session, self.client_to_RO[item], item_id, create_desc, all_tenants=all_tenants)
630 return remove_envelop(item, outdata)
631 except aiohttp.errors.ClientOSError as e:
632 raise ROClientException(e, http_code=504)
633
634 async def create(self, item, descriptor=None, descriptor_format=None, **kwargs):
635 """
636 Creates an item from its descriptor
637 :param item: can be 'tenant', 'vnfd', 'nsd', 'ns', 'vim', 'vim_account', 'sdn'
638 :param descriptor: can be a dict, or a yaml/json text. Autodetect unless descriptor_format is provided
639 :param descriptor_format: Can be 'json' or 'yaml'
640 :param kwargs: Overrides descriptor with values as name, description, vim_url, vim_url_admin, vim_type
641 keys can be a dot separated list to specify elements inside dict
642 :return: dictionary with the information or raises ROClientException on Error
643 """
644 try:
645 if isinstance(descriptor, str):
646 descriptor = self._parse(descriptor, descriptor_format)
647 elif descriptor:
648 pass
649 else:
650 descriptor = {}
651
652 if item not in self.client_to_RO:
653 raise ROClientException("Invalid item {}".format(item))
654 desc = remove_envelop(item, descriptor)
655
656 # Override descriptor with kwargs
657 if kwargs:
658 desc = self.update_descriptor(desc, kwargs)
659
660 for mandatory in self.mandatory_for_create[item]:
661 if mandatory not in desc:
662 raise ROClientException("'{}' is mandatory parameter for {}".format(mandatory, item))
663
664 all_tenants = False
665 if item in ('tenant', 'vim'):
666 all_tenants = None
667
668 create_desc = self._create_envelop(item, desc)
669
670 with aiohttp.ClientSession(loop=self.loop) as session:
671 outdata = await self._create_item(session, self.client_to_RO[item], create_desc,
672 all_tenants=all_tenants)
673 return remove_envelop(item, outdata)
674 except aiohttp.errors.ClientOSError as e:
675 raise ROClientException(e, http_code=504)
676
677 async def attach_datacenter(self, datacenter=None, descriptor=None, descriptor_format=None, **kwargs):
678
679 if isinstance(descriptor, str):
680 descriptor = self._parse(descriptor, descriptor_format)
681 elif descriptor:
682 pass
683 else:
684 descriptor = {}
685 desc = remove_envelop("vim", descriptor)
686
687 # # check that exist
688 # uuid = self._get_item_uuid(session, "datacenters", uuid_name, all_tenants=True)
689 # tenant_text = "/" + self._get_tenant()
690 if kwargs:
691 desc = self.update_descriptor(desc, kwargs)
692
693 if not desc.get("vim_tenant_name") and not desc.get("vim_tenant_id"):
694 raise ROClientException("Wrong descriptor. At least vim_tenant_name or vim_tenant_id must be provided")
695 create_desc = self._create_envelop("vim", desc)
696 payload_req = yaml.safe_dump(create_desc)
697 with aiohttp.ClientSession(loop=self.loop) as session:
698 # check that exist
699 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=True)
700 await self._get_tenant(session)
701
702 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=self.tenant,
703 datacenter=item_id)
704 self.logger.debug("openmano POST %s %s", url, payload_req)
705 with aiohttp.Timeout(self.timeout_large):
706 async with session.post(url, headers=self.headers_req, data=payload_req) as response:
707 response_text = await response.read()
708 self.logger.debug("POST {} [{}] {}".format(url, response.status, response_text[:100]))
709 if response.status >= 300:
710 raise ROClientException(response_text, http_code=response.status)
711
712 response_desc = self._parse_yaml(response_text, response=True)
713 desc = remove_envelop("vim", response_desc)
714 return desc
715
716 async def detach_datacenter(self, datacenter=None):
717 #TODO replace the code with delete_item(vim_account,...)
718 with aiohttp.ClientSession(loop=self.loop) as session:
719 # check that exist
720 item_id = await self._get_item_uuid(session, "datacenters", datacenter, all_tenants=False)
721 tenant = await self._get_tenant(session)
722
723 url = "{}/{tenant}/datacenters/{datacenter}".format(self.endpoint_url, tenant=tenant,
724 datacenter=item_id)
725 self.logger.debug("openmano DELETE %s", url)
726 with aiohttp.Timeout(self.timeout_large):
727 async with session.delete(url, headers=self.headers_req) as response:
728 response_text = await response.read()
729 self.logger.debug("DELETE {} [{}] {}".format(url, response.status, response_text[:100]))
730 if response.status >= 300:
731 raise ROClientException(response_text, http_code=response.status)
732
733 response_desc = self._parse_yaml(response_text, response=True)
734 desc = remove_envelop("vim", response_desc)
735 return desc
736
737
738 # TODO convert to asyncio
739
740 #DATACENTERS
741
742 def edit_datacenter(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False, **kwargs):
743 """Edit the parameters of a datacenter
744 Params: must supply a descriptor or/and a parameter to change
745 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
746 descriptor: with format {'datacenter':{params to change info}}
747 must be a dictionary or a json/yaml text.
748 parameters to change can be supplyied by the descriptor or as parameters:
749 new_name: the datacenter name
750 vim_url: the datacenter URL
751 vim_url_admin: the datacenter URL for administrative issues
752 vim_type: the datacenter type, can be openstack or openvim.
753 public: boolean, available to other tenants
754 description: datacenter description
755 Return: Raises an exception on error, not found or found several
756 Obtain a dictionary with format {'datacenter':{new_datacenter_info}}
757 """
758
759 if isinstance(descriptor, str):
760 descriptor = self.parse(descriptor, descriptor_format)
761 elif descriptor:
762 pass
763 elif kwargs:
764 descriptor={"datacenter": {}}
765 else:
766 raise ROClientException("Missing descriptor")
767
768 if 'datacenter' not in descriptor or len(descriptor)!=1:
769 raise ROClientException("Descriptor must contain only one 'datacenter' field")
770 for param in kwargs:
771 if param=='new_name':
772 descriptor['datacenter']['name'] = kwargs[param]
773 else:
774 descriptor['datacenter'][param] = kwargs[param]
775 return self._edit_item("datacenters", descriptor, uuid, name, all_tenants=None)
776
777
778 def edit_scenario(self, uuid=None, name=None, descriptor=None, descriptor_format=None, all_tenants=False, **kwargs):
779 """Edit the parameters of a scenario
780 Params: must supply a descriptor or/and a parameters to change
781 uuid or/and name. If only name is supplied, there must be only one or an exception is raised
782 descriptor: with format {'scenario':{params to change info}}
783 must be a dictionary or a json/yaml text.
784 parameters to change can be supplyied by the descriptor or as parameters:
785 new_name: the scenario name
786 public: boolean, available to other tenants
787 description: scenario description
788 tenant_id. Propietary tenant
789 Return: Raises an exception on error, not found or found several
790 Obtain a dictionary with format {'scenario':{new_scenario_info}}
791 """
792
793 if isinstance(descriptor, str):
794 descriptor = self.parse(descriptor, descriptor_format)
795 elif descriptor:
796 pass
797 elif kwargs:
798 descriptor={"scenario": {}}
799 else:
800 raise ROClientException("Missing descriptor")
801
802 if 'scenario' not in descriptor or len(descriptor)>2:
803 raise ROClientException("Descriptor must contain only one 'scenario' field")
804 for param in kwargs:
805 if param=='new_name':
806 descriptor['scenario']['name'] = kwargs[param]
807 else:
808 descriptor['scenario'][param] = kwargs[param]
809 return self._edit_item("scenarios", descriptor, uuid, name, all_tenants=None)
810
811 #VIM ACTIONS
812 def vim_action(self, action, item, uuid=None, all_tenants=False, **kwargs):
813 """Perform an action over a vim
814 Params:
815 action: can be 'list', 'get'/'show', 'delete' or 'create'
816 item: can be 'tenants' or 'networks'
817 uuid: uuid of the tenant/net to show or to delete. Ignore otherwise
818 other parameters:
819 datacenter_name, datacenter_id: datacenters to act on, if missing uses classes store datacenter
820 descriptor, descriptor_format: descriptor needed on creation, can be a dict or a yaml/json str
821 must be a dictionary or a json/yaml text.
822 name: for created tenant/net Overwrite descriptor name if any
823 description: tenant descriptor. Overwrite descriptor description if any
824
825 Return: Raises an exception on error
826 Obtain a dictionary with format {'tenant':{new_tenant_info}}
827 """
828 if item not in ("tenants", "networks", "images"):
829 raise ROClientException("Unknown value for item '{}', must be 'tenants', 'nets' or "
830 "images".format(str(item)))
831
832 image_actions = ['list','get','show','delete']
833 if item == "images" and action not in image_actions:
834 raise ROClientException("Only available actions for item '{}' are {}\n"
835 "Requested action was '{}'".format(item, ', '.join(image_actions), action))
836 if all_tenants:
837 tenant_text = "/any"
838 else:
839 tenant_text = "/"+self._get_tenant()
840
841 if "datacenter_id" in kwargs or "datacenter_name" in kwargs:
842 datacenter = self._get_item_uuid(session, "datacenters", kwargs.get("datacenter"), all_tenants=all_tenants)
843 else:
844 datacenter = self.get_datacenter(session)
845
846 if action=="list":
847 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
848 self.logger.debug("GET %s", url )
849 mano_response = requests.get(url, headers=self.headers_req)
850 self.logger.debug("openmano response: %s", mano_response.text )
851 content = self._parse_yaml(mano_response.text, response=True)
852 if mano_response.status_code==200:
853 return content
854 else:
855 raise ROClientException(str(content), http_code=mano_response.status)
856 elif action=="get" or action=="show":
857 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
858 self.logger.debug("GET %s", url )
859 mano_response = requests.get(url, headers=self.headers_req)
860 self.logger.debug("openmano response: %s", mano_response.text )
861 content = self._parse_yaml(mano_response.text, response=True)
862 if mano_response.status_code==200:
863 return content
864 else:
865 raise ROClientException(str(content), http_code=mano_response.status)
866 elif action=="delete":
867 url = "{}{}/vim/{}/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item, uuid)
868 self.logger.debug("DELETE %s", url )
869 mano_response = requests.delete(url, headers=self.headers_req)
870 self.logger.debug("openmano response: %s", mano_response.text )
871 content = self._parse_yaml(mano_response.text, response=True)
872 if mano_response.status_code==200:
873 return content
874 else:
875 raise ROClientException(str(content), http_code=mano_response.status)
876 elif action=="create":
877 if "descriptor" in kwargs:
878 if isinstance(kwargs["descriptor"], str):
879 descriptor = self._parse(kwargs["descriptor"], kwargs.get("descriptor_format") )
880 else:
881 descriptor = kwargs["descriptor"]
882 elif "name" in kwargs:
883 descriptor={item[:-1]: {"name": kwargs["name"]}}
884 else:
885 raise ROClientException("Missing descriptor")
886
887 if item[:-1] not in descriptor or len(descriptor)!=1:
888 raise ROClientException("Descriptor must contain only one 'tenant' field")
889 if "name" in kwargs:
890 descriptor[ item[:-1] ]['name'] = kwargs["name"]
891 if "description" in kwargs:
892 descriptor[ item[:-1] ]['description'] = kwargs["description"]
893 payload_req = yaml.safe_dump(descriptor)
894 #print payload_req
895 url = "{}{}/vim/{}/{}".format(self.endpoint_url, tenant_text, datacenter, item)
896 self.logger.debug("openmano POST %s %s", url, payload_req)
897 mano_response = requests.post(url, headers = self.headers_req, data=payload_req)
898 self.logger.debug("openmano response: %s", mano_response.text )
899 content = self._parse_yaml(mano_response.text, response=True)
900 if mano_response.status_code==200:
901 return content
902 else:
903 raise ROClientException(str(content), http_code=mano_response.status)
904 else:
905 raise ROClientException("Unknown value for action '{}".format(str(action)))
906
907
908 if __name__ == '__main__':
909 RO_URL = "http://localhost:9090/openmano"
910 TEST_TENANT = "myTenant"
911 TEST_VIM1 = "myvim"
912 TEST_URL1 = "https://localhost:5000/v1"
913 TEST_TYPE1 = "openstack"
914 TEST_CONFIG1 = {"use_floating_ip": True}
915 TEST_VIM2 = "myvim2"
916 TEST_URL2 = "https://localhost:5000/v2"
917 TEST_TYPE2 = "openvim"
918 TEST_CONFIG2 = {"config2": "config2", "config3": True}
919
920 streamformat = "%(asctime)s %(name)s %(levelname)s: %(message)s"
921 logging.basicConfig(format=streamformat)
922 logger = logging.getLogger("ROClient")
923
924 tenant_id = None
925 vim_id = False
926 loop = asyncio.get_event_loop()
927 myClient = ROClient(endpoint_url=RO_URL, loop=loop, loglevel="DEBUG")
928 try:
929 # test tenant
930 content = loop.run_until_complete(myClient.get_list("tenant"))
931 print("tenants", content)
932 content = loop.run_until_complete(myClient.create("tenant", name=TEST_TENANT))
933 tenant_id = True
934 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
935 print("tenant", TEST_TENANT, content)
936 content = loop.run_until_complete(myClient.edit("tenant", TEST_TENANT, description="another description"))
937 content = loop.run_until_complete(myClient.show("tenant", TEST_TENANT))
938 print("tenant edited", TEST_TENANT, content)
939 myClient["tenant"] = TEST_TENANT
940
941
942 # test VIM
943 content = loop.run_until_complete(myClient.create("vim", name=TEST_VIM1, type=TEST_TYPE1, vim_url=TEST_URL1, config=TEST_CONFIG1))
944 vim_id = True
945 content = loop.run_until_complete(myClient.get_list("vim"))
946 print("vim", content)
947 content = loop.run_until_complete(myClient.show("vim", TEST_VIM1))
948 print("vim", TEST_VIM1, content)
949 content = loop.run_until_complete(myClient.edit("vim", TEST_VIM1, description="another description",
950 name=TEST_VIM2, type=TEST_TYPE2, vim_url=TEST_URL2,
951 config=TEST_CONFIG2))
952 content = loop.run_until_complete(myClient.show("vim", TEST_VIM2))
953 print("vim edited", TEST_VIM2, content)
954
955 # test VIM_ACCOUNT
956 content = loop.run_until_complete(myClient.attach_datacenter(TEST_VIM2, vim_username='user',
957 vim_password='pass', vim_tenant_name='vimtenant1', config=TEST_CONFIG1))
958 vim_id = True
959 content = loop.run_until_complete(myClient.get_list("vim_account"))
960 print("vim_account", content)
961 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
962 print("vim_account", TEST_VIM2, content)
963 content = loop.run_until_complete(myClient.edit("vim_account", TEST_VIM2, vim_username='user2', vim_password='pass2',
964 vim_tenant_name="vimtenant2", config=TEST_CONFIG2))
965 content = loop.run_until_complete(myClient.show("vim_account", TEST_VIM2))
966 print("vim_account edited", TEST_VIM2, content)
967
968 myClient["vim"] = TEST_VIM2
969
970 except Exception as e:
971 logger.error("Error {}".format(e), exc_info=True)
972
973 for item in (("vim_account", TEST_VIM1), ("vim", TEST_VIM1),
974 ("vim_account", TEST_VIM2), ("vim", TEST_VIM2),
975 ("tenant", TEST_TENANT)):
976 try:
977 content = loop.run_until_complete(myClient.delete(item[0], item[1]))
978 print("{} {} deleted; {}".format(item[0], item[1], content))
979 except Exception as e:
980 if e.http_code == 404:
981 print("{} {} not present or already deleted".format(item[0], item[1]))
982 else:
983 logger.error("Error {}".format(e), exc_info=True)
984
985 loop.close()
986
987