Action plugin for timestamping a file
This change adds an action plugin called timestamp_file that will check if a file exists on a remote system and copies it out of the way so that a new version can be created. This plugin can be used to ensure that logs aren't overwritten or some other file you want to keep before modifying. Change-Id: I699641f65f20c7c4814632eb380525843ebcba98
This commit is contained in:
parent
65636ba813
commit
4326b34a7d
167
plugins/action/timestamp_file.py
Normal file
167
plugins/action/timestamp_file.py
Normal file
@ -0,0 +1,167 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleActionFail
|
||||
from ansible.errors import AnsibleActionSkip
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.plugins.action import ActionBase
|
||||
from datetime import datetime
|
||||
|
||||
import json
|
||||
import yaml
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: timestamp_file
|
||||
author:
|
||||
- "Alex Schultz (@mwhahaha)"
|
||||
version_added: '2.9'
|
||||
short_description: Take a copy of a file and append a timestamp
|
||||
notes: []
|
||||
description:
|
||||
- Take a copy of a file and append a timestamp
|
||||
requirements:
|
||||
- None
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Path to file
|
||||
type: str
|
||||
remove:
|
||||
description:
|
||||
- Remove original file
|
||||
default: False
|
||||
type: bool
|
||||
force:
|
||||
description:
|
||||
- Overwrite destination file if it exists
|
||||
default: False
|
||||
type: bool
|
||||
date_format:
|
||||
description:
|
||||
- Timestamp format to use when appending to destination file
|
||||
default: "%Y-%m-%d_%H:%M:%S"
|
||||
type: str
|
||||
"""
|
||||
EXAMPLES = """
|
||||
- name: Snapshot a file
|
||||
timestamp_file:
|
||||
path: /tmp/file.log
|
||||
- name: Snapshot a file and remove original
|
||||
timestamp_file:
|
||||
path: /tmp/file.log
|
||||
remove: True
|
||||
"""
|
||||
RETURN = """
|
||||
dest:
|
||||
description: Path to the new file
|
||||
returned: if changed
|
||||
type: str
|
||||
sample: "/tmp/file.log.2017-07-27_16:39:00"
|
||||
"""
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
_VALID_ARGS = yaml.safe_load(DOCUMENTATION)['options']
|
||||
|
||||
def _get_args(self):
|
||||
missing = []
|
||||
args = {}
|
||||
|
||||
for option, vals in self._VALID_ARGS.items():
|
||||
if 'default' not in vals:
|
||||
if self._task.args.get(option, None) is None:
|
||||
missing.append(option)
|
||||
continue
|
||||
args[option] = self._task.args.get(option)
|
||||
else:
|
||||
args[option] = self._task.args.get(option, vals['default'])
|
||||
|
||||
if missing:
|
||||
raise AnsibleActionFail('Missing required parameters: {}'.format(
|
||||
', '.join(missing)))
|
||||
return args
|
||||
|
||||
def _get_date_string(self, date_format):
|
||||
return datetime.now().strftime(date_format)
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
if task_vars is None:
|
||||
task_vars = dict()
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
del tmp
|
||||
# parse args
|
||||
args = self._get_args()
|
||||
|
||||
changed = False
|
||||
src_path = args['path']
|
||||
|
||||
# check if source file exists
|
||||
file_stat = self._execute_module(
|
||||
module_name='stat',
|
||||
module_args=dict(path=src_path),
|
||||
task_vars=task_vars
|
||||
)
|
||||
timestamp = self._get_date_string(args['date_format'])
|
||||
dest_path = '.'.join([src_path, timestamp])
|
||||
if file_stat.get('stat', {}).get('exists', False) is False:
|
||||
# file doesn't exist so we're done
|
||||
raise AnsibleActionSkip("{} does not exist.".format(src_path))
|
||||
|
||||
# check if destination file exists
|
||||
file_stat = self._execute_module(
|
||||
module_name='stat',
|
||||
module_args=dict(path=dest_path),
|
||||
task_vars=task_vars
|
||||
)
|
||||
if (not args['force']
|
||||
and file_stat.get('stat', {}).get('exists', False) is True):
|
||||
raise AnsibleActionFail("Destination file {} exists. Use force "
|
||||
"option to proceed.".format(dest_path))
|
||||
|
||||
# copy file out of the way
|
||||
copy_result = self._execute_module(
|
||||
module_name='copy',
|
||||
module_args=dict(src=src_path, dest=dest_path, remote_src=True),
|
||||
task_vars=task_vars
|
||||
)
|
||||
if copy_result.get('failed', False):
|
||||
return copy_result
|
||||
changed = True
|
||||
|
||||
if boolean(args.get('remove', False), strict=False):
|
||||
# cleanup original file as requested
|
||||
file_result = self._execute_module(
|
||||
module_name='file',
|
||||
module_args=dict(path=src_path, state='absent'),
|
||||
task_vars=task_vars
|
||||
)
|
||||
if file_result.get('failed', False):
|
||||
return file_result
|
||||
|
||||
result['dest'] = copy_result['dest']
|
||||
result['changed'] = changed
|
||||
return result
|
0
tests/plugins/action/__init__.py
Normal file
0
tests/plugins/action/__init__.py
Normal file
188
tests/plugins/action/test_timestamp_file.py
Normal file
188
tests/plugins/action/test_timestamp_file.py
Normal file
@ -0,0 +1,188 @@
|
||||
# Copyright 2019 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 mock
|
||||
|
||||
from ansible.errors import AnsibleActionFail
|
||||
from ansible.errors import AnsibleActionSkip
|
||||
from ansible.playbook.play_context import PlayContext
|
||||
|
||||
from tests import base as tests_base
|
||||
from plugins.action import timestamp_file
|
||||
|
||||
|
||||
class TestTimestampFile(tests_base.TestCase):
|
||||
|
||||
def test_run(self):
|
||||
mock_task = mock.MagicMock()
|
||||
mock_task.async_val = None
|
||||
mock_task.action = "timestamp_file"
|
||||
mock_task.args = dict(path='foo.log')
|
||||
mock_connection = mock.MagicMock()
|
||||
play_context = PlayContext()
|
||||
|
||||
action = timestamp_file.ActionModule(mock_task,
|
||||
mock_connection,
|
||||
play_context,
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
|
||||
mock_datetime = mock.MagicMock()
|
||||
mock_datetime.return_value = 'foo'
|
||||
action._get_date_string = mock_datetime
|
||||
mock_execute = mock.MagicMock()
|
||||
mock_execute.side_effect = [{'stat': {'exists': True}},
|
||||
{'stat': {'exists': False}},
|
||||
{'dest': 'foo.log.foo',
|
||||
'failed': False,
|
||||
'changed': True}]
|
||||
|
||||
action._execute_module = mock_execute
|
||||
|
||||
result = action.run()
|
||||
|
||||
execute_calls = [mock.call(module_args={'path': 'foo.log'},
|
||||
module_name='stat',
|
||||
task_vars={}),
|
||||
mock.call(module_args={'path': 'foo.log.foo'},
|
||||
module_name='stat',
|
||||
task_vars={}),
|
||||
mock.call(module_args={'src': 'foo.log',
|
||||
'dest': 'foo.log.foo',
|
||||
'remote_src': True},
|
||||
module_name='copy',
|
||||
task_vars={})
|
||||
]
|
||||
self.assertEqual(3, mock_execute.call_count)
|
||||
mock_execute.assert_has_calls(execute_calls)
|
||||
|
||||
expected_result = {'dest': 'foo.log.foo', 'changed': True}
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
def test_run_source_missing_skips(self):
|
||||
mock_task = mock.MagicMock()
|
||||
mock_task.async_val = None
|
||||
mock_task.action = "timestamp_file"
|
||||
mock_task.args = dict(path='foo.log')
|
||||
mock_connection = mock.MagicMock()
|
||||
play_context = PlayContext()
|
||||
|
||||
action = timestamp_file.ActionModule(mock_task,
|
||||
mock_connection,
|
||||
play_context,
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
|
||||
mock_datetime = mock.MagicMock()
|
||||
mock_datetime.return_value = 'foo'
|
||||
action._get_date_string = mock_datetime
|
||||
mock_execute = mock.MagicMock()
|
||||
mock_execute.side_effect = [{'stat': {'exists': False}}]
|
||||
|
||||
action._execute_module = mock_execute
|
||||
|
||||
self.assertRaises(AnsibleActionSkip, action.run)
|
||||
|
||||
execute_calls = [mock.call(module_args={'path': 'foo.log'},
|
||||
module_name='stat',
|
||||
task_vars={})
|
||||
]
|
||||
self.assertEqual(1, mock_execute.call_count)
|
||||
mock_execute.assert_has_calls(execute_calls)
|
||||
|
||||
def test_run_destination_exists_fails(self):
|
||||
mock_task = mock.MagicMock()
|
||||
mock_task.async_val = None
|
||||
mock_task.action = "timestamp_file"
|
||||
mock_task.args = dict(path='foo.log')
|
||||
mock_connection = mock.MagicMock()
|
||||
play_context = PlayContext()
|
||||
|
||||
action = timestamp_file.ActionModule(mock_task,
|
||||
mock_connection,
|
||||
play_context,
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
|
||||
mock_datetime = mock.MagicMock()
|
||||
mock_datetime.return_value = 'foo'
|
||||
action._get_date_string = mock_datetime
|
||||
mock_execute = mock.MagicMock()
|
||||
mock_execute.side_effect = [{'stat': {'exists': True}},
|
||||
{'stat': {'exists': True}}]
|
||||
|
||||
action._execute_module = mock_execute
|
||||
|
||||
self.assertRaises(AnsibleActionFail, action.run)
|
||||
|
||||
execute_calls = [mock.call(module_args={'path': 'foo.log'},
|
||||
module_name='stat',
|
||||
task_vars={}),
|
||||
mock.call(module_args={'path': 'foo.log.foo'},
|
||||
module_name='stat',
|
||||
task_vars={})
|
||||
]
|
||||
self.assertEqual(2, mock_execute.call_count)
|
||||
mock_execute.assert_has_calls(execute_calls)
|
||||
|
||||
def test_run_destination_exists_force(self):
|
||||
mock_task = mock.MagicMock()
|
||||
mock_task.async_val = None
|
||||
mock_task.action = "timestamp_file"
|
||||
mock_task.args = dict(path='foo.log', force=True)
|
||||
mock_connection = mock.MagicMock()
|
||||
play_context = PlayContext()
|
||||
|
||||
action = timestamp_file.ActionModule(mock_task,
|
||||
mock_connection,
|
||||
play_context,
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
|
||||
mock_datetime = mock.MagicMock()
|
||||
mock_datetime.return_value = 'foo'
|
||||
action._get_date_string = mock_datetime
|
||||
mock_execute = mock.MagicMock()
|
||||
mock_execute.side_effect = [{'stat': {'exists': True}},
|
||||
{'stat': {'exists': True}},
|
||||
{'dest': 'foo.log.foo',
|
||||
'failed': False,
|
||||
'changed': True}]
|
||||
|
||||
action._execute_module = mock_execute
|
||||
|
||||
result = action.run()
|
||||
|
||||
execute_calls = [mock.call(module_args={'path': 'foo.log'},
|
||||
module_name='stat',
|
||||
task_vars={}),
|
||||
mock.call(module_args={'path': 'foo.log.foo'},
|
||||
module_name='stat',
|
||||
task_vars={}),
|
||||
mock.call(module_args={'src': 'foo.log',
|
||||
'dest': 'foo.log.foo',
|
||||
'remote_src': True},
|
||||
module_name='copy',
|
||||
task_vars={})
|
||||
]
|
||||
self.assertEqual(3, mock_execute.call_count)
|
||||
mock_execute.assert_has_calls(execute_calls)
|
||||
|
||||
expected_result = {'dest': 'foo.log.foo', 'changed': True}
|
||||
self.assertEqual(expected_result, result)
|
Loading…
Reference in New Issue
Block a user