backend/bms_app/services/rman.py (70 lines of code) (raw):
# Copyright 2022 Google LLC
#
# 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.
from bms_app import settings
from bms_app.services.gcs import blob_exists, get_file_content
class RmanLogFileError(Exception):
def __init__(self, errors):
self.errors = errors
super().__init__()
class RmanCommandLogParser:
"""Parser for one rman command output.
Exmaple:
RMAN> restore database preview;
Starting restore at 12-MAY-22
using channel ORA_DISK_1
List of Backup Sets
===================
....
"""
ERROR_WORDS = ['EXPIRED', 'RMAN-', 'ORA-', 'ERROR']
# list of commands with specific error output
CMD_WITH_TABLE_STYLE_ERRORS = (
'report need backup',
'report unrecoverable'
)
def __init__(self, text):
self.text = text.strip()
self.lines = text.split('\n')
self.cmd = self.lines[0].strip(' ;')
def has_error(self):
if self.cmd in self.CMD_WITH_TABLE_STYLE_ERRORS:
has_error = self._has_table_style_err()
else:
has_error = self._has_error_word()
return has_error
def _has_error_word(self):
"""Check if contains any of words that means error."""
return any(word in self.text for word in self.ERROR_WORDS)
def _has_table_style_err(self):
"""Check "table" style error.
Error example:
RMAN> report need backup;
RMAN retention policy will be applied to the command
RMAN retention policy is set to redundancy 1
Report of files with less than 1 redundant backups
File #bkps Name
---- ----- -------------------------------------------------------------------------------
1 0 +DATA/FINDB/DATAFILE/system.256.1095521433
7 0 +DATA/FINDB/DATAFILE/users.259.1095521503
No error example:
RMAN> report need backup;
RMAN retention policy will be applied to the command
RMAN retention policy is set to redundancy 1
Report of files with less than 1 redundant backups
File #bkps Name
---- ----- ---------------------------------------------------------------------------------
"""
# check if the last line has any data except separator (space and dash)
return bool(self.lines[-1].strip(' -'))
class RmanLogFileParser:
RMAN_FILE_LOCATION = 'control_node_logs/bms_logs_{operation_id}/rman_pre_restore.log'
COMMANDS_TO_SKIP = ('delete noprompt expired backup')
RMAN_CMD_PREFIX = 'RMAN>'
def __init__(self, operation_id):
self.operation_id = operation_id
self._rman_file_content = None
def validate(self):
failed_commands = []
for chunk in self._split_file():
cmd_parser = RmanCommandLogParser(chunk)
# skip logs not related to specific rman command
if cmd_parser.cmd and cmd_parser.cmd not in self.COMMANDS_TO_SKIP:
if cmd_parser.has_error():
failed_commands.append(
f'{self.RMAN_CMD_PREFIX} {cmd_parser.cmd}'
)
if failed_commands:
raise RmanLogFileError(failed_commands)
@property
def rman_file_content(self):
if self._rman_file_content is None:
gcs_key = self.RMAN_FILE_LOCATION.format(
operation_id=self.operation_id
)
self._rman_file_content = get_file_content(
settings.GCS_BUCKET,
gcs_key
)
return self._rman_file_content
def get_cmd_output(self, cmd):
"""Return output of the specific rman command."""
if cmd.startswith(self.RMAN_CMD_PREFIX):
cmd = cmd[5:].strip() # to remove spaces around
for chunk in self._split_file():
cmd_parser = RmanCommandLogParser(chunk)
if cmd_parser.cmd == cmd:
return chunk
def _split_file(self):
splitted = [x.strip() for x in self.rman_file_content.split(self.RMAN_CMD_PREFIX)]
# skip 1-st section that contains some general information
return splitted[1:]
def log_file_exists(self):
gcs_key = self.RMAN_FILE_LOCATION.format(
operation_id=self.operation_id
)
return blob_exists(settings.GCS_BUCKET, gcs_key)