summaryrefslogtreecommitdiff
path: root/trove/guestagent/guest_log.py
blob: 570a08edbee41c1a2a5eed588ce7814cbfd971ee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# Copyright 2015 Tesora Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import enum
import hashlib
import os
from pathlib import Path
from requests.exceptions import ConnectionError

from oslo_log import log as logging
from swiftclient.client import ClientException

from trove.common import cfg
from trove.common import clients
from trove.common import exception
from trove.common.i18n import _
from trove.common import stream_codecs
from trove.common import timeutils
from trove.guestagent.common import operating_system
from trove.guestagent.common.operating_system import FileMode


LOG = logging.getLogger(__name__)
CONF = cfg.CONF


class LogType(enum.Enum):
    """Represent the type of the log object."""

    # System logs.  These are always enabled.
    SYS = 1

    # User logs.  These can be enabled or disabled.
    USER = 2


class LogStatus(enum.Enum):
    """Represent the status of the log object."""

    # The log is disabled and potentially no data is being written to
    # the corresponding log file
    Disabled = 1

    # Logging is on, but no determination has been made about data availability
    Enabled = 2

    # Logging is on, but no log data is available to publish
    Unavailable = 3

    # Logging is on and data is available to be published
    Ready = 4

    # Logging is on and all data has been published
    Published = 5

    # Logging is on and some data has been published
    Partial = 6

    # Log file has been rotated, so next publish will discard log first
    Rotated = 7

    # Waiting for a datastore restart to begin logging
    Restart_Required = 8

    # Now that restart has completed, regular status can be reported again
    # This is an internal status
    Restart_Completed = 9


class GuestLog(object):

    MF_FILE_SUFFIX = '_metafile'
    MF_LABEL_LOG_NAME = 'log_name'
    MF_LABEL_LOG_TYPE = 'log_type'
    MF_LABEL_LOG_FILE = 'log_file'
    MF_LABEL_LOG_SIZE = 'log_size'
    MF_LABEL_LOG_HEADER = 'log_header_digest'

    def __init__(self, log_context, log_name, log_type, log_user, log_file,
                 log_exposed):
        self._context = log_context
        self._name = log_name
        self._type = log_type
        self._user = log_user
        self._file = log_file
        self._exposed = log_exposed
        self._size = None
        self._published_size = None
        self._header_digest = 'abc'
        self._published_header_digest = None
        self._status = None
        self._cached_context = None
        self._cached_swift_client = None
        self._enabled = log_type == LogType.SYS
        self._file_readable = False
        self._container_name = None
        self._codec = stream_codecs.JsonCodec()

        self._set_status(self._type == LogType.USER,
                         LogStatus.Disabled, LogStatus.Enabled)

        # The directory should already exist - make sure we have access to it
        log_dir = os.path.dirname(self._file)
        operating_system.chmod(
            log_dir, FileMode.ADD_GRP_RX_OTH_RX, as_root=True)

    @property
    def context(self):
        return self._context

    @context.setter
    def context(self, context):
        self._context = context

    @property
    def type(self):
        return self._type

    @property
    def swift_client(self):
        if not self._cached_swift_client or (
                self._cached_context != self.context):
            self._cached_swift_client = clients.swift_client(self.context)
            self._cached_context = self.context
        return self._cached_swift_client

    @property
    def exposed(self):
        return self._exposed or self.context.is_admin

    @property
    def enabled(self):
        return self._enabled

    @enabled.setter
    def enabled(self, enabled):
        self._enabled = enabled

    @property
    def status(self):
        return self._status

    @status.setter
    def status(self, status):
        # Keep the status in Restart_Required until we're set
        # to Restart_Completed
        if (self.status != LogStatus.Restart_Required or
                (self.status == LogStatus.Restart_Required and
                 status == LogStatus.Restart_Completed)):
            self._status = status
            LOG.debug("Log status for '%(name)s' set to %(status)s",
                      {'name': self._name, 'status': status})
        else:
            LOG.debug("Log status for '%(name)s' *not* set to %(status)s "
                      "(currently %(current_status)s)",
                      {'name': self._name, 'status': status,
                       'current_status': self.status})

    def get_container_name(self, force=False):
        if not self._container_name or force:
            container_name = CONF.guest_log_container_name
            try:
                self.swift_client.get_container(container_name, prefix='dummy')
            except ClientException as ex:
                if ex.http_status == 404:
                    LOG.debug("Container '%s' not found; creating now",
                              container_name)
                    self.swift_client.put_container(
                        container_name, headers=self._get_headers())
                else:
                    LOG.exception("Could not retrieve container '%s'",
                                  container_name)
                    raise
            self._container_name = container_name
        return self._container_name

    def _set_status(self, use_first, first_status, second_status):
        if use_first:
            self.status = first_status
        else:
            self.status = second_status

    def show(self):
        if self.exposed:
            self._refresh_details()
            container_name = 'None'
            prefix = 'None'
            if self._published_size:
                container_name = self.get_container_name()
                prefix = self._object_prefix()
            pending = self._size - self._published_size
            if self.status == LogStatus.Rotated:
                pending = self._size
            return {
                'name': self._name,
                'type': self._type.name,
                'status': self.status.name.replace('_', ' '),
                'published': self._published_size,
                'pending': pending,
                'container': container_name,
                'prefix': prefix,
                'metafile': self._metafile_name()
            }
        else:
            raise exception.LogAccessForbidden(action='show', log=self._name)

    def _refresh_details(self):
        if self._published_size is None:
            # Initializing, so get all the values
            try:
                meta_details = self._get_meta_details()
                self._published_size = int(
                    meta_details[self.MF_LABEL_LOG_SIZE])
                self._published_header_digest = (
                    meta_details[self.MF_LABEL_LOG_HEADER])
            except ClientException as ex:
                if ex.http_status == 404:
                    LOG.debug("No published metadata found for log '%s'",
                              self._name)
                    self._published_size = 0
                else:
                    LOG.exception("Could not get meta details for log '%s'",
                                  self._name)
                    raise
            except ConnectionError as e:
                # A bad endpoint will cause a ConnectionError
                # This exception contains another exception that we want
                exc = e.args[0]
                raise exc

        self._update_details()
        LOG.debug("Log size for '%(name)s' set to %(size)d "
                  "(published %(published)d)",
                  {'name': self._name, 'size': self._size,
                   'published': self._published_size})

    def _update_details(self):
        if operating_system.exists(self._file, as_root=True):
            file_path = Path(self._file)

            # Make sure guest agent can read the log file.
            if not os.access(self._file, os.R_OK):
                operating_system.chmod(self._file, FileMode.ADD_ALL_R,
                                       as_root=True)
                operating_system.chmod(str(file_path.parent),
                                       FileMode.ADD_GRP_RX_OTH_RX,
                                       as_root=True)

            self._size = file_path.stat().st_size
            self._update_log_header_digest(self._file)

            if self.status != LogStatus.Disabled:
                if self._log_rotated():
                    self.status = LogStatus.Rotated
                # See if we have stuff to publish
                elif self._size > self._published_size:
                    self._set_status(self._published_size,
                                     LogStatus.Partial, LogStatus.Ready)
                # We've published everything so far
                elif self._size == self._published_size:
                    self._set_status(self._published_size,
                                     LogStatus.Published, LogStatus.Enabled)
                # We've already handled this case (log rotated) so what gives?
                else:
                    raise Exception(_("Bug in _log_rotated ?"))
        else:
            LOG.warning(f"File {self._file} does not exist")
            self._published_size = 0
            self._size = 0

    def _log_rotated(self):
        """If the file is smaller than the last reported size
        or the first line hash is different, we can probably assume
        the file changed under our nose.
        """
        if (self._published_size > 0 and
                (self._size < self._published_size or
                 self._published_header_digest != self._header_digest)):
            return True

    def _update_log_header_digest(self, log_file):
        with open(log_file, 'rb') as log:
            self._header_digest = hashlib.md5(log.readline()).hexdigest()

    def _get_headers(self):
        return {'X-Delete-After': str(CONF.guest_log_expiry)}

    def publish_log(self):
        if self.exposed:
            if self._log_rotated():
                LOG.debug("Log file rotation detected for '%s' - "
                          "discarding old log", self._name)
                self._delete_log_components()
            if operating_system.exists(self._file, as_root=True):
                self._publish_to_container(self._file)
            else:
                raise RuntimeError(_(
                    "Cannot publish log file '%s' as it does not exist.") %
                    self._file)
            return self.show()
        else:
            raise exception.LogAccessForbidden(
                action='publish', log=self._name)

    def discard_log(self):
        if self.exposed:
            self._delete_log_components()
            return self.show()
        else:
            raise exception.LogAccessForbidden(
                action='discard', log=self._name)

    def _delete_log_components(self):
        container_name = self.get_container_name(force=True)
        prefix = self._object_prefix()
        swift_files = [swift_file['name']
                       for swift_file in self.swift_client.get_container(
                       container_name, prefix=prefix)[1]]
        swift_files.append(self._metafile_name())
        for swift_file in swift_files:
            self.swift_client.delete_object(container_name, swift_file)
        self._published_size = 0

    def _publish_to_container(self, log_filename):
        log_component, log_lines = '', 0
        chunk_size = CONF.guest_log_limit
        container_name = self.get_container_name(force=True)

        def _read_chunk(f):
            while True:
                current_chunk = f.read(chunk_size)
                if not current_chunk:
                    break
                yield current_chunk

        def _write_log_component():
            object_headers.update({'x-object-meta-lines': str(log_lines)})
            component_name = '%s%s' % (self._object_prefix(),
                                       self._object_name())
            self.swift_client.put_object(container_name,
                                         component_name, log_component,
                                         headers=object_headers)
            self._published_size = (
                self._published_size + len(log_component))
            self._published_header_digest = self._header_digest

        self._refresh_details()
        self._put_meta_details()
        object_headers = self._get_headers()
        with open(log_filename, 'r') as log:
            LOG.debug("seeking to %s", self._published_size)
            log.seek(self._published_size)
            for chunk in _read_chunk(log):
                for log_line in chunk.splitlines():
                    if len(log_component) + len(log_line) > chunk_size:
                        _write_log_component()
                        log_component, log_lines = '', 0
                    log_component = log_component + log_line + '\n'
                    log_lines += 1
        if log_lines > 0:
            _write_log_component()
        self._put_meta_details()

    def _put_meta_details(self):
        metafile_name = self._metafile_name()
        metafile_details = {
            self.MF_LABEL_LOG_NAME: self._name,
            self.MF_LABEL_LOG_TYPE: self._type.name,
            self.MF_LABEL_LOG_FILE: self._file,
            self.MF_LABEL_LOG_SIZE: self._published_size,
            self.MF_LABEL_LOG_HEADER: self._header_digest,
        }
        container_name = self.get_container_name()
        self.swift_client.put_object(container_name, metafile_name,
                                     self._codec.serialize(metafile_details),
                                     headers=self._get_headers())
        LOG.debug("_put_meta_details has published log size as %s",
                  self._published_size)

    def _metafile_name(self):
        return self._object_prefix().rstrip('/') + '_metafile'

    def _object_prefix(self):
        return '%(instance_id)s/%(datastore)s-%(log)s/' % {
            'instance_id': CONF.guest_id,
            'datastore': CONF.datastore_manager,
            'log': self._name}

    def _object_name(self):
        return 'log-%s' % str(timeutils.utcnow()).replace(' ', 'T')

    def _get_meta_details(self):
        LOG.debug("Getting meta details for '%s'", self._name)
        metafile_name = self._metafile_name()
        container_name = self.get_container_name()
        headers, metafile_details = self.swift_client.get_object(
            container_name, metafile_name)
        LOG.debug("Found meta details for '%s'", self._name)
        return self._codec.deserialize(metafile_details)