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