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