Merge "Support multiple customer instantiation"
[integration.git] / test / vcpe / vcpecommon.py
1 import json
2 import logging
3 import os
4 import pickle
5 import re
6 import sys
7
8 import ipaddress
9 import mysql.connector
10 import requests
11 import commands
12 import time
13
14
15 class VcpeCommon:
16     #############################################################################################
17     #     Start: configurations that you must change for a new ONAP installation
18     external_net_addr = '10.12.0.0'
19     external_net_prefix_len = 16
20     #############################################################################################
21     # set the openstack cloud access credentials here
22     cloud = {
23         '--os-auth-url': 'http://10.12.25.2:5000',
24         '--os-username': 'kxi',
25         '--os-user-domain-id': 'default',
26         '--os-project-domain-id': 'default',
27         '--os-tenant-id': '1e097c6713e74fd7ac8e4295e605ee1e',
28         '--os-region-name': 'RegionOne',
29         '--os-password': 'n3JhGMGuDzD8',
30         '--os-project-domain-name': 'Integration-SB-07',
31         '--os-identity-api-version': '3'
32     }
33
34     common_preload_config = {
35         'oam_onap_net': 'oam_onap_lAky',
36         'oam_onap_subnet': 'oam_onap_lAky',
37         'public_net': 'external',
38         'public_net_id': '971040b2-7059-49dc-b220-4fab50cb2ad4'
39     }
40     #     End: configurations that you must change for a new ONAP installation
41     #############################################################################################
42
43     template_variable_symbol = '${'
44     cpe_vm_prefix = 'zdcpe'
45     #############################################################################################
46     # preloading network config
47     #  key=network role
48     #  value = [subnet_start_ip, subnet_gateway_ip]
49     preload_network_config = {
50         'cpe_public': ['10.2.0.2', '10.2.0.1'],
51         'cpe_signal': ['10.4.0.2', '10.4.0.1'],
52         'brg_bng': ['10.3.0.2', '10.3.0.1'],
53         'bng_mux': ['10.1.0.10', '10.1.0.1'],
54         'mux_gw': ['10.5.0.10', '10.5.0.1']
55     }
56
57     dcae_ves_collector_name = 'dcae-bootstrap'
58     global_subscriber_id = 'SDN-ETHERNET-INTERNET'
59     project_name = 'Project-Demonstration'
60     owning_entity_id = '520cc603-a3c4-4ec2-9ef4-ca70facd79c0'
61     owning_entity_name = 'OE-Demonstration'
62
63     def __init__(self, extra_host_names=None):
64         self.logger = logging.getLogger(__name__)
65         self.logger.info('Initializing configuration')
66
67         self.host_names = ['so', 'sdnc', 'robot', 'aai-inst1', self.dcae_ves_collector_name]
68         if extra_host_names:
69             self.host_names.extend(extra_host_names)
70         # get IP addresses
71         self.hosts = self.get_vm_ip(self.host_names, self.external_net_addr, self.external_net_prefix_len)
72         # this is the keyword used to name vgw stack, must not be used in other stacks
73         self.vgw_name_keyword = 'base_vcpe_vgw'
74         self.svc_instance_uuid_file = '__var/svc_instance_uuid'
75         self.preload_dict_file = '__var/preload_dict'
76         self.vgmux_vnf_name_file = '__var/vgmux_vnf_name'
77         self.product_family_id = 'f9457e8c-4afd-45da-9389-46acd9bf5116'
78         self.custom_product_family_id = 'a9a77d5a-123e-4ca2-9eb9-0b015d2ee0fb'
79         self.instance_name_prefix = {
80             'service': 'vcpe_svc',
81             'network': 'vcpe_net',
82             'vnf': 'vcpe_vnf',
83             'vfmodule': 'vcpe_vfmodule'
84         }
85         self.aai_userpass = 'AAI', 'AAI'
86         self.pub_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKXDgoo3+WOqcUG8/5uUbk81+yczgwC4Y8ywTmuQqbNxlY1oQ0YxdMUqUnhitSXs5S/yRuAVOYHwGg2mCs20oAINrP+mxBI544AMIb9itPjCtgqtE2EWo6MmnFGbHB4Sx3XioE7F4VPsh7japsIwzOjbrQe+Mua1TGQ5d4nfEOQaaglXLLPFfuc7WbhbJbK6Q7rHqZfRcOwAMXgDoBqlyqKeiKwnumddo2RyNT8ljYmvB6buz7KnMinzo7qB0uktVT05FH9Rg0CTWH5norlG5qXgP2aukL0gk1ph8iAt7uYLf1ktp+LJI2gaF6L0/qli9EmVCSLr1uJ38Q8CBflhkh'
87         self.os_tenant_id = self.cloud['--os-tenant-id']
88         self.os_region_name = self.cloud['--os-region-name']
89         self.common_preload_config['pub_key'] = self.pub_key
90         self.sniro_url = 'http://' + self.hosts['robot'] + ':8080/__admin/mappings'
91         self.sniro_headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
92         self.homing_solution = 'sniro'  # value is either 'sniro' or 'oof'
93 #        self.homing_solution = 'oof'
94         self.customer_location_used_by_oof = {
95             "customerLatitude": "32.897480",
96             "customerLongitude": "-97.040443",
97             "customerName": "some_company"
98         }
99
100         #############################################################################################
101         # SDNC urls
102         self.sdnc_userpass = 'admin', 'Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U'
103         self.sdnc_db_name = 'sdnctl'
104         self.sdnc_db_user = 'sdnctl'
105         self.sdnc_db_pass = 'gamma'
106         self.sdnc_db_port = '32774'
107         self.sdnc_headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
108         self.sdnc_preload_network_url = 'http://' + self.hosts['sdnc'] + \
109                                         ':8282/restconf/operations/VNF-API:preload-network-topology-operation'
110         self.sdnc_preload_vnf_url = 'http://' + self.hosts['sdnc'] + \
111                                     ':8282/restconf/operations/VNF-API:preload-vnf-topology-operation'
112         self.sdnc_ar_cleanup_url = 'http://' + self.hosts['sdnc'] + ':8282/restconf/config/GENERIC-RESOURCE-API:'
113
114         #############################################################################################
115         # SO urls, note: do NOT add a '/' at the end of the url
116         self.so_req_api_url = {'v4': 'http://' + self.hosts['so'] + ':8080/ecomp/mso/infra/serviceInstances/v4',
117                            'v5': 'http://' + self.hosts['so'] + ':8080/ecomp/mso/infra/serviceInstances/v5'}
118         self.so_check_progress_api_url = 'http://' + self.hosts['so'] + ':8080/ecomp/mso/infra/orchestrationRequests/v5'
119         self.so_userpass = 'InfraPortalClient', 'password1$'
120         self.so_headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
121         self.so_db_name = 'mso_catalog'
122         self.so_db_user = 'root'
123         self.so_db_pass = 'password'
124         self.so_db_port = '32769'
125
126         self.vpp_inf_url = 'http://{0}:8183/restconf/config/ietf-interfaces:interfaces'
127         self.vpp_api_headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
128         self.vpp_api_userpass = ('admin', 'admin')
129         self.vpp_ves_url= 'http://{0}:8183/restconf/config/vesagent:vesagent'
130
131     def headbridge(self, openstack_stack_name, svc_instance_uuid):
132         """
133         Add vserver information to AAI
134         """
135         self.logger.info('Adding vServer information to AAI for {0}'.format(openstack_stack_name))
136         cmd = '/opt/demo.sh heatbridge {0} {1} vCPE'.format(openstack_stack_name, svc_instance_uuid)
137         ret = commands.getstatusoutput("ssh -i onap_dev root@{0} '{1}'".format(self.hosts['robot'], cmd))
138         self.logger.debug('%s', ret)
139
140     def get_brg_mac_from_sdnc(self):
141         """
142         Check table DHCP_MAP in the SDNC DB. Find the newly instantiated BRG MAC address.
143         Note that there might be multiple BRGs, the most recently instantiated BRG always has the largest IP address.
144         """
145         cnx = mysql.connector.connect(user=self.sdnc_db_user, password=self.sdnc_db_pass, database=self.sdnc_db_name,
146                                       host=self.hosts['sdnc'], port=self.sdnc_db_port)
147         cursor = cnx.cursor()
148         query = "SELECT * from DHCP_MAP"
149         cursor.execute(query)
150
151         self.logger.debug('DHCP_MAP table in SDNC')
152         mac_recent = None
153         host = -1
154         for mac, ip in cursor:
155             self.logger.debug(mac + ':' + ip)
156             this_host = int(ip.split('.')[-1])
157             if host < this_host:
158                 host = this_host
159                 mac_recent = mac
160
161         cnx.close()
162
163         assert mac_recent
164         return mac_recent
165
166     def execute_cmds_sdnc_db(self, cmds):
167         self.execute_cmds_db(cmds, self.sdnc_db_user, self.sdnc_db_pass, self.sdnc_db_name,
168                              self.hosts['sdnc'], self.sdnc_db_port)
169
170     def execute_cmds_so_db(self, cmds):
171         self.execute_cmds_db(cmds, self.so_db_user, self.so_db_pass, self.so_db_name,
172                              self.hosts['so'], self.so_db_port)
173
174     def execute_cmds_db(self, cmds, dbuser, dbpass, dbname, host, port):
175         cnx = mysql.connector.connect(user=dbuser, password=dbpass, database=dbname, host=host, port=port)
176         cursor = cnx.cursor()
177         for cmd in cmds:
178             self.logger.debug(cmd)
179             cursor.execute(cmd)
180             self.logger.debug('%s', cursor)
181         cnx.commit()
182         cursor.close()
183         cnx.close()
184
185     def find_file(self, file_name_keyword, file_ext, search_dir):
186         """
187         :param file_name_keyword:  keyword used to look for the csar file, case insensitive matching, e.g, infra
188         :param file_ext: e.g., csar, json
189         :param search_dir path to search
190         :return: path name of the file
191         """
192         file_name_keyword = file_name_keyword.lower()
193         file_ext = file_ext.lower()
194         if not file_ext.startswith('.'):
195             file_ext = '.' + file_ext
196
197         filenamepath = None
198         for file_name in os.listdir(search_dir):
199             file_name_lower = file_name.lower()
200             if file_name_keyword in file_name_lower and file_name_lower.endswith(file_ext):
201                 if filenamepath:
202                     self.logger.error('Multiple files found for *{0}*.{1} in '
203                                       'directory {2}'.format(file_name_keyword, file_ext, search_dir))
204                     sys.exit()
205                 filenamepath = os.path.abspath(os.path.join(search_dir, file_name))
206
207         if filenamepath:
208             return filenamepath
209         else:
210             self.logger.error("Cannot find *{0}*{1} in directory {2}".format(file_name_keyword, file_ext, search_dir))
211             sys.exit()
212
213     @staticmethod
214     def network_name_to_subnet_name(network_name):
215         """
216         :param network_name: example: vcpe_net_cpe_signal_201711281221
217         :return: vcpe_net_cpe_signal_subnet_201711281221
218         """
219         fields = network_name.split('_')
220         fields.insert(-1, 'subnet')
221         return '_'.join(fields)
222
223     def set_network_name(self, network_name):
224         param = ' '.join([k + ' ' + v for k, v in self.cloud.items()])
225         openstackcmd = 'openstack ' + param
226         cmd = ' '.join([openstackcmd, 'network set --name', network_name, 'ONAP-NW1'])
227         os.popen(cmd)
228
229     def set_subnet_name(self, network_name):
230         """
231         Example: network_name =  vcpe_net_cpe_signal_201711281221
232         set subnet name to vcpe_net_cpe_signal_subnet_201711281221
233         :return:
234         """
235         param = ' '.join([k + ' ' + v for k, v in self.cloud.items()])
236         openstackcmd = 'openstack ' + param
237
238         # expected results: | subnets | subnet_id |
239         subnet_info = os.popen(openstackcmd + ' network show ' + network_name + ' |grep subnets').read().split('|')
240         if len(subnet_info) > 2 and subnet_info[1].strip() == 'subnets':
241             subnet_id = subnet_info[2].strip()
242             subnet_name = self.network_name_to_subnet_name(network_name)
243             cmd = ' '.join([openstackcmd, 'subnet set --name', subnet_name, subnet_id])
244             os.popen(cmd)
245             self.logger.info("Subnet name set to: " + subnet_name)
246             return True
247         else:
248             self.logger.error("Can't get subnet info from network name: " + network_name)
249             return False
250
251     def is_node_in_aai(self, node_type, node_uuid):
252         key = None
253         search_node_type = None
254         if node_type == 'service':
255             search_node_type = 'service-instance'
256             key = 'service-instance-id'
257         elif node_type == 'vnf':
258             search_node_type = 'generic-vnf'
259             key = 'vnf-id'
260         else:
261             logging.error('Invalid node_type: ' + node_type)
262             sys.exit()
263
264         url = 'https://{0}:8443/aai/v11/search/nodes-query?search-node-type={1}&filter={2}:EQUALS:{3}'.format(
265             self.hosts['aai-inst1'], search_node_type, key, node_uuid)
266
267         headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'X-FromAppID': 'vCPE-Robot', 'X-TransactionId': 'get_aai_subscr'}
268         requests.packages.urllib3.disable_warnings()
269         r = requests.get(url, headers=headers, auth=self.aai_userpass, verify=False)
270         response = r.json()
271         self.logger.debug('aai query: ' + url)
272         self.logger.debug('aai response:\n' + json.dumps(response, indent=4, sort_keys=True))
273         return 'result-data' in response
274
275     @staticmethod
276     def extract_ip_from_str(net_addr, net_addr_len, sz):
277         """
278         :param net_addr:  e.g. 10.5.12.0
279         :param net_addr_len: e.g. 24
280         :param sz: a string
281         :return: the first IP address matching the network, e.g. 10.5.12.3
282         """
283         network = ipaddress.ip_network(unicode('{0}/{1}'.format(net_addr, net_addr_len)), strict=False)
284         ip_list = re.findall(r'[0-9]+(?:\.[0-9]+){3}', sz)
285         for ip in ip_list:
286             this_net = ipaddress.ip_network(unicode('{0}/{1}'.format(ip, net_addr_len)), strict=False)
287             if this_net == network:
288                 return str(ip)
289         return None
290
291     def get_vm_ip(self, keywords, net_addr=None, net_addr_len=None):
292         """
293         :param keywords: list of keywords to search for vm, e.g. ['bng', 'gmux', 'brg']
294         :param net_addr: e.g. 10.12.5.0
295         :param net_addr_len: e.g. 24
296         :return: dictionary {keyword: ip}
297         """
298         if not net_addr:
299             net_addr = self.external_net_addr
300
301         if not net_addr_len:
302             net_addr_len = self.external_net_prefix_len
303
304         param = ' '.join([k + ' ' + v for k, v in self.cloud.items() if 'identity' not in k])
305         openstackcmd = 'nova ' + param + ' list'
306         self.logger.debug(openstackcmd)
307
308         results = os.popen(openstackcmd).read()
309         all_vm_ip_dict = self.extract_vm_ip_as_dict(results, net_addr, net_addr_len)
310         latest_vm_list = self.remove_old_vms(all_vm_ip_dict.keys(), self.cpe_vm_prefix)
311         latest_vm_ip_dict = {vm: all_vm_ip_dict[vm] for vm in latest_vm_list}
312         ip_dict = self.select_subset_vm_ip(latest_vm_ip_dict, keywords)
313
314         if len(ip_dict) != len(keywords):
315             self.logger.error('Cannot find all desired IP addresses for %s.', keywords)
316             self.logger.error(json.dumps(ip_dict, indent=4, sort_keys=True))
317             self.logger.error('Temporarily continue.. remember to check back vcpecommon.py line: 316')
318 #            sys.exit()
319         return ip_dict
320
321     def extract_vm_ip_as_dict(self, novalist_results, net_addr, net_addr_len):
322         vm_ip_dict = {}
323         for line in novalist_results.split('\n'):
324             fields = line.split('|')
325             if len(fields) == 8:
326                 vm_name = fields[2]
327                 ip_info = fields[-2]
328                 ip = self.extract_ip_from_str(net_addr, net_addr_len, ip_info)
329                 vm_ip_dict[vm_name] = ip
330
331         return vm_ip_dict
332
333     def remove_old_vms(self, vm_list, prefix):
334         """
335         For vms with format name_timestamp, only keep the one with the latest timestamp.
336         E.g.,
337             zdcpe1cpe01brgemu01_201805222148        (drop this)
338             zdcpe1cpe01brgemu01_201805222229        (keep this)
339             zdcpe1cpe01gw01_201805162201
340         """
341         new_vm_list = []
342         same_type_vm_dict = {}
343         for vm in vm_list:
344             fields = vm.split('_')
345             if vm.startswith(prefix) and len(fields) == 2 and len(fields[-1]) == len('201805222148') and fields[-1].isdigit():
346                 if vm > same_type_vm_dict.get(fields[0], '0'):
347                     same_type_vm_dict[fields[0]] = vm
348             else:
349                 new_vm_list.append(vm)
350
351         new_vm_list.extend(same_type_vm_dict.values())
352         return new_vm_list
353
354     def select_subset_vm_ip(self, all_vm_ip_dict, vm_name_keyword_list):
355         vm_ip_dict = {}
356         for keyword in vm_name_keyword_list:
357             for vm, ip in all_vm_ip_dict.items():
358                 if keyword in vm:
359                     vm_ip_dict[keyword] = ip
360                     break
361         return vm_ip_dict
362
363     def del_vgmux_ves_mode(self):
364         url = self.vpp_ves_url.format(self.hosts['mux']) + '/mode'
365         r = requests.delete(url, headers=self.vpp_api_headers, auth=self.vpp_api_userpass)
366         self.logger.debug('%s', r)
367
368     def del_vgmux_ves_collector(self):
369         url = self.vpp_ves_url.format(self.hosts['mux']) + '/config'
370         r = requests.delete(url, headers=self.vpp_api_headers, auth=self.vpp_api_userpass)
371         self.logger.debug('%s', r)
372
373     def set_vgmux_ves_collector(self ):
374         url = self.vpp_ves_url.format(self.hosts['mux'])
375         data = {'config':
376                     {'server-addr': self.hosts[self.dcae_ves_collector_name],
377                      'server-port': '8081',
378                      'read-interval': '10',
379                      'is-add':'1'
380                      }
381                 }
382         r = requests.post(url, headers=self.vpp_api_headers, auth=self.vpp_api_userpass, json=data)
383         self.logger.debug('%s', r)
384
385     def set_vgmux_packet_loss_rate(self, lossrate, vg_vnf_instance_name):
386         url = self.vpp_ves_url.format(self.hosts['mux'])
387         data = {"mode":
388                     {"working-mode": "demo",
389                      "base-packet-loss": str(lossrate),
390                      "source-name": vg_vnf_instance_name
391                      }
392                 }
393         r = requests.post(url, headers=self.vpp_api_headers, auth=self.vpp_api_userpass, json=data)
394         self.logger.debug('%s', r)
395
396         # return all the VxLAN interface names of BRG or vGMUX based on the IP address
397     def get_vxlan_interfaces(self, ip, print_info=False):
398         url = self.vpp_inf_url.format(ip)
399         self.logger.debug('url is this: %s', url)
400         r = requests.get(url, headers=self.vpp_api_headers, auth=self.vpp_api_userpass)
401         data = r.json()['interfaces']['interface']
402         if print_info:
403             for inf in data:
404                 if 'name' in inf and 'type' in inf and inf['type'] == 'v3po:vxlan-tunnel':
405                     print(json.dumps(inf, indent=4, sort_keys=True))
406
407         return [inf['name'] for inf in data if 'name' in inf and 'type' in inf and inf['type'] == 'v3po:vxlan-tunnel']
408
409     # delete all VxLAN interfaces of each hosts
410     def delete_vxlan_interfaces(self, host_dic):
411         for host, ip in host_dic.items():
412             deleted = False
413             self.logger.info('{0}: Getting VxLAN interfaces'.format(host))
414             inf_list = self.get_vxlan_interfaces(ip)
415             for inf in inf_list:
416                 deleted = True
417                 time.sleep(2)
418                 self.logger.info("{0}: Deleting VxLAN crossconnect {1}".format(host, inf))
419                 url = self.vpp_inf_url.format(ip) + '/interface/' + inf + '/v3po:l2'
420                 requests.delete(url, headers=self.vpp_api_headers, auth=self.vpp_api_userpass)
421
422             for inf in inf_list:
423                 deleted = True
424                 time.sleep(2)
425                 self.logger.info("{0}: Deleting VxLAN interface {1}".format(host, inf))
426                 url = self.vpp_inf_url.format(ip) + '/interface/' + inf
427                 requests.delete(url, headers=self.vpp_api_headers, auth=self.vpp_api_userpass)
428
429             if len(self.get_vxlan_interfaces(ip)) > 0:
430                 self.logger.error("Error deleting VxLAN from {0}, try to restart the VM, IP is {1}.".format(host, ip))
431                 return False
432
433             if not deleted:
434                 self.logger.info("{0}: no VxLAN interface found, nothing to delete".format(host))
435         return True
436
437     @staticmethod
438     def save_object(obj, filepathname):
439         with open(filepathname, 'wb') as fout:
440             pickle.dump(obj, fout)
441
442     @staticmethod
443     def load_object(filepathname):
444         with open(filepathname, 'rb') as fin:
445             return pickle.load(fin)
446
447     @staticmethod
448     def increase_ip_address_or_vni_in_template(vnf_template_file, vnf_parameter_name_list):
449         with open(vnf_template_file) as json_input:
450             json_data = json.load(json_input)
451             param_list = json_data['VNF-API:input']['VNF-API:vnf-topology-information']['VNF-API:vnf-parameters']
452             for param in param_list:
453                 if param['vnf-parameter-name'] in vnf_parameter_name_list:
454                     ipaddr_or_vni = param['vnf-parameter-value'].split('.')
455                     number = int(ipaddr_or_vni[-1])
456                     if 254 == number:
457                         number = 10
458                     else:
459                         number = number + 1
460                     ipaddr_or_vni[-1] = str(number)
461                     param['vnf-parameter-value'] = '.'.join(ipaddr_or_vni)
462
463         assert json_data is not None
464         with open(vnf_template_file, 'w') as json_output:
465             json.dump(json_data, json_output, indent=4, sort_keys=True)
466
467     def save_preload_data(self, preload_data):
468         self.save_object(preload_data, self.preload_dict_file)
469
470     def load_preload_data(self):
471         return self.load_object(self.preload_dict_file)
472
473     def save_vgmux_vnf_name(self, vgmux_vnf_name):
474         self.save_object(vgmux_vnf_name, self.vgmux_vnf_name_file)
475
476     def load_vgmux_vnf_name(self):
477         return self.load_object(self.vgmux_vnf_name_file)
478