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