Fixes stale undercloud delorean repos
[apex.git] / apex / common / utils.py
1 ##############################################################################
2 # Copyright (c) 2016 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 datetime
11 import distro
12 import json
13 import logging
14 import os
15 import pprint
16 import socket
17 import subprocess
18 import tarfile
19 import time
20 import urllib.error
21 import urllib.request
22 import urllib.parse
23 import yaml
24
25
26 def str2bool(var):
27     if isinstance(var, bool):
28         return var
29     else:
30         return var.lower() in ("true", "yes")
31
32
33 def parse_yaml(yaml_file):
34     with open(yaml_file) as f:
35         parsed_dict = yaml.safe_load(f)
36         return parsed_dict
37
38
39 def dump_yaml(data, file):
40     """
41     Dumps data to a file as yaml
42     :param data: yaml to be written to file
43     :param file: filename to write to
44     :return:
45     """
46     logging.debug("Writing file {} with "
47                   "yaml data:\n{}".format(file, yaml.safe_dump(data)))
48     with open(file, "w") as fh:
49         yaml.safe_dump(data, fh, default_flow_style=False)
50
51
52 def dict_objects_to_str(dictionary):
53         if isinstance(dictionary, list):
54             tmp_list = []
55             for element in dictionary:
56                 if isinstance(element, dict):
57                     tmp_list.append(dict_objects_to_str(element))
58                 else:
59                     tmp_list.append(str(element))
60             return tmp_list
61         elif not isinstance(dictionary, dict):
62             if not isinstance(dictionary, bool):
63                 return str(dictionary)
64             else:
65                 return dictionary
66         return dict((k, dict_objects_to_str(v)) for
67                     k, v in dictionary.items())
68
69
70 def run_ansible(ansible_vars, playbook, host='localhost', user='root',
71                 tmp_dir=None, dry_run=False):
72     """
73     Executes ansible playbook and checks for errors
74     :param ansible_vars: dictionary of variables to inject into ansible run
75     :param playbook: playbook to execute
76     :param tmp_dir: temp directory to store ansible command
77     :param dry_run: Do not actually apply changes
78     :return: None
79     """
80     logging.info("Executing ansible playbook: {}".format(playbook))
81     inv_host = "{},".format(host)
82     if host == 'localhost':
83         conn_type = 'local'
84     else:
85         conn_type = 'smart'
86     ansible_command = ['ansible-playbook', '--become', '-i', inv_host,
87                        '-u', user, '-c', conn_type, '-T', '30',
88                        playbook, '-vv']
89     if dry_run:
90         ansible_command.append('--check')
91
92     if isinstance(ansible_vars, dict) and ansible_vars:
93         logging.debug("Ansible variables to be set:\n{}".format(
94             pprint.pformat(ansible_vars)))
95         ansible_command.append('--extra-vars')
96         ansible_command.append(json.dumps(ansible_vars))
97         if tmp_dir:
98             ansible_tmp = os.path.join(tmp_dir,
99                                        os.path.basename(playbook) + '.rerun')
100             # FIXME(trozet): extra vars are printed without single quotes
101             # so a dev has to add them manually to the command to rerun
102             # the playbook.  Need to test if we can just add the single quotes
103             # to the json dumps to the ansible command and see if that works
104             with open(ansible_tmp, 'w') as fh:
105                 fh.write("ANSIBLE_HOST_KEY_CHECKING=FALSE {}".format(
106                     ' '.join(ansible_command)))
107
108     my_env = os.environ.copy()
109     my_env['ANSIBLE_HOST_KEY_CHECKING'] = 'False'
110     logging.info("Executing playbook...this may take some time")
111     p = subprocess.Popen(ansible_command,
112                          stdin=subprocess.PIPE,
113                          stdout=subprocess.PIPE,
114                          bufsize=1,
115                          env=my_env,
116                          universal_newlines=True)
117     # read first line
118     x = p.stdout.readline()
119     # initialize task
120     task = ''
121     while x:
122         # append lines to task
123         task += x
124         # log the line and read another
125         x = p.stdout.readline()
126         # deliver the task to info when we get a blank line
127         if not x.strip():
128             task += x
129             logging.info(task.replace('\\n', '\n'))
130             task = ''
131             x = p.stdout.readline()
132     # clean up and get return code
133     p.stdout.close()
134     rc = p.wait()
135     if rc:
136         # raise errors
137         e = "Ansible playbook failed. See Ansible logs for details."
138         logging.error(e)
139         raise Exception(e)
140
141
142 def fetch_upstream_and_unpack(dest, url, targets):
143     """
144     Fetches targets from a url destination and downloads them if they are
145     newer.  Also unpacks tar files in dest dir.
146     :param dest: Directory to download and unpack files to
147     :param url: URL where target files are located
148     :param targets: List of target files to download
149     :return: None
150     """
151     os.makedirs(dest, exist_ok=True)
152     assert isinstance(targets, list)
153     for target in targets:
154         download_target = True
155         target_url = urllib.parse.urljoin(url, target)
156         target_dest = os.path.join(dest, target)
157         logging.debug("Fetching and comparing upstream target: \n{}".format(
158             target_url))
159         try:
160             u = urllib.request.urlopen(target_url)
161         except urllib.error.URLError as e:
162             logging.error("Failed to fetch target url. Error: {}".format(
163                 e.reason))
164             raise
165         if os.path.isfile(target_dest):
166             logging.debug("Previous file found: {}".format(target_dest))
167             metadata = u.info()
168             headers = metadata.items()
169             target_url_date = None
170             for header in headers:
171                 if isinstance(header, tuple) and len(header) == 2:
172                     if header[0] == 'Last-Modified':
173                         target_url_date = header[1]
174                         break
175             if target_url_date is not None:
176                 target_dest_mtime = os.path.getmtime(target_dest)
177                 target_url_mtime = time.mktime(
178                     datetime.datetime.strptime(target_url_date,
179                                                "%a, %d %b %Y %X "
180                                                "GMT").timetuple())
181                 if target_url_mtime > target_dest_mtime:
182                     logging.debug('URL target is newer than disk...will '
183                                   'download')
184                 else:
185                     logging.info('URL target does not need to be downloaded')
186                     download_target = False
187             else:
188                 logging.debug('Unable to find last modified url date')
189         if download_target:
190             urllib.request.urlretrieve(target_url, filename=target_dest)
191             logging.info("Target downloaded: {}".format(target))
192         if target.endswith('.tar'):
193             logging.info('Unpacking tar file')
194             tar = tarfile.open(target_dest)
195             tar.extractall(path=dest)
196             tar.close()
197
198
199 def install_ansible():
200     # we only install for CentOS/Fedora for now
201     dist = distro.id()
202     if 'centos' in dist:
203         pkg_mgr = 'yum'
204     elif 'fedora' in dist:
205         pkg_mgr = 'dnf'
206     else:
207         return
208
209     # yum python module only exists for 2.x, so use subprocess
210     try:
211         subprocess.check_call([pkg_mgr, '-y', 'install', 'ansible'])
212     except subprocess.CalledProcessError:
213         logging.warning('Unable to install Ansible')
214
215
216 def internet_connectivity():
217     try:
218         urllib.request.urlopen('http://opnfv.org', timeout=3)
219         return True
220     except (urllib.request.URLError, socket.timeout):
221         logging.debug('No internet connectivity detected')
222         return False