Adds deployment via snapshot
[apex.git] / apex / deployment / snapshot.py
1 ##############################################################################
2 # Copyright (c) 2018 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 import fnmatch
10 import logging
11 import os
12 import pprint
13 import socket
14 import time
15
16 import libvirt
17
18 import apex.common.constants as con
19 from apex.common import exceptions as exc
20 from apex.common import utils
21 from apex.overcloud.node import OvercloudNode
22 import apex.settings.deploy_settings as ds
23
24
25 SNAP_FILE = 'snapshot.properties'
26 CHECKSUM = 'OPNFV_SNAP_SHA512SUM'
27 OVERCLOUD_RC = 'overcloudrc'
28 SSH_KEY = 'id_rsa'
29 OPENSTACK = 'openstack'
30 OPENDAYLIGHT = 'opendaylight'
31 SERVICES = (OPENSTACK, OPENDAYLIGHT)
32
33
34 class SnapshotDeployment:
35     def __init__(self, deploy_settings, snap_cache_dir, fetch=True,
36                  all_in_one=False):
37         self.id_rsa = None
38         self.fetch = fetch
39         ds_opts = deploy_settings['deploy_options']
40         self.os_version = ds_opts['os_version']
41         self.ha_enabled = deploy_settings['global_params']['ha_enabled']
42         if self.ha_enabled:
43             self.ha_ext = 'ha'
44         elif all_in_one:
45             self.ha_ext = 'noha-allinone'
46         else:
47             self.ha_ext = 'noha'
48         self.snap_cache_dir = os.path.join(snap_cache_dir,
49                                            "{}/{}".format(self.os_version,
50                                                           self.ha_ext))
51         self.networks = []
52         self.oc_nodes = []
53         self.properties_url = "{}/apex/{}/{}".format(con.OPNFV_ARTIFACTS,
54                                                      self.os_version,
55                                                      self.ha_ext)
56         self.conn = libvirt.open('qemu:///system')
57         if not self.conn:
58             raise exc.SnapshotDeployException(
59                 'Unable to open libvirt connection')
60         if self.fetch:
61             self.pull_snapshot(self.properties_url, self.snap_cache_dir)
62         else:
63             logging.info('No fetch enabled. Will not attempt to pull latest '
64                          'snapshot')
65         self.deploy_snapshot()
66
67     @staticmethod
68     def pull_snapshot(url_path, snap_cache_dir):
69         """
70         Compare opnfv properties file and download and unpack snapshot if
71         necessary
72         :param url_path: path of latest snap info
73         :param snap_cache_dir: local directory for snap cache
74         :return: None
75         """
76         full_url = os.path.join(url_path, SNAP_FILE)
77         upstream_props = utils.fetch_properties(full_url)
78         logging.debug("Upstream properties are: {}".format(upstream_props))
79         try:
80             upstream_sha = upstream_props[CHECKSUM]
81         except KeyError:
82             logging.error('Unable to find {} for upstream properties: '
83                           '{}'.format(CHECKSUM, upstream_props))
84             raise exc.SnapshotDeployException('Unable to find upstream '
85                                               'properties checksum value')
86         local_prop_file = os.path.join(snap_cache_dir, SNAP_FILE)
87         try:
88             local_props = utils.fetch_properties(local_prop_file)
89             local_sha = local_props[CHECKSUM]
90             pull_snap = local_sha != upstream_sha
91         except (exc.FetchException, KeyError):
92             logging.info("No locally cached properties found, will pull "
93                          "latest")
94             local_sha = None
95             pull_snap = True
96         logging.debug('Local sha: {}, Upstream sha: {}'.format(local_sha,
97                                                                upstream_sha))
98         if pull_snap:
99             logging.info('SHA mismatch, will download latest snapshot')
100             full_snap_url = upstream_props['OPNFV_SNAP_URL']
101             snap_file = os.path.basename(full_snap_url)
102             snap_url = full_snap_url.replace(snap_file, '')
103             if not snap_url.startswith('http://'):
104                 snap_url = 'http://' + snap_url
105             utils.fetch_upstream_and_unpack(dest=snap_cache_dir,
106                                             url=snap_url,
107                                             targets=[SNAP_FILE, snap_file]
108                                             )
109         else:
110             logging.info('SHA match, artifacts in cache are already latest. '
111                          'Will not download.')
112
113     def create_networks(self):
114         logging.info("Detecting snapshot networks")
115         try:
116             xmls = fnmatch.filter(os.listdir(self.snap_cache_dir), '*.xml')
117         except FileNotFoundError:
118             raise exc.SnapshotDeployException(
119                 'No XML files found in snap cache directory: {}'.format(
120                     self.snap_cache_dir))
121         net_xmls = list()
122         for xml in xmls:
123             if xml.startswith('baremetal'):
124                 continue
125             net_xmls.append(os.path.join(self.snap_cache_dir, xml))
126         if not net_xmls:
127             raise exc.SnapshotDeployException(
128                 'No network XML files detected in snap cache, '
129                 'please check local snap cache contents')
130         logging.info('Snapshot networks found: {}'.format(net_xmls))
131         for xml in net_xmls:
132             logging.debug('Creating network from {}'.format(xml))
133             with open(xml, 'r') as fh:
134                 net_xml = fh.read()
135             net = self.conn.networkCreateXML(net_xml)
136             self.networks.append(net)
137             logging.info('Network started: {}'.format(net.name()))
138
139     def parse_and_create_nodes(self):
140         """
141         Parse snapshot node.yaml config file and create overcloud nodes
142         :return: None
143         """
144         node_file = os.path.join(self.snap_cache_dir, 'node.yaml')
145         if not os.path.isfile(node_file):
146             raise exc.SnapshotDeployException('Missing node definitions from '
147                                               ''.format(node_file))
148         node_data = utils.parse_yaml(node_file)
149         if 'servers' not in node_data:
150             raise exc.SnapshotDeployException('Invalid node.yaml format')
151         for node, data in node_data['servers'].items():
152             logging.info('Creating node: {}'.format(node))
153             logging.debug('Node data is:\n{}'.format(pprint.pformat(data)))
154             node_xml = os.path.join(self.snap_cache_dir,
155                                     '{}.xml'.format(data['vNode-name']))
156             node_qcow = os.path.join(self.snap_cache_dir,
157                                      '{}.qcow2'.format(data['vNode-name']))
158             self.oc_nodes.append(
159                 OvercloudNode(ip=data['address'],
160                               ovs_ctrlrs=data['ovs-controller'],
161                               ovs_mgrs=data['ovs-managers'],
162                               role=data['type'],
163                               name=node,
164                               node_xml=node_xml,
165                               disk_img=node_qcow)
166             )
167             logging.info('Node Created')
168         logging.info('Starting nodes')
169         for node in self.oc_nodes:
170             node.start()
171
172     def get_controllers(self):
173         controllers = []
174         for node in self.oc_nodes:
175             if node.role == 'controller':
176                 controllers.append(node)
177         return controllers
178
179     def is_service_up(self, service):
180         assert service in SERVICES
181         if service == OPENSTACK:
182             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
183             sock.settimeout(5)
184         controllers = self.get_controllers()
185         if not controllers:
186             raise exc.SnapshotDeployException('No OpenStack controllers found')
187
188         for node in controllers:
189             logging.info('Waiting until {} is up on controller: '
190                          '{}'.format(service, node.name))
191             for x in range(10):
192                 logging.debug('Checking {} is up attempt {}'.format(service,
193                               str(x + 1)))
194                 if service == OPENSTACK:
195                     # Check if Neutron is up
196                     if sock.connect_ex((node.ip, 9696)) == 0:
197                         logging.info('{} is up on controller {}'.format(
198                                      service, node.name))
199                         break
200                 elif service == OPENDAYLIGHT:
201                     url = 'http://{}:8081/diagstatus'.format(node.ip)
202                     try:
203                         utils.open_webpage(url)
204                         logging.info('{} is up on controller {}'.format(
205                                      service, node.name))
206                         break
207                     except Exception as e:
208                         logging.debug('Cannot contact ODL. Reason: '
209                                       '{}'.format(e))
210                 time.sleep(60)
211             else:
212                 logging.error('{} is not running after 10 attempts'.format(
213                     service))
214                 return False
215         return True
216
217     def deploy_snapshot(self):
218         # bring up networks
219         self.create_networks()
220         # check overcloudrc exists, id_rsa
221         for snap_file in (OVERCLOUD_RC, SSH_KEY):
222             if not os.path.isfile(os.path.join(self.snap_cache_dir,
223                                                snap_file)):
224                 logging.warning('File is missing form snap cache: '
225                                 '{}'.format(snap_file))
226         # create nodes
227         self.parse_and_create_nodes()
228         # validate deployment
229         if self.is_service_up(OPENSTACK):
230             logging.info('OpenStack is up')
231         else:
232             raise exc.SnapshotDeployException('OpenStack is not alive')
233         if self.is_service_up(OPENDAYLIGHT):
234             logging.info('OpenDaylight is up')
235         else:
236             raise exc.SnapshotDeployException(
237                 'OpenDaylight {} is not reporting diag status')
238         # TODO(trozet): recreate external network/subnet if missing
239         logging.info('Snapshot deployment complete. Please use the {} file '
240                      'in {} to interact with '
241                      'OpenStack'.format(OVERCLOUD_RC, self.snap_cache_dir))