20fb4a609a50c853166b804a13942c5fa95d9dbd
[apex.git] / apex / overcloud / overcloud_deploy.py
1 ##############################################################################
2 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10 import base64
11 import fileinput
12 import logging
13 import os
14 import re
15 import shutil
16 import uuid
17 import struct
18 import time
19
20 from apex.common import constants as con
21 from apex.common.exceptions import ApexDeployException
22 from apex.common import parsers
23 from apex.virtual import virtual_utils as virt_utils
24 from cryptography.hazmat.primitives import serialization as \
25     crypto_serialization
26 from cryptography.hazmat.primitives.asymmetric import rsa
27 from cryptography.hazmat.backends import default_backend as \
28     crypto_default_backend
29
30
31 SDN_FILE_MAP = {
32     'opendaylight': {
33         'sfc': 'neutron-sfc-opendaylight.yaml',
34         'vpn': 'neutron-bgpvpn-opendaylight.yaml',
35         'gluon': 'gluon.yaml',
36         'vpp': {
37             'odl_vpp_netvirt': 'neutron-opendaylight-netvirt-vpp.yaml',
38             'default': 'neutron-opendaylight-honeycomb.yaml'
39         },
40         'default': 'neutron-opendaylight.yaml',
41     },
42     'onos': {
43         'sfc': 'neutron-onos-sfc.yaml',
44         'default': 'neutron-onos.yaml'
45     },
46     'ovn': 'neutron-ml2-ovn.yaml',
47     False: {
48         'vpp': 'neutron-ml2-vpp.yaml',
49         'dataplane': ('ovs_dpdk', 'neutron-ovs-dpdk.yaml')
50     }
51 }
52
53 OTHER_FILE_MAP = {
54     'tacker': 'enable_tacker.yaml',
55     'congress': 'enable_congress.yaml',
56     'barometer': 'enable_barometer.yaml',
57     'rt_kvm': 'enable_rt_kvm.yaml'
58 }
59
60 OVS_PERF_MAP = {
61     'HostCpusList': 'dpdk_cores',
62     'NeutronDpdkCoreList': 'pmd_cores',
63     'NeutronDpdkSocketMemory': 'socket_memory',
64     'NeutronDpdkMemoryChannels': 'memory_channels'
65 }
66
67 OVS_NSH_KMOD_RPM = "openvswitch-kmod-2.6.1-1.el7.centos.x86_64.rpm"
68 OVS_NSH_RPM = "openvswitch-2.6.1-1.el7.centos.x86_64.rpm"
69 ODL_NETVIRT_VPP_RPM = "/root/opendaylight-7.0.0-0.1.20170531snap665.el7" \
70                       ".noarch.rpm"
71
72
73 def build_sdn_env_list(ds, sdn_map, env_list=None):
74     if env_list is None:
75         env_list = list()
76     for k, v in sdn_map.items():
77         if ds['sdn_controller'] == k or (k in ds and ds[k] is True):
78             if isinstance(v, dict):
79                 env_list.extend(build_sdn_env_list(ds, v))
80             else:
81                 env_list.append(os.path.join(con.THT_ENV_DIR, v))
82         elif isinstance(v, tuple):
83                 if ds[k] == v[0]:
84                     env_list.append(os.path.join(con.THT_ENV_DIR, v[1]))
85     if len(env_list) == 0:
86         try:
87             env_list.append(os.path.join(
88                 con.THT_ENV_DIR, sdn_map['default']))
89         except KeyError:
90             logging.warning("Unable to find default file for SDN")
91
92     return env_list
93
94
95 def create_deploy_cmd(ds, ns, inv, tmp_dir,
96                       virtual, env_file='opnfv-environment.yaml'):
97
98     logging.info("Creating deployment command")
99     deploy_options = [env_file, 'network-environment.yaml']
100     ds_opts = ds['deploy_options']
101     deploy_options += build_sdn_env_list(ds_opts, SDN_FILE_MAP)
102
103     # TODO(trozet): make sure rt kvm file is in tht dir
104     for k, v in OTHER_FILE_MAP.items():
105         if k in ds_opts and ds_opts[k]:
106             deploy_options.append(os.path.join(con.THT_ENV_DIR, v))
107
108     if ds_opts['ceph']:
109         prep_storage_env(ds, tmp_dir)
110         deploy_options.append(os.path.join(con.THT_ENV_DIR,
111                                            'storage-environment.yaml'))
112     if ds['global_params']['ha_enabled']:
113         deploy_options.append(os.path.join(con.THT_ENV_DIR,
114                                            'puppet-pacemaker.yaml'))
115
116     if virtual:
117         deploy_options.append('virtual-environment.yaml')
118     else:
119         deploy_options.append('baremetal-environment.yaml')
120
121     nodes = inv['nodes']
122     num_control = 0
123     num_compute = 0
124     for node in nodes:
125         if 'profile:control' in node['capabilities']:
126             num_control += 1
127         elif 'profile:compute' in node['capabilities']:
128             num_compute += 1
129         else:
130             # TODO(trozet) do we want to allow capabilities to not exist?
131             logging.error("Every node must include a 'capabilities' key "
132                           "tagged with either 'profile:control' or "
133                           "'profile:compute'")
134             raise ApexDeployException("Node missing capabilities "
135                                       "key: {}".format(node))
136     if num_control == 0 or num_compute == 0:
137         logging.error("Detected 0 control or compute nodes.  Control nodes: "
138                       "{}, compute nodes{}".format(num_control, num_compute))
139         raise ApexDeployException("Invalid number of control or computes")
140     elif num_control > 1 and not ds['global_params']['ha_enabled']:
141         num_control = 1
142     cmd = "openstack overcloud deploy --templates --timeout {} " \
143           "--libvirt-type kvm".format(con.DEPLOY_TIMEOUT)
144     # build cmd env args
145     for option in deploy_options:
146         cmd += " -e {}".format(option)
147     cmd += " --ntp-server {}".format(ns['ntp'][0])
148     cmd += " --control-scale {}".format(num_control)
149     cmd += " --compute-scale {}".format(num_compute)
150     cmd += ' --control-flavor control --compute-flavor compute'
151     logging.info("Deploy command set: {}".format(cmd))
152
153     with open(os.path.join(tmp_dir, 'deploy_command'), 'w') as fh:
154         fh.write(cmd)
155     return cmd
156
157
158 def prep_image(ds, img, tmp_dir, root_pw=None):
159     """
160     Locates sdn image and preps for deployment.
161     :param ds: deploy settings
162     :param img: sdn image
163     :param tmp_dir: dir to store modified sdn image
164     :param root_pw: password to configure for overcloud image
165     :return: None
166     """
167     # TODO(trozet): Come up with a better way to organize this logic in this
168     # function
169     logging.info("Preparing image: {} for deployment".format(img))
170     if not os.path.isfile(img):
171         logging.error("Missing SDN image {}".format(img))
172         raise ApexDeployException("Missing SDN image file: {}".format(img))
173
174     ds_opts = ds['deploy_options']
175     virt_cmds = list()
176     sdn = ds_opts['sdn_controller']
177     # we need this due to rhbz #1436021
178     # fixed in systemd-219-37.el7
179     if sdn is not False:
180         logging.info("Neutron openvswitch-agent disabled")
181         virt_cmds.extend([{
182             con.VIRT_RUN_CMD:
183                 "rm -f /etc/systemd/system/multi-user.target.wants/"
184                 "neutron-openvswitch-agent.service"},
185             {
186             con.VIRT_RUN_CMD:
187                 "rm -f /usr/lib/systemd/system/neutron-openvswitch-agent"
188                 ".service"
189         }])
190
191     if ds_opts['vpn']:
192         virt_cmds.append({con.VIRT_RUN_CMD: "systemctl enable zrpcd"})
193         logging.info("ZRPC and Quagga enabled")
194
195     dataplane = ds_opts['dataplane']
196     if dataplane == 'ovs_dpdk' or dataplane == 'fdio':
197         logging.info("Enabling kernel modules for dpdk")
198         # file to module mapping
199         uio_types = {
200             os.path.join(tmp_dir, 'vfio_pci.modules'): 'vfio_pci',
201             os.path.join(tmp_dir, 'uio_pci_generic.modules'): 'uio_pci_generic'
202         }
203         for mod_file, mod in uio_types.items():
204             with open(mod_file, 'w') as fh:
205                 fh.write('#!/bin/bash\n')
206                 fh.write('exec /sbin/modprobe {}'.format(mod))
207                 fh.close()
208
209             virt_cmds.extend([
210                 {con.VIRT_UPLOAD: "{}:/etc/sysconfig/modules/".format(
211                     mod_file)},
212                 {con.VIRT_RUN_CMD: "chmod 0755 /etc/sysconfig/modules/"
213                                    "{}".format(os.path.basename(mod_file))}
214             ])
215     if root_pw:
216         pw_op = "password:{}".format(root_pw)
217         virt_cmds.append({con.VIRT_PW: pw_op})
218     if ds_opts['sfc'] and dataplane == 'ovs':
219         virt_cmds.extend([
220             {con.VIRT_RUN_CMD: "yum -y install "
221                                "/root/ovs/rpm/rpmbuild/RPMS/x86_64/"
222                                "{}".format(OVS_NSH_KMOD_RPM)},
223             {con.VIRT_RUN_CMD: "yum downgrade -y "
224                                "/root/ovs/rpm/rpmbuild/RPMS/x86_64/"
225                                "{}".format(OVS_NSH_RPM)}
226         ])
227     if dataplane == 'fdio':
228         # Patch neutron with using OVS external interface for router
229         # and add generic linux NS interface driver
230         virt_cmds.append(
231             {con.VIRT_RUN_CMD: "cd /usr/lib/python2.7/site-packages && patch "
232                                "-p1 < neutron-patch-NSDriver.patch"})
233
234     if sdn == 'opendaylight':
235         if ds_opts['odl_version'] != con.DEFAULT_ODL_VERSION:
236             virt_cmds.extend([
237                 {con.VIRT_RUN_CMD: "yum -y remove opendaylight"},
238                 {con.VIRT_RUN_CMD: "yum -y install /root/{}/*".format(
239                     ds_opts['odl_version'])},
240                 {con.VIRT_RUN_CMD: "rm -rf /etc/puppet/modules/opendaylight"},
241                 {con.VIRT_RUN_CMD: "cd /etc/puppet/modules && tar xzf "
242                                    "/root/puppet-opendaylight-"
243                                    "{}.tar.gz".format(ds_opts['odl_version'])}
244             ])
245         elif sdn == 'opendaylight' and 'odl_vpp_netvirt' in ds_opts \
246                 and ds_opts['odl_vpp_netvirt']:
247             virt_cmds.extend([
248                 {con.VIRT_RUN_CMD: "yum -y remove opendaylight"},
249                 {con.VIRT_RUN_CMD: "yum -y install /root/{}/*".format(
250                     ODL_NETVIRT_VPP_RPM)}
251             ])
252
253     if sdn == 'ovn':
254         virt_cmds.extend([
255             {con.VIRT_RUN_CMD: "cd /root/ovs28 && yum update -y "
256                                "*openvswitch*"},
257             {con.VIRT_RUN_CMD: "cd /root/ovs28 && yum downgrade -y "
258                                "*openvswitch*"}
259         ])
260
261     tmp_oc_image = os.path.join(tmp_dir, 'overcloud-full.qcow2')
262     shutil.copyfile(img, tmp_oc_image)
263     logging.debug("Temporary overcloud image stored as: {}".format(
264         tmp_oc_image))
265     virt_utils.virt_customize(virt_cmds, tmp_oc_image)
266     logging.info("Overcloud image customization complete")
267
268
269 def make_ssh_key():
270     """
271     Creates public and private ssh keys with 1024 bit RSA encryption
272     :return: private, public key
273     """
274     key = rsa.generate_private_key(
275         backend=crypto_default_backend(),
276         public_exponent=65537,
277         key_size=1024
278     )
279
280     private_key = key.private_bytes(
281         crypto_serialization.Encoding.PEM,
282         crypto_serialization.PrivateFormat.PKCS8,
283         crypto_serialization.NoEncryption())
284     public_key = key.public_key().public_bytes(
285         crypto_serialization.Encoding.OpenSSH,
286         crypto_serialization.PublicFormat.OpenSSH
287     )
288     pub_key = re.sub('ssh-rsa\s*', '', public_key.decode('utf-8'))
289     return private_key.decode('utf-8'), pub_key
290
291
292 def prep_env(ds, ns, opnfv_env, net_env, tmp_dir):
293     """
294     Creates modified opnfv/network environments for deployment
295     :param ds: deploy settings
296     :param ns: network settings
297     :param opnfv_env: file path for opnfv-environment file
298     :param net_env: file path for network-environment file
299     :param tmp_dir: Apex tmp dir
300     :return:
301     """
302
303     logging.info("Preparing opnfv-environment and network-environment files")
304     ds_opts = ds['deploy_options']
305     tmp_opnfv_env = os.path.join(tmp_dir, os.path.basename(opnfv_env))
306     shutil.copyfile(opnfv_env, tmp_opnfv_env)
307     tenant_nic_map = ns['networks']['tenant']['nic_mapping']
308     tenant_ctrl_nic = tenant_nic_map['controller']['members'][0]
309     tenant_comp_nic = tenant_nic_map['compute']['members'][0]
310
311     # SSH keys
312     private_key, public_key = make_ssh_key()
313
314     # Make easier/faster variables to index in the file editor
315     if 'performance' in ds_opts:
316         perf = True
317         # vpp
318         if 'vpp' in ds_opts['performance']['Compute']:
319             perf_vpp_comp = ds_opts['performance']['Compute']['vpp']
320         else:
321             perf_vpp_comp = None
322         if 'vpp' in ds_opts['performance']['Controller']:
323             perf_vpp_ctrl = ds_opts['performance']['Controller']['vpp']
324         else:
325             perf_vpp_ctrl = None
326
327         # ovs
328         if 'ovs' in ds_opts['performance']['Compute']:
329             perf_ovs_comp = ds_opts['performance']['Compute']['ovs']
330         else:
331             perf_ovs_comp = None
332
333         # kernel
334         if 'kernel' in ds_opts['performance']['Compute']:
335             perf_kern_comp = ds_opts['performance']['Compute']['kernel']
336         else:
337             perf_kern_comp = None
338     else:
339         perf = False
340
341     # Modify OPNFV environment
342     # TODO: Change to build a dict and outputing yaml rather than parsing
343     for line in fileinput.input(tmp_opnfv_env, inplace=True):
344         line = line.strip('\n')
345         output_line = line
346         if 'CloudDomain' in line:
347             output_line = "  CloudDomain: {}".format(ns['domain_name'])
348         elif 'replace_private_key' in line:
349             output_line = "      key: '{}'".format(private_key)
350         elif 'replace_public_key' in line:
351             output_line = "      key: '{}'".format(public_key)
352
353         if ds_opts['sdn_controller'] == 'opendaylight' and \
354                 'odl_vpp_routing_node' in ds_opts and ds_opts[
355                 'odl_vpp_routing_node'] != 'dvr':
356             if 'opendaylight::vpp_routing_node' in line:
357                 output_line = ("    opendaylight::vpp_routing_node: ${}.${}"
358                                .format(ds_opts['odl_vpp_routing_node'],
359                                        ns['domain_name']))
360             elif 'ControllerExtraConfig' in line:
361                 output_line = ("  ControllerExtraConfig:\n    "
362                                "tripleo::profile::base::neutron::agents::"
363                                "honeycomb::interface_role_mapping:"
364                                " ['{}:tenant-interface]'"
365                                .format(tenant_ctrl_nic))
366             elif 'NovaComputeExtraConfig' in line:
367                 output_line = ("  NovaComputeExtraConfig:\n    "
368                                "tripleo::profile::base::neutron::agents::"
369                                "honeycomb::interface_role_mapping:"
370                                " ['{}:tenant-interface]'"
371                                .format(tenant_comp_nic))
372         elif not ds_opts['sdn_controller'] and ds_opts['dataplane'] == 'fdio':
373             if 'NeutronVPPAgentPhysnets' in line:
374                 output_line = ("  NeutronVPPAgentPhysnets: 'datacentre:{}'".
375                                format(tenant_ctrl_nic))
376
377         if perf:
378             for role in 'NovaCompute', 'Controller':
379                 if role == 'NovaCompute':
380                     perf_opts = perf_vpp_comp
381                 else:
382                     perf_opts = perf_vpp_ctrl
383                 cfg = "{}ExtraConfig".format(role)
384                 if cfg in line and perf_opts:
385                     perf_line = ''
386                     if 'main-core' in perf_opts:
387                         perf_line += ("\n    fdio::vpp_cpu_main_core: '{}'"
388                                       .format(perf_opts['main-core']))
389                     if 'corelist-workers' in perf_opts:
390                         perf_line += ("\n    "
391                                       "fdio::vpp_cpu_corelist_workers: '{}'"
392                                       .format(perf_opts['corelist-workers']))
393                     if perf_line:
394                         output_line = ("  {}:{}".format(cfg, perf_line))
395
396             # kernel args
397             # (FIXME) use compute's kernel settings for all nodes for now.
398             if 'ComputeKernelArgs' in line and perf_kern_comp:
399                 kernel_args = ''
400                 for k, v in perf_kern_comp.items():
401                     kernel_args += "{}={} ".format(k, v)
402                 if kernel_args:
403                     output_line = "  ComputeKernelArgs: '{}'".\
404                         format(kernel_args)
405             if ds_opts['dataplane'] == 'ovs_dpdk' and perf_ovs_comp:
406                 for k, v in OVS_PERF_MAP.items():
407                     if k in line and v in perf_ovs_comp:
408                         output_line = "  {}: '{}'".format(k, perf_ovs_comp[v])
409
410         print(output_line)
411
412     logging.info("opnfv-environment file written to {}".format(tmp_opnfv_env))
413
414     # Modify Network environment
415     for line in fileinput.input(net_env, inplace=True):
416         line = line.strip('\n')
417         if 'ComputeExtraConfigPre' in line and \
418                 ds_opts['dataplane'] == 'ovs_dpdk':
419             print('  OS::TripleO::ComputeExtraConfigPre: '
420                   './ovs-dpdk-preconfig.yaml')
421         elif perf and perf_kern_comp:
422             if 'resource_registry' in line:
423                 print("resource_registry:\n"
424                       "  OS::TripleO::NodeUserData: first-boot.yaml")
425             elif 'NovaSchedulerDefaultFilters' in line:
426                 print("  NovaSchedulerDefaultFilters: 'RamFilter,"
427                       "ComputeFilter,AvailabilityZoneFilter,"
428                       "ComputeCapabilitiesFilter,ImagePropertiesFilter,"
429                       "NUMATopologyFilter'")
430             else:
431                 print(line)
432         else:
433             print(line)
434
435     logging.info("network-environment file written to {}".format(net_env))
436
437
438 def generate_ceph_key():
439     key = os.urandom(16)
440     header = struct.pack('<hiih', 1, int(time.time()), 0, len(key))
441     return base64.b64encode(header + key)
442
443
444 def prep_storage_env(ds, tmp_dir):
445     """
446     Creates storage environment file for deployment.  Source file is copied by
447     undercloud playbook to host.
448     :param ds:
449     :param tmp_dir:
450     :return:
451     """
452     ds_opts = ds['deploy_options']
453     storage_file = os.path.join(tmp_dir, 'storage-environment.yaml')
454     if not os.path.isfile(storage_file):
455         logging.error("storage-environment file is not in tmp directory: {}. "
456                       "Check if file was copied from "
457                       "undercloud".format(tmp_dir))
458         raise ApexDeployException("storage-environment file not copied from "
459                                   "undercloud")
460     for line in fileinput.input(storage_file, inplace=True):
461         line = line.strip('\n')
462         if 'CephClusterFSID' in line:
463             print("  CephClusterFSID: {}".format(str(uuid.uuid4())))
464         elif 'CephMonKey' in line:
465             print("  CephMonKey: {}".format(generate_ceph_key().decode(
466                 'utf-8')))
467         elif 'CephAdminKey' in line:
468             print("  CephAdminKey: {}".format(generate_ceph_key().decode(
469                 'utf-8')))
470         else:
471             print(line)
472     if 'ceph_device' in ds_opts and ds_opts['ceph_device']:
473         with open(storage_file, 'a') as fh:
474             fh.write('  ExtraConfig:\n')
475             fh.write("    ceph::profile::params::osds:{{{}:{{}}}}\n".format(
476                 ds_opts['ceph_device']
477             ))
478
479
480 def external_network_cmds(ns):
481     """
482     Generates external network openstack commands
483     :param ns: network settings
484     :return: list of commands to configure external network
485     """
486     if 'external' in ns.enabled_network_list:
487         net_config = ns['networks']['external'][0]
488         external = True
489         pool_start, pool_end = net_config['floating_ip_range']
490     else:
491         net_config = ns['networks']['admin']
492         external = False
493         pool_start, pool_end = ns['apex']['networks']['admin'][
494             'introspection_range']
495     nic_config = net_config['nic_mapping']
496     gateway = net_config['gateway']
497     cmds = list()
498     # create network command
499     if nic_config['compute']['vlan'] == 'native':
500         ext_type = 'flat'
501     else:
502         ext_type = "vlan --provider-segment {}".format(nic_config[
503                                                        'compute']['vlan'])
504     cmds.append("openstack network create external --project service "
505                 "--external --provider-network-type {} "
506                 "--provider-physical-network datacentre".format(ext_type))
507     # create subnet command
508     cidr = net_config['cidr']
509     subnet_cmd = "openstack subnet create external-subnet --project " \
510                  "service --network external --no-dhcp --gateway {} " \
511                  "--allocation-pool start={},end={} --subnet-range " \
512                  "{}".format(gateway, pool_start, pool_end, str(cidr))
513     if external and cidr.version == 6:
514         subnet_cmd += ' --ip-version 6 --ipv6-ra-mode slaac ' \
515                       '--ipv6-address-mode slaac'
516     cmds.append(subnet_cmd)
517     logging.debug("Neutron external network commands determined "
518                   "as: {}".format(cmds))
519     return cmds
520
521
522 def create_congress_cmds(overcloud_file):
523     drivers = ['nova', 'neutronv2', 'cinder', 'glancev2', 'keystone', 'doctor']
524     overcloudrc = parsers.parse_overcloudrc(overcloud_file)
525     logging.info("Creating congress commands")
526     try:
527         ds_cfg = [
528             "username={}".format(overcloudrc['OS_USERNAME']),
529             "tenant_name={}".format(overcloudrc['OS_PROJECT_NAME']),
530             "password={}".format(overcloudrc['OS_PASSWORD']),
531             "auth_url={}".format(overcloudrc['OS_AUTH_URL'])
532         ]
533     except KeyError:
534         logging.error("Unable to find all keys required for congress in "
535                       "overcloudrc: OS_USERNAME, OS_PROJECT_NAME, "
536                       "OS_PASSWORD, OS_AUTH_URL.  Please check overcloudrc "
537                       "file: {}".format(overcloud_file))
538         raise
539     cmds = list()
540     ds_cfg = '--config ' + ' --config '.join(ds_cfg)
541
542     for driver in drivers:
543         if driver == 'doctor':
544             cmd = "{} \"{}\"".format(driver, driver)
545         else:
546             cmd = "{} \"{}\" {}".format(driver, driver, ds_cfg)
547         if driver == 'nova':
548             cmd += ' --config api_version="2.34"'
549         logging.debug("Congress command created: {}".format(cmd))
550         cmds.append(cmd)
551     return cmds