1 ##############################################################################
2 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
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 ##############################################################################
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
25 class ApexUndercloudException(Exception):
31 This class represents an Apex Undercloud VM
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):
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,
43 self.image_path = image_path
44 self.image_name = image_name
45 self.template_path = template_path
47 if Undercloud._get_vm():
48 logging.error("Undercloud VM already exists. Please clean "
50 raise ApexUndercloudException("Undercloud VM already exists!")
55 conn = libvirt.open('qemu:///system')
57 vm = conn.lookupByName('undercloud')
59 except libvirt.libvirtError:
60 logging.debug("No undercloud VM exists")
65 networks.append('external')
66 console = 'ttyAMA0' if platform.machine() == 'aarch64' else 'ttyS0'
67 root = 'vda2' if platform.machine() == 'aarch64' else 'sda'
69 self.vm = vm_lib.create_vm(name='undercloud',
71 baremetal_interfaces=networks,
72 direct_boot='overcloud-full',
73 kernel_args=['console={}'.format(console),
74 'root=/dev/{}'.format(root)],
76 template_dir=self.template_path,
83 ip_out = vm.interfaceAddresses(
84 libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0)
86 for (name, val) in ip_out.items():
87 for ipaddr in val['addrs']:
88 if ipaddr['type'] == libvirt.VIR_IP_ADDR_TYPE_IPV4:
92 ip = self._get_ip(self.vm)
99 vm = Undercloud._get_vm()
100 return Undercloud._get_ip(vm)
107 if self.vm.isActive():
108 logging.info("Undercloud already started")
110 logging.info("Starting undercloud")
112 # give 10 seconds to come up
117 logging.info("Undercloud started. IP Address: {}".format(
120 logging.debug("Did not find undercloud IP in {} "
121 "attempts...".format(x))
124 logging.error("Cannot find IP for Undercloud")
125 raise ApexUndercloudException(
126 "Unable to find IP for undercloud. Check if VM booted "
129 def detect_nat(self, net_settings):
130 if self.external_net:
131 net = net_settings['networks'][constants.EXTERNAL_NETWORK][0]
133 net = net_settings['networks'][constants.ADMIN_NETWORK]
134 if net['gateway'] == net['installer_vm']['ip']:
139 def configure(self, net_settings, deploy_settings,
140 playbook, apex_temp_dir, virtual_oc=False):
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
151 logging.info("Configuring Undercloud...")
153 ansible_vars = Undercloud.generate_config(net_settings,
155 ansible_vars['apex_temp_dir'] = apex_temp_dir
157 ansible_vars['nat'] = self.detect_nat(net_settings)
158 ansible_vars['container_client'] = utils.find_container_client(
161 utils.run_ansible(ansible_vars, playbook, host=self.ip,
163 except subprocess.CalledProcessError:
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!")
171 def setup_volumes(self):
172 for img_file in ('overcloud-full.vmlinuz', 'overcloud-full.initrd',
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,
179 dest_img = os.path.join(constants.LIBVIRT_VOLUME_PATH,
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):
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()
191 shutil.chown(dest_img, user='qemu', group='qemu')
192 os.chmod(dest_img, 0o0744)
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)
201 def inject_auth(self):
203 # virt-customize keys/pws
205 pw_op = "password:{}".format(self.root_pw)
206 virt_ops.append({constants.VIRT_PW: pw_op})
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'})
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'
225 virt_ops.append({constants.VIRT_RUN_CMD: cmd})
226 virt_utils.virt_customize(virt_ops, self.volume)
229 def generate_config(ns, ds):
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
237 ns_admin = ns['networks']['admin']
238 intro_range = ns['apex']['networks']['admin']['introspection_range']
240 # Check if this is an ARM deployment
241 config['aarch64'] = platform.machine() == 'aarch64'
242 # Configuration for undercloud.conf
243 config['undercloud_config'] = [
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"
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)),
269 config['ironic_config'] = [
270 "disk_utils iscsi_verify_attempts 30",
271 "disk_partitioner check_device_max_retries 40"
274 config['nova_config'] = [
275 "dns_domain {}".format(ns['dns-domain']),
276 "dhcp_domain {}".format(ns['dns-domain'])
279 config['neutron_config'] = [
280 "dns_domain {}".format(ns['dns-domain']),
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']
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']
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
302 config['nat_network_ipv6'] = False
303 config['http_proxy'] = ns.get('http_proxy', '')
304 config['https_proxy'] = ns.get('https_proxy', '')
308 def _update_delorean_repo(self):
309 if utils.internet_connectivity():
310 logging.info('Updating delorean repo on Undercloud')
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))
317 virt_utils.virt_customize([{constants.VIRT_RUN_CMD: cmd}],
320 logging.warning("Failed to download and update delorean repo "