Tasklib implementation in python
Medium between configuration providers and fuel orchestration solution. check README.md and functional tests for better understanding how it works HOW TO RUN: python setup.py develop taskcmd -c tasklib/tests/functional/conf.yaml run puppet/sleep taskcmd -c tasklib/tests/functional/conf.yaml run exec/fail taskcmd -c tasklib/tests/functional/conf.yaml daemon exec/long taskcmd -c tasklib/tests/functional/conf.yaml status exec/long Related to blueprint granular-deployment-based-on-tasks Change-Id: Ifed1a9b90042bbbc93215a30dfb34e29dbe8fba2
This commit is contained in:
parent
d8d2d3ced8
commit
4dd82c6be3
26
run_tests.sh
26
run_tests.sh
|
@ -23,6 +23,7 @@ function usage {
|
|||
echo " -a, --agent Run FUEL_AGENT unit tests"
|
||||
echo " -A, --no-agent Don't run FUEL_AGENT unit tests"
|
||||
echo " -n, --nailgun Run NAILGUN both unit and integration tests"
|
||||
echo " -l, --tasklib Run tasklib unit and functional tests"
|
||||
echo " -N, --no-nailgun Don't run NAILGUN tests"
|
||||
echo " -w, --webui Run WEB-UI tests"
|
||||
echo " -W, --no-webui Don't run WEB-UI tests"
|
||||
|
@ -53,6 +54,8 @@ function process_options {
|
|||
-A|--no-agent) no_agent_tests=1;;
|
||||
-n|--nailgun) nailgun_tests=1;;
|
||||
-N|--no-nailgun) no_nailgun_tests=1;;
|
||||
-l|--tasklib) tasklib_tests=1;;
|
||||
-L|--no-tasklib) no_tasklib_tests=1;;
|
||||
-w|--webui) webui_tests=1;;
|
||||
-W|--no-webui) no_webui_tests=1;;
|
||||
-c|--cli) cli_tests=1;;
|
||||
|
@ -115,6 +118,8 @@ no_flake8_checks=0
|
|||
jslint_checks=0
|
||||
no_jslint_checks=0
|
||||
certain_tests=0
|
||||
tasklib_tests=0
|
||||
no_tasklib_tests=0
|
||||
|
||||
|
||||
function run_tests {
|
||||
|
@ -134,6 +139,7 @@ function run_tests {
|
|||
# Enable all tests if none was specified skipping all explicitly disabled tests.
|
||||
if [[ $agent_tests -eq 0 && \
|
||||
$nailgun_tests -eq 0 && \
|
||||
$tasklib_tests -eq 0 && \
|
||||
$webui_tests -eq 0 && \
|
||||
$cli_tests -eq 0 && \
|
||||
$upgrade_system -eq 0 && \
|
||||
|
@ -143,6 +149,7 @@ function run_tests {
|
|||
|
||||
if [ $no_agent_tests -ne 1 ]; then agent_tests=1; fi
|
||||
if [ $no_nailgun_tests -ne 1 ]; then nailgun_tests=1; fi
|
||||
if [ $no_tasklib_tests -ne 1 ]; then tasklib_tests=1; fi
|
||||
if [ $no_webui_tests -ne 1 ]; then webui_tests=1; fi
|
||||
if [ $no_cli_tests -ne 1 ]; then cli_tests=1; fi
|
||||
if [ $no_upgrade_system -ne 1 ]; then upgrade_system=1; fi
|
||||
|
@ -167,6 +174,11 @@ function run_tests {
|
|||
run_nailgun_tests || errors+=" nailgun_tests"
|
||||
fi
|
||||
|
||||
if [ $tasklib_tests -eq 1 ]; then
|
||||
echo "Starting Tasklib tests"
|
||||
run_tasklib_tests || errors+=" tasklib tests"
|
||||
fi
|
||||
|
||||
if [ $webui_tests -eq 1 ]; then
|
||||
echo "Starting WebUI tests..."
|
||||
run_webui_tests || errors+=" webui_tests"
|
||||
|
@ -411,6 +423,19 @@ function run_shotgun_tests {
|
|||
return $result
|
||||
}
|
||||
|
||||
function run_tasklib_tests {
|
||||
local result=0
|
||||
|
||||
pushd $ROOT/tasklib >> /dev/null
|
||||
|
||||
# run tests
|
||||
tox -epy26 || result=1
|
||||
|
||||
popd >> /dev/null
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function run_flake8_subproject {
|
||||
local DIRECTORY=$1
|
||||
local result=0
|
||||
|
@ -432,6 +457,7 @@ function run_flake8 {
|
|||
local result=0
|
||||
run_flake8_subproject fuel_agent && \
|
||||
run_flake8_subproject nailgun && \
|
||||
run_flake8_subproject tasklib && \
|
||||
run_flake8_subproject fuelclient && \
|
||||
run_flake8_subproject fuelmenu && \
|
||||
run_flake8_subproject network_checker && \
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
tmp/*
|
||||
*.egg-info
|
||||
*.pyc
|
||||
tasklib/tests/functional/tmp/
|
||||
tasklib.log
|
|
@ -0,0 +1 @@
|
|||
include *requirements.txt
|
|
@ -0,0 +1,72 @@
|
|||
Tasklib
|
||||
=======
|
||||
|
||||
Tasklib is mediator between different configuration management providers
|
||||
and orchestration mechanism in Fuel.
|
||||
|
||||
It will try to cover next areas:
|
||||
- part of the plugable fuel architecture
|
||||
See tasklib/tasklib/tests/functional for detailed descriptionof
|
||||
how tasklib plugability will work
|
||||
- Control mechanism for tasks in fuel
|
||||
To support different types of workflow we will provide
|
||||
ability to terminate, list all running, stop/pause task
|
||||
- Abstraction layer between tasks and orchestration, which will allow
|
||||
easier development and debuging of tasks
|
||||
- General reporting solution for tasks
|
||||
|
||||
Executions drivers
|
||||
==================
|
||||
- puppet
|
||||
- exec
|
||||
|
||||
Puppet
|
||||
--------
|
||||
Puppet executor supports general metadata for running puppet manifests.
|
||||
Example of such metadata (task.yaml):
|
||||
|
||||
type: puppet - required
|
||||
puppet_manifest: file.pp - default is site.pp
|
||||
puppet_moduels: /etc/puppet/modules
|
||||
puppet_options: --debug
|
||||
|
||||
All defaults you can find in:
|
||||
>> taskcmd conf
|
||||
|
||||
It works next way:
|
||||
After task.yaml is found - executor will look for puppet_manifest
|
||||
and run:
|
||||
puppet apply --modulepath=/etc/puppet/modules file.pp
|
||||
with additional options you will provide
|
||||
|
||||
Exec
|
||||
-----
|
||||
|
||||
type: exec - required
|
||||
cmd: echo 12 - required
|
||||
|
||||
will execute any cmd provided as subprocess
|
||||
|
||||
EXAMPLES:
|
||||
=========
|
||||
|
||||
taskcmd -c tasklib/tests/functional/conf.yaml conf
|
||||
|
||||
taskcmd -c tasklib/tests/functional/conf.yaml list
|
||||
|
||||
taskcmd -c tasklib/tests/functional/conf.yaml daemon puppet/sleep
|
||||
taskcmd -c tasklib/tests/functional/conf.yaml status puppet/sleep
|
||||
|
||||
taskcmd -c tasklib/tests/functional/conf.yaml run puppet/cmd
|
||||
|
||||
taskcmd -c tasklib/tests/functional/conf.yaml run puppet/invalid
|
||||
|
||||
HOW TO RUN TESTS:
|
||||
==================
|
||||
python setup.py develop
|
||||
pip install -r test-requires.txt
|
||||
|
||||
nosetests tasklib/tests
|
||||
|
||||
For some functional tests installed puppet is required,
|
||||
so if you dont have puppet - they will be skipped
|
|
@ -0,0 +1,3 @@
|
|||
argparse
|
||||
daemonize
|
||||
pyyaml
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright 2014 Mirantis, 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 os
|
||||
import setuptools
|
||||
|
||||
|
||||
def requirements():
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
requirements = []
|
||||
with open('{0}/requirements.txt'.format(dir_path), 'r') as reqs:
|
||||
requirements = reqs.readlines()
|
||||
return requirements
|
||||
|
||||
|
||||
major_version = '0.1'
|
||||
minor_version = '0'
|
||||
name = 'tasklib'
|
||||
|
||||
version = "%s.%s" % (major_version, minor_version)
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name=name,
|
||||
version=version,
|
||||
description='Tasklib package',
|
||||
long_description="""Tasklib is intended to be mediator between different
|
||||
configuration management solutions and orchestrator.
|
||||
This is required to support plugable tasks/actions with good
|
||||
amount of control, such as stop/pause/poll state.
|
||||
""",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python",
|
||||
],
|
||||
author='Mirantis Inc.',
|
||||
author_email='product@mirantis.com',
|
||||
url='http://mirantis.com',
|
||||
keywords='tasklib mirantis',
|
||||
packages=setuptools.find_packages(),
|
||||
zip_safe=False,
|
||||
install_requires=requirements(),
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'taskcmd = tasklib.cli:main',
|
||||
]})
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
|
@ -0,0 +1,35 @@
|
|||
# Copyright 2014 Mirantis, 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 logging
|
||||
|
||||
from tasklib import exceptions
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Action(object):
|
||||
|
||||
def __init__(self, task, config):
|
||||
self.task = task
|
||||
self.config = config
|
||||
log.debug('Init action with task %s', self.task.name)
|
||||
|
||||
def verify(self):
|
||||
if 'type' not in self.task.metadata:
|
||||
raise exceptions.NotValidMetadata()
|
||||
|
||||
def run(self):
|
||||
raise NotImplementedError('Should be implemented by action driver.')
|
|
@ -0,0 +1,44 @@
|
|||
# Copyright 2014 Mirantis, 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 logging
|
||||
|
||||
from tasklib.actions import action
|
||||
from tasklib import exceptions
|
||||
from tasklib import utils
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExecAction(action.Action):
|
||||
|
||||
def verify(self):
|
||||
super(ExecAction, self).verify()
|
||||
if 'cmd' not in self.task.metadata:
|
||||
raise exceptions.NotValidMetadata()
|
||||
|
||||
@property
|
||||
def command(self):
|
||||
return self.task.metadata['cmd']
|
||||
|
||||
def run(self):
|
||||
log.debug('Running task %s with command %s',
|
||||
self.task.name, self.command)
|
||||
exit_code, stdout, stderr = utils.execute(self.command)
|
||||
log.debug(
|
||||
'Task %s with command %s\n returned code %s\n out %s err%s',
|
||||
self.task.name, self.command, exit_code, stdout, stderr)
|
||||
if exit_code != 0:
|
||||
raise exceptions.Failed()
|
||||
return exit_code
|
|
@ -0,0 +1,68 @@
|
|||
# Copyright 2014 Mirantis, 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 logging
|
||||
import os
|
||||
|
||||
from tasklib.actions import action
|
||||
from tasklib import exceptions
|
||||
from tasklib import utils
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PuppetAction(action.Action):
|
||||
|
||||
def run(self):
|
||||
log.debug('Running puppet task %s with command %s',
|
||||
self.task.name, self.command)
|
||||
exit_code, stdout, stderr = utils.execute(self.command)
|
||||
log.debug(
|
||||
'Task %s with command %s\n returned code %s\n out %s err%s',
|
||||
self.task.name, self.command, exit_code, stdout, stderr)
|
||||
# 0 - no changes
|
||||
# 2 - was some changes but successfull
|
||||
# 4 - failures during transaction
|
||||
# 6 - changes and failures
|
||||
if exit_code not in [0, 2]:
|
||||
raise exceptions.Failed()
|
||||
return exit_code
|
||||
|
||||
@property
|
||||
def manifest(self):
|
||||
return (self.task.metadata.get('puppet_manifest') or
|
||||
self.config['puppet_manifest'])
|
||||
|
||||
@property
|
||||
def puppet_options(self):
|
||||
if 'puppet_options' in self.task.metadata:
|
||||
return self.task.metadata['puppet_options']
|
||||
return self.config['puppet_options']
|
||||
|
||||
@property
|
||||
def puppet_modules(self):
|
||||
return (self.task.metadata.get('puppet_modules') or
|
||||
self.config['puppet_modules'])
|
||||
|
||||
@property
|
||||
def command(self):
|
||||
cmd = ['puppet', 'apply', '--detailed-exitcodes']
|
||||
if self.puppet_modules:
|
||||
cmd.append('--modulepath={0}'.format(self.puppet_modules))
|
||||
if self.puppet_options:
|
||||
cmd.append(self.puppet_options)
|
||||
if self.config['debug']:
|
||||
cmd.append('--debug --verbose --evaltrace')
|
||||
cmd.append(os.path.join(self.task.dir, self.manifest))
|
||||
return ' '.join(cmd)
|
|
@ -0,0 +1,94 @@
|
|||
# Copyright 2014 Mirantis, 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 logging
|
||||
import os
|
||||
|
||||
import daemonize
|
||||
|
||||
from tasklib import exceptions
|
||||
from tasklib import task
|
||||
from tasklib import utils
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskAgent(object):
|
||||
|
||||
def __init__(self, task_name, config):
|
||||
log.debug('Initializing task agent for task %s', task_name)
|
||||
self.config = config
|
||||
self.task = task.Task(task_name, self.config)
|
||||
self.init_directories()
|
||||
|
||||
def init_directories(self):
|
||||
utils.ensure_dir_created(self.config['pid_dir'])
|
||||
utils.ensure_dir_created(self.config['report_dir'])
|
||||
utils.ensure_dir_created(self.task.pid_dir)
|
||||
utils.ensure_dir_created(self.task.report_dir)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.set_status(utils.STATUS.running.name)
|
||||
result = self.task.run()
|
||||
self.set_status(utils.STATUS.end.name)
|
||||
return result
|
||||
except exceptions.NotFound as exc:
|
||||
log.warning('Cant find task %s with path %s',
|
||||
self.task.name, self.task.file)
|
||||
self.set_status(utils.STATUS.notfound.name)
|
||||
except exceptions.Failed as exc:
|
||||
log.error('Task %s failed with msg %s', self.task.name, exc.msg)
|
||||
self.set_status(utils.STATUS.failed.name)
|
||||
except Exception:
|
||||
log.exception('Task %s erred', self.task.name)
|
||||
self.set_status(utils.STATUS.error.name)
|
||||
finally:
|
||||
self.clean()
|
||||
|
||||
def __str__(self):
|
||||
return 'tasklib agent - {0}'.format(self.task.name)
|
||||
|
||||
def report(self):
|
||||
return 'placeholder'
|
||||
|
||||
def status(self):
|
||||
if not os.path.exists(self.task.status_file):
|
||||
return utils.STATUS.notfound.name
|
||||
with open(self.task.status_file) as f:
|
||||
return f.read()
|
||||
|
||||
def set_status(self, status):
|
||||
with open(self.task.status_file, 'w') as f:
|
||||
f.write(status)
|
||||
|
||||
def code(self):
|
||||
status = self.status()
|
||||
return getattr(utils.STATUS, status).code
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
return os.path.join(self.task.pid_dir, 'run.pid')
|
||||
|
||||
def daemon(self):
|
||||
log.debug('Daemonizing task %s with pidfile %s',
|
||||
self.task.name, self.pid)
|
||||
daemon = daemonize.Daemonize(
|
||||
app=str(self), pid=self.pid, action=self.run)
|
||||
daemon.start()
|
||||
|
||||
def clean(self):
|
||||
if os.path.exists(self.pid):
|
||||
os.unlink(self.pid)
|
|
@ -0,0 +1,115 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
||||
"""Tasklib cmd interface
|
||||
Exit Codes:
|
||||
ended successfully - 0
|
||||
running - 1
|
||||
valid but failed - 2
|
||||
unexpected error - 3
|
||||
notfound such task - 4
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import yaml
|
||||
|
||||
from tasklib import agent
|
||||
from tasklib import config
|
||||
from tasklib import logger
|
||||
from tasklib import task
|
||||
from tasklib import utils
|
||||
|
||||
|
||||
class CmdApi(object):
|
||||
|
||||
def __init__(self):
|
||||
self.parser = argparse.ArgumentParser(
|
||||
description=textwrap.dedent(__doc__),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
self.subparser = self.parser.add_subparsers(
|
||||
title='actions',
|
||||
description='Supported actions',
|
||||
help='Provide of one valid actions')
|
||||
self.config = config.Config()
|
||||
self.register_options()
|
||||
self.register_actions()
|
||||
|
||||
def register_options(self):
|
||||
self.parser.add_argument(
|
||||
'--config', '-c', dest='config', default=None,
|
||||
help='Path to configuration file')
|
||||
self.parser.add_argument(
|
||||
'--debug', '-d', dest='debug', action='store_true', default=None)
|
||||
|
||||
def register_actions(self):
|
||||
task_arg = [(('task',), {'type': str})]
|
||||
self.register_parser('list')
|
||||
self.register_parser('conf')
|
||||
for name in ('run', 'daemon', 'report', 'status', 'show'):
|
||||
self.register_parser(name, task_arg)
|
||||
|
||||
def register_parser(self, func_name, arguments=()):
|
||||
parser = self.subparser.add_parser(func_name)
|
||||
parser.set_defaults(func=getattr(self, func_name))
|
||||
for args, kwargs in arguments:
|
||||
parser.add_argument(*args, **kwargs)
|
||||
|
||||
def parse(self, args):
|
||||
parsed = self.parser.parse_args(args)
|
||||
if parsed.config:
|
||||
self.config.update_from_file(parsed.config)
|
||||
if parsed.debug is not None:
|
||||
self.config['debug'] = parsed.debug
|
||||
logger.setup_logging(self.config)
|
||||
return parsed.func(parsed)
|
||||
|
||||
def list(self, args):
|
||||
for task_dir in utils.find_all_tasks(self.config):
|
||||
print(task.Task.task_from_dir(task_dir, self.config))
|
||||
|
||||
def show(self, args):
|
||||
meta = task.Task(args.task, self.config).metadata
|
||||
print(yaml.dump(meta, default_flow_style=False))
|
||||
|
||||
def run(self, args):
|
||||
task_agent = agent.TaskAgent(args.task, self.config)
|
||||
task_agent.run()
|
||||
status = task_agent.status()
|
||||
print(status)
|
||||
return task_agent.code()
|
||||
|
||||
def daemon(self, args):
|
||||
task_agent = agent.TaskAgent(args.task, self.config)
|
||||
task_agent.daemon()
|
||||
|
||||
def report(self, args):
|
||||
task_agent = agent.TaskAgent(args.task, self.config)
|
||||
print(task_agent.report())
|
||||
|
||||
def status(self, args):
|
||||
task_agent = agent.TaskAgent(args.task, self.config)
|
||||
exit_code = task_agent.code()
|
||||
print(task_agent.status())
|
||||
return exit_code
|
||||
|
||||
def conf(self, args):
|
||||
print(self.config)
|
||||
|
||||
|
||||
def main():
|
||||
api = CmdApi()
|
||||
exit_code = api.parse(sys.argv[1:])
|
||||
exit(exit_code)
|
|
@ -0,0 +1,57 @@
|
|||
# Copyright 2014 Mirantis, 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 os
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Config(object):
|
||||
|
||||
def __init__(self, config_file=None):
|
||||
self.curdir = os.getcwd()
|
||||
self.config = self.default_config
|
||||
if config_file:
|
||||
self.update_from_file(config_file)
|
||||
|
||||
@property
|
||||
def default_config(self):
|
||||
return {
|
||||
'library_dir': '/etc/puppet/tasks',
|
||||
'puppet_modules': '/etc/puppet/modules',
|
||||
'puppet_options': ('--logdest syslog '
|
||||
'--logdest /var/log/puppet.log'
|
||||
'--trace --no-report'),
|
||||
'report_dir': '/var/tmp/task_report',
|
||||
'pid_dir': '/var/tmp/task_run',
|
||||
'puppet_manifest': 'site.pp',
|
||||
'status_file': 'status',
|
||||
'debug': True,
|
||||
'task_file': 'task.yaml',
|
||||
'log_file': '/var/log/tasklib.log'}
|
||||
|
||||
def update_from_file(self, config_file):
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file) as f:
|
||||
loaded = yaml.load(f.read())
|
||||
self.config.update(loaded)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.config.get(key, None)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.config[key] = value
|
||||
|
||||
def __repr__(self):
|
||||
return yaml.dump(self.config, default_flow_style=False)
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
||||
|
||||
|
||||
class TasklibException(Exception):
|
||||
|
||||
msg = 'Tasklib generic error'
|
||||
|
||||
|
||||
class NotFound(TasklibException):
|
||||
|
||||
msg = 'No task with provided name'
|
||||
|
||||
|
||||
class NotValidMetadata(TasklibException):
|
||||
|
||||
msg = 'Missing critical items in metadata'
|
||||
|
||||
|
||||
class Failed(TasklibException):
|
||||
|
||||
msg = 'Task failed'
|
|
@ -0,0 +1,37 @@
|
|||
# Copyright 2014 Mirantis, 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 logging
|
||||
import sys
|
||||
|
||||
|
||||
def setup_logging(config):
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(process)d (%(module)s) %(message)s',
|
||||
"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if sys.stdout.isatty():
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.DEBUG)
|
||||
stream_handler.setFormatter(formatter)
|
||||
logger.addHandler(stream_handler)
|
||||
|
||||
if config['log_file']:
|
||||
file_handler = logging.FileHandler(config['log_file'])
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
return logger
|
|
@ -0,0 +1,79 @@
|
|||
# Copyright 2014 Mirantis, 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 logging
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
from tasklib.actions import exec_action
|
||||
from tasklib.actions import puppet
|
||||
from tasklib import exceptions
|
||||
|
||||
# use stevedore here
|
||||
type_mapping = {'exec': exec_action.ExecAction,
|
||||
'puppet': puppet.PuppetAction}
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Task(object):
|
||||
"""Unit of execution. Contains pre/post/run subtasks."""
|
||||
|
||||
def __init__(self, task_name, config):
|
||||
self.config = config
|
||||
self.name = task_name
|
||||
self.dir = os.path.abspath(
|
||||
os.path.join(config['library_dir'], self.name))
|
||||
self.file = os.path.abspath(
|
||||
os.path.join(self.dir, config['task_file']))
|
||||
self.pid_dir = os.path.abspath(
|
||||
os.path.join(self.config['pid_dir'], self.name))
|
||||
self.report_dir = os.path.abspath(
|
||||
os.path.join(self.config['report_dir'], self.name))
|
||||
self.status_file = os.path.abspath(os.path.join(
|
||||
self.report_dir, self.config['status_file']))
|
||||
self._metadata = {}
|
||||
log.debug('Init task %s with task file %s', self.name, self.file)
|
||||
|
||||
def verify(self):
|
||||
if not os.path.exists(self.file):
|
||||
raise exceptions.NotFound()
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
if self._metadata:
|
||||
return self._metadata
|
||||
with open(self.file) as f:
|
||||
self._metadata = yaml.load(f.read())
|
||||
return self._metadata
|
||||
|
||||
@classmethod
|
||||
def task_from_dir(cls, task_dir, config):
|
||||
path = task_dir.replace(config['library_dir'], '').split('/')
|
||||
task_name = '/'.join((p for p in path if p))
|
||||
return cls(task_name, config)
|
||||
|
||||
def __repr__(self):
|
||||
return "{0:10} | {1:15}".format(self.name, self.dir)
|
||||
|
||||
def run(self):
|
||||
"""Will be used to run a task."""
|
||||
self.verify()
|
||||
action_class = type_mapping.get(self.metadata.get('type'))
|
||||
if action_class is None:
|
||||
raise exceptions.NotValidMetadata()
|
||||
action = action_class(self, self.config)
|
||||
action.verify()
|
||||
return action.run()
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
|
@ -0,0 +1,52 @@
|
|||
# Copyright 2014 Mirantis, 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 os
|
||||
import time
|
||||
|
||||
import unittest
|
||||
|
||||
from tasklib import utils
|
||||
from tasklib.utils import STATUS
|
||||
|
||||
|
||||
class BaseUnitTest(unittest.TestCase):
|
||||
"""Tasklib base unittest."""
|
||||
|
||||
|
||||
class BaseFunctionalTest(BaseUnitTest):
|
||||
|
||||
def setUp(self):
|
||||
self.dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
self.conf_path = os.path.join(self.dir_path, 'functional', 'conf.yaml')
|
||||
self.base_command = ['taskcmd', '-c', self.conf_path]
|
||||
|
||||
def check_puppet_installed(self):
|
||||
exit_code, out, err = utils.execute('which puppet')
|
||||
if exit_code == 1:
|
||||
self.skipTest('Puppet is not installed')
|
||||
|
||||
def execute(self, add_command):
|
||||
command = self.base_command + add_command
|
||||
cmd = ' '.join(command)
|
||||
return utils.execute(cmd)
|
||||
|
||||
def wait_ready(self, cmd, timeout):
|
||||
started = time.time()
|
||||
while time.time() - started < timeout:
|
||||
exit_code, out, err = self.execute(cmd)
|
||||
if out.strip('\n') != STATUS.running.name:
|
||||
return exit_code, out, err
|
||||
self.fail('Command {0} failed to finish with timeout {1}'.format(
|
||||
cmd, timeout))
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
|
@ -0,0 +1,5 @@
|
|||
library_dir: tasklib/tests/functional
|
||||
pid_dir: tmp/task_run
|
||||
report_dir: tmp/task_report
|
||||
debug: true
|
||||
log_file: tasklib.log
|
|
@ -0,0 +1,2 @@
|
|||
type: exec
|
||||
description: "Should fail on validation"
|
|
@ -0,0 +1,3 @@
|
|||
type: exec
|
||||
cmd: 'echo 42 | grep 100'
|
||||
description: "grep will return 1, if nothing will be found"
|
|
@ -0,0 +1,3 @@
|
|||
type: exec
|
||||
cmd: sleep 1.5
|
||||
description: "Will be used for testing stop action"
|
|
@ -0,0 +1,3 @@
|
|||
type: exec
|
||||
cmd: echo 1
|
||||
description: "Most primitive task available"
|
|
@ -0,0 +1,3 @@
|
|||
exec { "which which":
|
||||
path => ["/usr/bin", "/usr/sbin"]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
type: puppet
|
|
@ -0,0 +1,6 @@
|
|||
file {"tasklibtest":
|
||||
path => "/tmp/tasklibtest",
|
||||
ensure => present,
|
||||
mode => 0640,
|
||||
content => "I'm a file created created by tasklib test",
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
type: puppet
|
||||
puppet_manifest: file.pp
|
|
@ -0,0 +1 @@
|
|||
file {"invalid":
|
|
@ -0,0 +1,2 @@
|
|||
type: puppet
|
||||
description: "site.pp is default manifest provided by config"
|
|
@ -0,0 +1,3 @@
|
|||
exec { "echo 15":
|
||||
path => ["/bin", "sbin"]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
type: puppet
|
||||
description: 'Just sleep fot 1 seconds'
|
|
@ -0,0 +1,34 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
||||
|
||||
from tasklib.tests import base
|
||||
from tasklib.utils import STATUS
|
||||
|
||||
|
||||
class TestDaemon(base.BaseFunctionalTest):
|
||||
|
||||
def test_exec_long_task(self):
|
||||
exit_code, out, err = self.execute(['daemon', 'exec/long'])
|
||||
self.assertEqual(exit_code, 0)
|
||||
exit_code, out, err = self.wait_ready(['status', 'exec/long'], 2)
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(out.strip('\n'), STATUS.end.name)
|
||||
|
||||
def test_puppet_simple_daemon(self):
|
||||
self.check_puppet_installed()
|
||||
exit_code, out, err = self.execute(['daemon', 'puppet/sleep'])
|
||||
self.assertEqual(exit_code, 0)
|
||||
exit_code, out, err = self.wait_ready(['status', 'puppet/sleep'], 10)
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(out.strip('\n'), STATUS.end.name)
|
|
@ -0,0 +1,51 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
||||
|
||||
from tasklib.tests import base
|
||||
from tasklib.utils import STATUS
|
||||
|
||||
|
||||
class TestFunctionalExecTasks(base.BaseFunctionalTest):
|
||||
"""Each test will follow next pattern:
|
||||
1. Run test with provided name - taskcmd -c conf.yaml run test/test
|
||||
2. check status of task
|
||||
"""
|
||||
|
||||
def test_simple_run(self):
|
||||
exit_code, out, err = self.execute(['run', 'exec/simple'])
|
||||
self.assertEqual(exit_code, 0)
|
||||
exit_code, out, err = self.execute(['status', 'exec/simple'])
|
||||
self.assertEqual(out.strip('\n'), STATUS.end.name)
|
||||
self.assertEqual(exit_code, 0)
|
||||
|
||||
def test_failed_run(self):
|
||||
exit_code, out, err = self.execute(['run', 'exec/fail'])
|
||||
self.assertEqual(exit_code, 2)
|
||||
exit_code, out, err = self.execute(['status', 'exec/fail'])
|
||||
self.assertEqual(out.strip('\n'), STATUS.failed.name)
|
||||
self.assertEqual(exit_code, 2)
|
||||
|
||||
def test_error(self):
|
||||
exit_code, out, err = self.execute(['run', 'exec/error'])
|
||||
self.assertEqual(exit_code, 3)
|
||||
exit_code, out, err = self.execute(['status', 'exec/error'])
|
||||
self.assertEqual(out.strip('\n'), STATUS.error.name)
|
||||
self.assertEqual(exit_code, 3)
|
||||
|
||||
def test_notfound(self):
|
||||
exit_code, out, err = self.execute(['run', 'exec/notfound'])
|
||||
self.assertEqual(exit_code, 4)
|
||||
exit_code, out, err = self.execute(['status', 'exec/notfound'])
|
||||
self.assertEqual(out.strip('\n'), STATUS.notfound.name)
|
||||
self.assertEqual(exit_code, 4)
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2014 Mirantis, 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 os
|
||||
|
||||
from tasklib.tests import base
|
||||
|
||||
|
||||
class TestFunctionalExecTasks(base.BaseFunctionalTest):
|
||||
"""Each test will follow next pattern:
|
||||
1. Run test with provided name - taskcmd -c conf.yaml run test/test
|
||||
2. check status of task
|
||||
"""
|
||||
|
||||
def test_puppet_file(self):
|
||||
test_file = '/tmp/tasklibtest'
|
||||
if os.path.exists(test_file):
|
||||
os.unlink(test_file)
|
||||
exit_code, out, err = self.execute(['run', 'puppet/file'])
|
||||
self.assertEqual(exit_code, 0)
|
||||
|
||||
def test_puppet_invalid(self):
|
||||
exit_code, out, err = self.execute(['run', 'puppet/invalid'])
|
||||
self.assertEqual(exit_code, 2)
|
||||
|
||||
def test_puppet_cmd(self):
|
||||
exit_code, out, err = self.execute(['run', 'puppet/cmd'])
|
||||
self.assertEqual(exit_code, 0)
|
|
@ -0,0 +1,37 @@
|
|||
# Copyright 2014 Mirantis, 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 mock
|
||||
|
||||
from tasklib import cli
|
||||
from tasklib.tests import base
|
||||
|
||||
|
||||
class TestCmdApi(base.BaseUnitTest):
|
||||
|
||||
def setUp(self):
|
||||
self.api = cli.CmdApi()
|
||||
self.api.config['log_file'] = None
|
||||
|
||||
@mock.patch('tasklib.cli.task.Task.task_from_dir')
|
||||
@mock.patch('tasklib.cli.utils.find_all_tasks')
|
||||
def test_list(self, mfind, mtask):
|
||||
tasks = ['/etc/library/test/deploy', '/etc/library/test/rollback']
|
||||
mfind.return_value = tasks
|
||||
self.api.parse(['list'])
|
||||
mfind.assert_called_once_with(self.api.config)
|
||||
expected_calls = []
|
||||
for t in tasks:
|
||||
expected_calls.append(mock.call(t, self.api.config))
|
||||
self.assertEqual(expected_calls, mtask.call_args_list)
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright 2014 Mirantis, 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 mock
|
||||
import yaml
|
||||
|
||||
from tasklib import config
|
||||
from tasklib.tests import base
|
||||
|
||||
|
||||
@mock.patch('tasklib.task.os.path.exists')
|
||||
class TestConfig(base.BaseUnitTest):
|
||||
|
||||
def test_default_config_when_no_file_exists(self, mexists):
|
||||
mexists.return_value = False
|
||||
conf = config.Config(config_file='/etc/tasklib/test.yaml')
|
||||
self.assertEqual(conf.default_config, conf.config)
|
||||
|
||||
def test_default_when_no_file_provided(self, mexists):
|
||||
conf = config.Config()
|
||||
self.assertEqual(conf.default_config, conf.config)
|
||||
|
||||
def test_non_default_config_from_valid_yaml(self, mexists):
|
||||
mexists.return_value = True
|
||||
provided = {'library_dir': '/var/run/tasklib',
|
||||
'puppet_manifest': 'init.pp'}
|
||||
mopen = mock.mock_open(read_data=yaml.dump(provided))
|
||||
with mock.patch('tasklib.config.open', mopen, create=True):
|
||||
conf = config.Config(config_file='/etc/tasklib/test.yaml')
|
||||
self.assertNotEqual(
|
||||
conf.config['library_dir'], conf.default_config['library_dir'])
|
||||
self.assertEqual(
|
||||
conf.config['library_dir'], provided['library_dir'])
|
||||
self.assertNotEqual(
|
||||
conf.config['puppet_manifest'],
|
||||
conf.default_config['puppet_manifest'])
|
||||
self.assertEqual(
|
||||
conf.config['puppet_manifest'], provided['puppet_manifest'])
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright 2014 Mirantis, 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 mock
|
||||
import yaml
|
||||
|
||||
from tasklib import config
|
||||
from tasklib import task
|
||||
from tasklib.tests import base
|
||||
|
||||
|
||||
@mock.patch('tasklib.task.os.path.exists')
|
||||
@mock.patch('tasklib.utils.execute')
|
||||
class TestExecTask(base.BaseUnitTest):
|
||||
|
||||
def setUp(self):
|
||||
self.meta = {'cmd': 'echo 1',
|
||||
'type': 'exec'}
|
||||
self.only_required = {'type': 'puppet'}
|
||||
self.config = config.Config()
|
||||
|
||||
def test_base_cmd_task(self, mexecute, mexists):
|
||||
mexists.return_value = True
|
||||
mexecute.return_value = (0, '', '')
|
||||
mopen = mock.mock_open(read_data=yaml.dump(self.meta))
|
||||
puppet_task = task.Task('test/cmd', self.config)
|
||||
with mock.patch('tasklib.task.open', mopen, create=True):
|
||||
puppet_task.run()
|
||||
expected_cmd = 'echo 1'
|
||||
mexecute.assert_called_once_with(expected_cmd)
|
|
@ -0,0 +1,63 @@
|
|||
# Copyright 2014 Mirantis, 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 mock
|
||||
import yaml
|
||||
|
||||
from tasklib import config
|
||||
from tasklib import task
|
||||
from tasklib.tests import base
|
||||
|
||||
|
||||
@mock.patch('tasklib.task.os.path.exists')
|
||||
@mock.patch('tasklib.utils.execute')
|
||||
class TestPuppetTask(base.BaseUnitTest):
|
||||
|
||||
def setUp(self):
|
||||
self.meta = {'puppet_manifest': 'init.pp',
|
||||
'puppet_options': '--debug',
|
||||
'puppet_modules': '/etc/my_puppet_modules',
|
||||
'type': 'puppet'}
|
||||
self.only_required = {'type': 'puppet'}
|
||||
self.config = config.Config()
|
||||
|
||||
def test_basic_puppet_action(self, mexecute, mexists):
|
||||
mexists.return_value = True
|
||||
mexecute.return_value = (0, '', '')
|
||||
mopen = mock.mock_open(read_data=yaml.dump(self.meta))
|
||||
puppet_task = task.Task('test/puppet', self.config)
|
||||
with mock.patch('tasklib.task.open', mopen, create=True):
|
||||
puppet_task.run()
|
||||
expected_cmd = ['puppet', 'apply', '--detailed-exitcodes',
|
||||
'--modulepath=/etc/my_puppet_modules',
|
||||
'--debug']
|
||||
expected = ' '.join(expected_cmd)
|
||||
self.assertEqual(mexecute.call_count, 1)
|
||||
received = mexecute.call_args[0][0]
|
||||
self.assertTrue(expected in received)
|
||||
|
||||
def test_default_puppet_action(self, mexecute, mexists):
|
||||
mexists.return_value = True
|
||||
mexecute.return_value = (0, '', '')
|
||||
mopen = mock.mock_open(read_data=yaml.dump(self.only_required))
|
||||
puppet_task = task.Task('test/puppet/only_required', self.config)
|
||||
with mock.patch('tasklib.task.open', mopen, create=True):
|
||||
puppet_task.run()
|
||||
expected_cmd = [
|
||||
'puppet', 'apply', '--detailed-exitcodes',
|
||||
'--modulepath={0}'.format(self.config['puppet_modules'])]
|
||||
expected = ' '.join(expected_cmd)
|
||||
self.assertEqual(mexecute.call_count, 1)
|
||||
received = mexecute.call_args[0][0]
|
||||
self.assertTrue(expected in received)
|
|
@ -0,0 +1,57 @@
|
|||
# Copyright 2014 Mirantis, 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 os
|
||||
|
||||
import mock
|
||||
import yaml
|
||||
|
||||
from tasklib import config
|
||||
from tasklib import exceptions
|
||||
from tasklib import task
|
||||
from tasklib.tests import base
|
||||
|
||||
|
||||
@mock.patch('tasklib.task.os.path.exists')
|
||||
class TestBaseTask(base.BaseUnitTest):
|
||||
"""Basic task tests."""
|
||||
|
||||
def setUp(self):
|
||||
self.conf = config.Config()
|
||||
|
||||
def test_create_task_from_path(self, mexists):
|
||||
name = 'ceph/deploy'
|
||||
task_dir = os.path.join(self.conf['library_dir'], name)
|
||||
test_task = task.Task.task_from_dir(task_dir, self.conf)
|
||||
self.assertEqual(test_task.name, name)
|
||||
self.assertEqual(test_task.dir, task_dir)
|
||||
|
||||
def test_verify_raises_not_found(self, mexists):
|
||||
mexists.return_value = False
|
||||
test_task = task.Task('ceph/deploy', self.conf)
|
||||
self.assertRaises(exceptions.NotFound, test_task.verify)
|
||||
|
||||
def test_verify_nothing_happens_if_file_exists(self, mexists):
|
||||
mexists.return_value = True
|
||||
test_task = task.Task('ceph/deploy', self.conf)
|
||||
test_task.verify()
|
||||
|
||||
def test_read_metadata_from_valid_yaml(self, mexists):
|
||||
mexists.return_value = True
|
||||
meta = {'report_dir': '/tmp/report_dir',
|
||||
'pid_dir': '/tmp/pid_dir'}
|
||||
mopen = mock.mock_open(read_data=yaml.dump(meta))
|
||||
test_task = task.Task('ceph/deploy', self.conf)
|
||||
with mock.patch('tasklib.task.open', mopen, create=True):
|
||||
self.assertEqual(meta, test_task.metadata)
|
|
@ -0,0 +1,51 @@
|
|||
# Copyright 2014 Mirantis, 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.
|
||||
|
||||
from collections import namedtuple
|
||||
import fnmatch
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
Status = namedtuple('Status', ['name', 'code'])
|
||||
|
||||
|
||||
def key_value_enum(enums):
|
||||
enums = dict([(k, Status(k, v)) for k, v in enums.iteritems()])
|
||||
return type('Enum', (), enums)
|
||||
|
||||
|
||||
STATUS = key_value_enum({'running': 1,
|
||||
'end': 0,
|
||||
'error': 3,
|
||||
'notfound': 4,
|
||||
'failed': 2})
|
||||
|
||||
|
||||
def find_all_tasks(config):
|
||||
for root, dirnames, filenames in os.walk(config['library_dir']):
|
||||
for filename in fnmatch.filter(filenames, config['task_file']):
|
||||
yield os.path.dirname(os.path.join(root, filename))
|
||||
|
||||
|
||||
def ensure_dir_created(path):
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
|
||||
def execute(cmd):
|
||||
command = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
||||
stdout, stderr = command.communicate()
|
||||
return command.returncode, stdout, stderr
|
|
@ -0,0 +1,4 @@
|
|||
-r requirements.txt
|
||||
mock
|
||||
nose
|
||||
tox
|
|
@ -0,0 +1,38 @@
|
|||
[tox]
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
envlist = py26,py27,pep8
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install {packages}
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
nosetests {posargs:tasklib}
|
||||
|
||||
[tox:jenkins]
|
||||
downloadcache = ~/cache/pip
|
||||
|
||||
[testenv:pep8]
|
||||
deps = hacking==0.7
|
||||
usedevelop = False
|
||||
commands =
|
||||
flake8 {posargs:tasklib}
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs:}
|
||||
|
||||
[testenv:devenv]
|
||||
envdir = devenv
|
||||
usedevelop = True
|
||||
|
||||
[flake8]
|
||||
ignore = H234,H302,H802
|
||||
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools,__init__.py,docs
|
||||
show-pep8 = True
|
||||
show-source = True
|
||||
count = True
|
||||
|
||||
[hacking]
|
||||
import_exceptions = testtools.matchers
|
Loading…
Reference in New Issue