1# Microsoft Azure Linux Agent
2#
3# Copyright 2018 Microsoft Corporation
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17# Requires Python 2.6+ and Openssl 1.0+
18#
19
20"""
21File operation util functions
22"""
23
24import errno as errno
25import glob
26import os
27import pwd
28import re
29import shutil
30
31import azurelinuxagent.common.logger as logger
32import azurelinuxagent.common.utils.textutil as textutil
33
34from azurelinuxagent.common.future import ustr
35
36KNOWN_IOERRORS = [
37    errno.EIO,          # I/O error
38    errno.ENOMEM,       # Out of memory
39    errno.ENFILE,       # File table overflow
40    errno.EMFILE,       # Too many open files
41    errno.ENOSPC,       # Out of space
42    errno.ENAMETOOLONG, # Name too long
43    errno.ELOOP,        # Too many symbolic links encountered
44    121                 # Remote I/O error (errno.EREMOTEIO -- not present in all Python 2.7+)
45]
46
47
48def read_file(filepath, asbin=False, remove_bom=False, encoding='utf-8'):
49    """
50    Read and return contents of 'filepath'.
51    """
52    mode = 'rb'
53    with open(filepath, mode) as in_file:
54        data = in_file.read()
55        if data is None:
56            return None
57
58        if asbin:
59            return data
60
61        if remove_bom:
62            # remove bom on bytes data before it is converted into string.
63            data = textutil.remove_bom(data)
64        data = ustr(data, encoding=encoding)
65        return data
66
67
68def write_file(filepath, contents, asbin=False, encoding='utf-8', append=False):
69    """
70    Write 'contents' to 'filepath'.
71    """
72    mode = "ab" if append else "wb"
73    data = contents
74    if not asbin:
75        data = contents.encode(encoding)
76    with open(filepath, mode) as out_file:
77        out_file.write(data)
78
79
80def append_file(filepath, contents, asbin=False, encoding='utf-8'):
81    """
82    Append 'contents' to 'filepath'.
83    """
84    write_file(filepath, contents, asbin=asbin, encoding=encoding, append=True)
85
86
87def base_name(path):
88    head, tail = os.path.split(path)  # pylint: disable=W0612
89    return tail
90
91
92def get_line_startingwith(prefix, filepath):
93    """
94    Return line from 'filepath' if the line startswith 'prefix'
95    """
96    for line in read_file(filepath).split('\n'):
97        if line.startswith(prefix):
98            return line
99    return None
100
101
102def mkdir(dirpath, mode=None, owner=None):
103    if not os.path.isdir(dirpath):
104        os.makedirs(dirpath)
105    if mode is not None:
106        chmod(dirpath, mode)
107    if owner is not None:
108        chowner(dirpath, owner)
109
110
111def chowner(path, owner):
112    if not os.path.exists(path):
113        logger.error("Path does not exist: {0}".format(path))
114    else:
115        owner_info = pwd.getpwnam(owner)
116        os.chown(path, owner_info[2], owner_info[3])
117
118
119def chmod(path, mode):
120    if not os.path.exists(path):
121        logger.error("Path does not exist: {0}".format(path))
122    else:
123        os.chmod(path, mode)
124
125
126def rm_files(*args):
127    for paths in args:
128        # find all possible file paths
129        for path in glob.glob(paths):
130            if os.path.isfile(path):
131                os.remove(path)
132
133
134def rm_dirs(*args):
135    """
136    Remove the contents of each directory
137    """
138    for p in args:
139        if not os.path.isdir(p):
140            continue
141
142        for pp in os.listdir(p):
143            path = os.path.join(p, pp)
144            if os.path.isfile(path):
145                os.remove(path)
146            elif os.path.islink(path):
147                os.unlink(path)
148            elif os.path.isdir(path):
149                shutil.rmtree(path)
150
151
152def trim_ext(path, ext):
153    if not ext.startswith("."):
154        ext = "." + ext
155    return path.split(ext)[0] if path.endswith(ext) else path
156
157
158def update_conf_file(path, line_start, val, chk_err=False):
159    conf = []
160    if not os.path.isfile(path) and chk_err:
161        raise IOError("Can't find config file:{0}".format(path))
162    conf = read_file(path).split('\n')
163    conf = [x for x in conf
164            if x is not None and len(x) > 0 and not x.startswith(line_start)]
165    conf.append(val)
166    write_file(path, '\n'.join(conf) + '\n')
167
168
169def search_file(target_dir_name, target_file_name):
170    for root, dirs, files in os.walk(target_dir_name):  # pylint: disable=W0612
171        for file_name in files:
172            if file_name == target_file_name:
173                return os.path.join(root, file_name)
174    return None
175
176
177def chmod_tree(path, mode):
178    for root, dirs, files in os.walk(path):  # pylint: disable=W0612
179        for file_name in files:
180            os.chmod(os.path.join(root, file_name), mode)
181
182
183def findstr_in_file(file_path, line_str):
184    """
185    Return True if the line is in the file; False otherwise.
186    (Trailing whitespace is ignored.)
187    """
188    try:
189        with open(file_path, 'r') as fh:
190            for line in fh.readlines():
191                if line_str == line.rstrip():
192                    return True
193    except Exception:
194        # swallow exception
195        pass
196    return False
197
198
199def findre_in_file(file_path, line_re):
200    """
201    Return match object if found in file.
202    """
203    try:
204        with open(file_path, 'r') as fh:
205            pattern = re.compile(line_re)
206            for line in fh.readlines():
207                match = re.search(pattern, line)
208                if match:
209                    return match
210    except:  # pylint: disable=W0702
211        pass
212
213    return None
214
215
216def get_all_files(root_path):
217    """
218    Find all files under the given root path
219    """
220    result = []
221    for root, dirs, files in os.walk(root_path):  # pylint: disable=W0612
222        result.extend([os.path.join(root, file) for file in files])  # pylint: disable=redefined-builtin
223
224    return result
225
226
227def clean_ioerror(e, paths=None):
228    """
229    Clean-up possibly bad files and directories after an IO error.
230    The code ignores *all* errors since disk state may be unhealthy.
231    """
232    if paths is None:
233        paths = []
234    if isinstance(e, IOError) and e.errno in KNOWN_IOERRORS:
235        for path in paths:
236            if path is None:
237                continue
238
239            try:
240                if os.path.isdir(path):
241                    shutil.rmtree(path, ignore_errors=True)
242                else:
243                    os.remove(path)
244            except Exception:
245                # swallow exception
246                pass
247