50035638772a6e1f491a491e18d4f625de59d62d
[apex.git] / apex / undercloud / undercloud.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 libvirt
11 import logging
12 import os
13 import platform
14 import shutil
15 import subprocess
16 import time
17
18 from apex.virtual import utils as virt_utils
19 from apex.virtual import configure_vm as vm_lib
20 from apex.common import constants
21 from apex.common import utils
22
23
24 class ApexUndercloudException(Exception):
25     pass
26
27
28 class Undercloud:
29     """
30     This class represents an Apex Undercloud VM
31     """
32     def __init__(self, image_path, template_path,
33                  root_pw=None, external_network=False):
34         self.ip = None
35         self.root_pw = root_pw
36         self.external_net = external_network
37         self.volume = os.path.join(constants.LIBVIRT_VOLUME_PATH,
38                                    'undercloud.qcow2')
39         self.image_path = image_path
40         self.template_path = template_path
41         self.vm = None
42         if Undercloud._get_vm():
43             logging.error("Undercloud VM already exists.  Please clean "
44                           "before creating")
45             raise ApexUndercloudException("Undercloud VM already exists!")
46         self.create()
47
48     @staticmethod
49     def _get_vm():
50         conn = libvirt.open('qemu:///system')
51         try:
52             vm = conn.lookupByName('undercloud')
53             return vm
54         except libvirt.libvirtError:
55             logging.debug("No undercloud VM exists")
56
57     def create(self):
58         networks = ['admin']
59         if self.external_net:
60             networks.append('external')
61         console = 'ttyAMA0' if platform.machine() == 'aarch64' else 'ttyS0'
62
63         self.vm = vm_lib.create_vm(name='undercloud',
64                                    image=self.volume,
65                                    baremetal_interfaces=networks,
66                                    direct_boot='overcloud-full',
67                                    kernel_args=['console={}'.format(console),
68                                                 'root=/dev/sda'],
69                                    default_network=True,
70                                    template_dir=self.template_path)
71         self.setup_volumes()
72         self.inject_auth()
73
74     def _set_ip(self):
75         ip_out = self.vm.interfaceAddresses(
76             libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0)
77         if ip_out:
78             for (name, val) in ip_out.items():
79                 for ipaddr in val['addrs']:
80                     if ipaddr['type'] == libvirt.VIR_IP_ADDR_TYPE_IPV4:
81                         self.ip = ipaddr['addr']
82                         return True
83
84     def start(self):
85         """
86         Start Undercloud VM
87         :return: None
88         """
89         if self.vm.isActive():
90             logging.info("Undercloud already started")
91         else:
92             logging.info("Starting undercloud")
93             self.vm.create()
94             # give 10 seconds to come up
95             time.sleep(10)
96         # set IP
97         for x in range(5):
98             if self._set_ip():
99                 logging.info("Undercloud started.  IP Address: {}".format(
100                     self.ip))
101                 break
102             logging.debug("Did not find undercloud IP in {} "
103                           "attempts...".format(x))
104             time.sleep(10)
105         else:
106             logging.error("Cannot find IP for Undercloud")
107             raise ApexUndercloudException(
108                 "Unable to find IP for undercloud.  Check if VM booted "
109                 "correctly")
110
111     def configure(self, net_settings, playbook, apex_temp_dir):
112         """
113         Configures undercloud VM
114         :param net_setings: Network settings for deployment
115         :param playbook: playbook to use to configure undercloud
116         :param apex_temp_dir: temporary apex directory to hold configs/logs
117         :return: None
118         """
119
120         logging.info("Configuring Undercloud...")
121         # run ansible
122         ansible_vars = Undercloud.generate_config(net_settings)
123         ansible_vars['apex_temp_dir'] = apex_temp_dir
124         try:
125             utils.run_ansible(ansible_vars, playbook, host=self.ip,
126                               user='stack')
127         except subprocess.CalledProcessError:
128             logging.error(
129                 "Failed to install undercloud..."
130                 "please check log: {}".format(os.path.join(
131                     apex_temp_dir, 'apex-undercloud-install.log')))
132             raise ApexUndercloudException('Failed to install undercloud')
133         logging.info("Undercloud installed!")
134
135     def setup_volumes(self):
136         for img_file in ('overcloud-full.vmlinuz', 'overcloud-full.initrd',
137                          'undercloud.qcow2'):
138             src_img = os.path.join(self.image_path, img_file)
139             dest_img = os.path.join(constants.LIBVIRT_VOLUME_PATH, img_file)
140             if not os.path.isfile(src_img):
141                 raise ApexUndercloudException(
142                     "Required source file does not exist:{}".format(src_img))
143             if os.path.exists(dest_img):
144                 os.remove(dest_img)
145             shutil.copyfile(src_img, dest_img)
146
147         # TODO(trozet):check if resize needed right now size is 50gb
148         # there is a lib called vminspect which has some dependencies and is
149         # not yet available in pip.  Consider switching to this lib later.
150         # execute ansible playbook
151
152     def inject_auth(self):
153         virt_ops = list()
154         # virt-customize keys/pws
155         if self.root_pw:
156             pw_op = "password:{}".format(self.root_pw)
157             virt_ops.append({constants.VIRT_PW: pw_op})
158         # ssh key setup
159         virt_ops.append({constants.VIRT_RUN_CMD:
160                         'mkdir -p /root/.ssh'})
161         virt_ops.append({constants.VIRT_UPLOAD:
162                          '/root/.ssh/id_rsa.pub:/root/.ssh/authorized_keys'})
163         run_cmds = [
164             'chmod 600 /root/.ssh/authorized_keys',
165             'restorecon /root/.ssh/authorized_keys',
166             'cp /root/.ssh/authorized_keys /home/stack/.ssh/',
167             'chown stack:stack /home/stack/.ssh/authorized_keys',
168             'chmod 600 /home/stack/.ssh/authorized_keys'
169         ]
170         for cmd in run_cmds:
171             virt_ops.append({constants.VIRT_RUN_CMD: cmd})
172         virt_utils.virt_customize(virt_ops, self.volume)
173
174     @staticmethod
175     def generate_config(ns):
176         """
177         Generates a dictionary of settings for configuring undercloud
178         :param ns: network settings to derive undercloud settings
179         :return: dictionary of settings
180         """
181
182         ns_admin = ns['networks']['admin']
183         intro_range = ns['apex']['networks']['admin']['introspection_range']
184         config = dict()
185         config['undercloud_config'] = [
186             "enable_ui false",
187             "undercloud_update_packages false",
188             "undercloud_debug false",
189             "undercloud_hostname undercloud.{}".format(ns['dns-domain']),
190             "local_ip {}/{}".format(str(ns_admin['installer_vm']['ip']),
191                                     str(ns_admin['cidr']).split('/')[1]),
192             "network_gateway {}".format(str(ns_admin['installer_vm']['ip'])),
193             "network_cidr {}".format(str(ns_admin['cidr'])),
194             "dhcp_start {}".format(str(ns_admin['dhcp_range'][0])),
195             "dhcp_end {}".format(str(ns_admin['dhcp_range'][1])),
196             "inspection_iprange {}".format(','.join(intro_range))
197         ]
198
199         config['ironic_config'] = [
200             "disk_utils iscsi_verify_attempts 30",
201             "disk_partitioner check_device_max_retries 40"
202         ]
203
204         config['nova_config'] = [
205             "dns_domain {}".format(ns['dns-domain']),
206             "dhcp_domain {}".format(ns['dns-domain'])
207         ]
208
209         config['neutron_config'] = [
210             "dns_domain {}".format(ns['dns-domain']),
211         ]
212         # FIXME(trozet): possible bug here with not using external network
213         ns_external = ns['networks']['external'][0]
214         config['external_network'] = {
215             "vlan": ns_external['installer_vm']['vlan'],
216             "ip": ns_external['installer_vm']['ip'],
217             "prefix": str(ns_external['cidr']).split('/')[1],
218             "enabled": ns_external['enabled']
219         }
220
221         # Check if this is an ARM deployment
222         config['aarch64'] = platform.machine() == 'aarch64'
223
224         return config