Fixes undercloud install failure with setting hostname
[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.builders import undercloud_builder as uc_builder
19 from apex.virtual import utils as virt_utils
20 from apex.virtual import configure_vm as vm_lib
21 from apex.common import constants
22 from apex.common import utils
23
24
25 class ApexUndercloudException(Exception):
26     pass
27
28
29 class Undercloud:
30     """
31     This class represents an Apex Undercloud VM
32     """
33     def __init__(self, image_path, template_path,
34                  root_pw=None, external_network=False,
35                  image_name='undercloud.qcow2',
36                  os_version=constants.DEFAULT_OS_VERSION):
37         self.ip = None
38         self.os_version = os_version
39         self.root_pw = root_pw
40         self.external_net = external_network
41         self.volume = os.path.join(constants.LIBVIRT_VOLUME_PATH,
42                                    'undercloud.qcow2')
43         self.image_path = image_path
44         self.image_name = image_name
45         self.template_path = template_path
46         self.vm = None
47         if Undercloud._get_vm():
48             logging.error("Undercloud VM already exists.  Please clean "
49                           "before creating")
50             raise ApexUndercloudException("Undercloud VM already exists!")
51         self.create()
52
53     @staticmethod
54     def _get_vm():
55         conn = libvirt.open('qemu:///system')
56         try:
57             vm = conn.lookupByName('undercloud')
58             return vm
59         except libvirt.libvirtError:
60             logging.debug("No undercloud VM exists")
61
62     def create(self):
63         networks = ['admin']
64         if self.external_net:
65             networks.append('external')
66         console = 'ttyAMA0' if platform.machine() == 'aarch64' else 'ttyS0'
67         root = 'vda2' if platform.machine() == 'aarch64' else 'sda'
68
69         self.vm = vm_lib.create_vm(name='undercloud',
70                                    image=self.volume,
71                                    baremetal_interfaces=networks,
72                                    direct_boot='overcloud-full',
73                                    kernel_args=['console={}'.format(console),
74                                                 'root=/dev/{}'.format(root)],
75                                    default_network=True,
76                                    template_dir=self.template_path,
77                                    memory=10240)
78         self.setup_volumes()
79         self.inject_auth()
80
81     @staticmethod
82     def _get_ip(vm):
83         ip_out = vm.interfaceAddresses(
84             libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0)
85         if ip_out:
86             for (name, val) in ip_out.items():
87                 for ipaddr in val['addrs']:
88                     if ipaddr['type'] == libvirt.VIR_IP_ADDR_TYPE_IPV4:
89                         return ipaddr['addr']
90
91     def _set_ip(self):
92         ip = self._get_ip(self.vm)
93         if ip:
94             self.ip = ip
95             return True
96
97     @staticmethod
98     def get_ip():
99         vm = Undercloud._get_vm()
100         return Undercloud._get_ip(vm)
101
102     def start(self):
103         """
104         Start Undercloud VM
105         :return: None
106         """
107         if self.vm.isActive():
108             logging.info("Undercloud already started")
109         else:
110             logging.info("Starting undercloud")
111             self.vm.create()
112             # give 10 seconds to come up
113             time.sleep(10)
114         # set IP
115         for x in range(10):
116             if self._set_ip():
117                 logging.info("Undercloud started.  IP Address: {}".format(
118                     self.ip))
119                 break
120             logging.debug("Did not find undercloud IP in {} "
121                           "attempts...".format(x))
122             time.sleep(10)
123         else:
124             logging.error("Cannot find IP for Undercloud")
125             raise ApexUndercloudException(
126                 "Unable to find IP for undercloud.  Check if VM booted "
127                 "correctly")
128
129     def detect_nat(self, net_settings):
130         if self.external_net:
131             net = net_settings['networks'][constants.EXTERNAL_NETWORK][0]
132         else:
133             net = net_settings['networks'][constants.ADMIN_NETWORK]
134         if net['gateway'] == net['installer_vm']['ip']:
135             return True
136         else:
137             return False
138
139     def configure(self, net_settings, deploy_settings,
140                   playbook, apex_temp_dir, virtual_oc=False):
141         """
142         Configures undercloud VM
143         :param net_settings: Network settings for deployment
144         :param deploy_settings: Deployment settings for deployment
145         :param playbook: playbook to use to configure undercloud
146         :param apex_temp_dir: temporary apex directory to hold configs/logs
147         :param virtual_oc: Boolean to determine if overcloud is virt
148         :return: None
149         """
150
151         logging.info("Configuring Undercloud...")
152         # run ansible
153         ansible_vars = Undercloud.generate_config(net_settings,
154                                                   deploy_settings)
155         ansible_vars['apex_temp_dir'] = apex_temp_dir
156
157         ansible_vars['nat'] = self.detect_nat(net_settings)
158         ansible_vars['container_client'] = utils.find_container_client(
159             self.os_version)
160         try:
161             utils.run_ansible(ansible_vars, playbook, host=self.ip,
162                               user='stack')
163         except subprocess.CalledProcessError:
164             logging.error(
165                 "Failed to install undercloud..."
166                 "please check log: {}".format(os.path.join(
167                     apex_temp_dir, 'apex-undercloud-install.log')))
168             raise ApexUndercloudException('Failed to install undercloud')
169         logging.info("Undercloud installed!")
170
171     def setup_volumes(self):
172         for img_file in ('overcloud-full.vmlinuz', 'overcloud-full.initrd',
173                          self.image_name):
174             src_img = os.path.join(self.image_path, img_file)
175             if img_file == self.image_name:
176                 dest_img = os.path.join(constants.LIBVIRT_VOLUME_PATH,
177                                         'undercloud.qcow2')
178             else:
179                 dest_img = os.path.join(constants.LIBVIRT_VOLUME_PATH,
180                                         img_file)
181             if not os.path.isfile(src_img):
182                 raise ApexUndercloudException(
183                     "Required source file does not exist:{}".format(src_img))
184             if os.path.exists(dest_img):
185                 os.remove(dest_img)
186             shutil.copyfile(src_img, dest_img)
187             if img_file == self.image_name and platform.machine() != 'aarch64':
188                 uc_builder.expand_disk(dest_img)
189                 self.expand_root_fs()
190
191             shutil.chown(dest_img, user='qemu', group='qemu')
192             os.chmod(dest_img, 0o0744)
193
194     def expand_root_fs(self):
195         # there is a lib called vminspect which has some dependencies and is
196         # not yet available in pip.  Consider switching to this lib later.
197         logging.debug("Expanding root filesystem on /dev/sda partition")
198         virt_ops = [{constants.VIRT_RUN_CMD: 'xfs_growfs /dev/sda'}]
199         virt_utils.virt_customize(virt_ops, self.volume)
200
201     def inject_auth(self):
202         virt_ops = list()
203         # virt-customize keys/pws
204         if self.root_pw:
205             pw_op = "password:{}".format(self.root_pw)
206             virt_ops.append({constants.VIRT_PW: pw_op})
207         # ssh key setup
208         virt_ops.append({constants.VIRT_RUN_CMD:
209                         'mkdir -p /root/.ssh'})
210         virt_ops.append({constants.VIRT_UPLOAD:
211                          '/root/.ssh/id_rsa.pub:/root/.ssh/authorized_keys'})
212         run_cmds = [
213             'chmod 600 /root/.ssh/authorized_keys',
214             'restorecon -R -v /root/.ssh',
215             'id -u stack || useradd -m stack',
216             'mkdir -p /home/stack/.ssh',
217             'chown stack:stack /home/stack/.ssh',
218             'cp /root/.ssh/authorized_keys /home/stack/.ssh/',
219             'chown stack:stack /home/stack/.ssh/authorized_keys',
220             'chmod 600 /home/stack/.ssh/authorized_keys',
221             'echo "stack       ALL = (ALL) NOPASSWD: ALL" >> /etc/sudoers',
222             'touch /etc/cloud/cloud-init.disabled'
223         ]
224         for cmd in run_cmds:
225             virt_ops.append({constants.VIRT_RUN_CMD: cmd})
226         virt_utils.virt_customize(virt_ops, self.volume)
227
228     @staticmethod
229     def generate_config(ns, ds):
230         """
231         Generates a dictionary of settings for configuring undercloud
232         :param ns: network settings to derive undercloud settings
233         :param ds: deploy settings to derive undercloud settings
234         :return: dictionary of settings
235         """
236
237         ns_admin = ns['networks']['admin']
238         intro_range = ns['apex']['networks']['admin']['introspection_range']
239         config = dict()
240         # Check if this is an ARM deployment
241         config['aarch64'] = platform.machine() == 'aarch64'
242         # Configuration for undercloud.conf
243         config['undercloud_config'] = [
244             "enable_ui false",
245             "undercloud_update_packages false",
246             "undercloud_debug false",
247             "inspection_extras false",
248             "ipxe_enabled {}".format(
249                 str(ds['global_params'].get('ipxe', True) and
250                     not config['aarch64'])),
251             "undercloud_hostname undercloud.{}".format(ns['dns-domain']),
252             "local_ip {}/{}".format(str(ns_admin['installer_vm']['ip']),
253                                     str(ns_admin['cidr']).split('/')[1]),
254             "generate_service_certificate false",
255             "undercloud_ntp_servers {}".format(str(ns['ntp'][0])),
256             "container_images_file "
257             "/home/stack/containers-prepare-parameter.yaml",
258             "undercloud_enable_selinux false"
259         ]
260
261         config['undercloud_network_config'] = [
262             "gateway {}".format(str(ns_admin['installer_vm']['ip'])),
263             "cidr {}".format(str(ns_admin['cidr'])),
264             "dhcp_start {}".format(str(ns_admin['dhcp_range'][0])),
265             "dhcp_end {}".format(str(ns_admin['dhcp_range'][1])),
266             "inspection_iprange {}".format(','.join(intro_range)),
267         ]
268
269         config['ironic_config'] = [
270             "disk_utils iscsi_verify_attempts 30",
271             "disk_partitioner check_device_max_retries 40"
272         ]
273
274         config['nova_config'] = [
275             "dns_domain {}".format(ns['dns-domain']),
276             "dhcp_domain {}".format(ns['dns-domain'])
277         ]
278
279         config['neutron_config'] = [
280             "dns_domain {}".format(ns['dns-domain']),
281         ]
282         # FIXME(trozet): possible bug here with not using external network
283         ns_external = ns['networks']['external'][0]
284         config['external_network'] = {
285             "vlan": ns_external['installer_vm']['vlan'],
286             "ip": ns_external['installer_vm']['ip'],
287             "prefix": str(ns_external['cidr']).split('/')[1],
288             "enabled": ns_external['enabled']
289         }
290         # We will NAT external network if it is enabled. If external network
291         # is IPv6, we will NAT admin network in case we need IPv4 connectivity
292         # for things like DNS server.
293         if 'external' in ns.enabled_network_list and \
294                 ns_external['cidr'].version == 4:
295             nat_cidr = ns_external['cidr']
296         else:
297             nat_cidr = ns['networks']['admin']['cidr']
298         config['nat_cidr'] = str(nat_cidr)
299         if nat_cidr.version == 6:
300             config['nat_network_ipv6'] = True
301         else:
302             config['nat_network_ipv6'] = False
303         config['http_proxy'] = ns.get('http_proxy', '')
304         config['https_proxy'] = ns.get('https_proxy', '')
305
306         return config
307
308     def _update_delorean_repo(self):
309         if utils.internet_connectivity():
310             logging.info('Updating delorean repo on Undercloud')
311             delorean_repo = (
312                 "https://trunk.rdoproject.org/centos7-{}"
313                 "/current-tripleo/delorean.repo".format(self.os_version))
314             cmd = ("curl -L -f -o "
315                    "/etc/yum.repos.d/deloran.repo {}".format(delorean_repo))
316             try:
317                 virt_utils.virt_customize([{constants.VIRT_RUN_CMD: cmd}],
318                                           self.volume)
319             except Exception:
320                 logging.warning("Failed to download and update delorean repo "
321                                 "for Undercloud")