mirror of
https://github.com/valitydev/salt.git
synced 2024-11-08 09:23:56 +00:00
Merge pull request #28824 from rallytime/bp-28778-and-28820
Back-port #28778 and #28820 to 2015.8
This commit is contained in:
commit
08891cb210
37
salt/grains/chronos.py
Normal file
37
salt/grains/chronos.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Generate chronos proxy minion grains.
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
|
||||||
|
import salt.utils
|
||||||
|
import salt.utils.http
|
||||||
|
__proxyenabled__ = ['chronos']
|
||||||
|
__virtualname__ = 'chronos'
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
if not salt.utils.is_proxy() or 'proxy' not in __opts__:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return __virtualname__
|
||||||
|
|
||||||
|
|
||||||
|
def kernel():
|
||||||
|
return {'kernel': 'chronos'}
|
||||||
|
|
||||||
|
|
||||||
|
def os():
|
||||||
|
return {'os': 'chronos'}
|
||||||
|
|
||||||
|
|
||||||
|
def os_family():
|
||||||
|
return {'os_family': 'chronos'}
|
||||||
|
|
||||||
|
|
||||||
|
def os_data():
|
||||||
|
return {'os_data': 'chronos'}
|
50
salt/grains/marathon.py
Normal file
50
salt/grains/marathon.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Generate marathon proxy minion grains.
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import salt.utils
|
||||||
|
import salt.utils.http
|
||||||
|
__proxyenabled__ = ['marathon']
|
||||||
|
__virtualname__ = 'marathon'
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
if not salt.utils.is_proxy() or 'proxy' not in __opts__:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return __virtualname__
|
||||||
|
|
||||||
|
|
||||||
|
def kernel():
|
||||||
|
return {'kernel': 'marathon'}
|
||||||
|
|
||||||
|
|
||||||
|
def os():
|
||||||
|
return {'os': 'marathon'}
|
||||||
|
|
||||||
|
|
||||||
|
def os_family():
|
||||||
|
return {'os_family': 'marathon'}
|
||||||
|
|
||||||
|
|
||||||
|
def os_data():
|
||||||
|
return {'os_data': 'marathon'}
|
||||||
|
|
||||||
|
|
||||||
|
def marathon():
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/v2/info".format(__opts__['proxy'].get(
|
||||||
|
'base_url',
|
||||||
|
"http://locahost:8080",
|
||||||
|
)),
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
if not response or 'dict' not in response:
|
||||||
|
return {'marathon': None}
|
||||||
|
return {'marathon': response['dict']}
|
132
salt/modules/chronos.py
Normal file
132
salt/modules/chronos.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Module providing a simple management interface to a chronos cluster.
|
||||||
|
|
||||||
|
Currently this only works when run through a proxy minion.
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import salt.utils
|
||||||
|
import salt.utils.http
|
||||||
|
|
||||||
|
|
||||||
|
__proxyenabled__ = ['chronos']
|
||||||
|
log = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
# only valid in proxy minions for now
|
||||||
|
return salt.utils.is_proxy() and 'proxy' in __opts__
|
||||||
|
|
||||||
|
|
||||||
|
def _base_url():
|
||||||
|
'''
|
||||||
|
Return the proxy configured base url.
|
||||||
|
'''
|
||||||
|
base_url = "http://locahost:4400"
|
||||||
|
if 'proxy' in __opts__:
|
||||||
|
base_url = __opts__['proxy'].get('base_url', base_url)
|
||||||
|
return base_url
|
||||||
|
|
||||||
|
|
||||||
|
def _jobs():
|
||||||
|
'''
|
||||||
|
Return the currently configured jobs.
|
||||||
|
'''
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/scheduler/jobs".format(_base_url()),
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
jobs = {}
|
||||||
|
for job in response['dict']:
|
||||||
|
jobs[job.pop('name')] = job
|
||||||
|
return jobs
|
||||||
|
|
||||||
|
|
||||||
|
def jobs():
|
||||||
|
'''
|
||||||
|
Return a list of the currently installed job names.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt chronos-minion-id chronos.jobs
|
||||||
|
'''
|
||||||
|
job_names = _jobs().keys()
|
||||||
|
job_names.sort()
|
||||||
|
return {'jobs': job_names}
|
||||||
|
|
||||||
|
|
||||||
|
def has_job(name):
|
||||||
|
'''
|
||||||
|
Return whether the given job is currently configured.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt chronos-minion-id chronos.has_job my-job
|
||||||
|
'''
|
||||||
|
return name in _jobs()
|
||||||
|
|
||||||
|
|
||||||
|
def job(name):
|
||||||
|
'''
|
||||||
|
Return the current server configuration for the specified job.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt chronos-minion-id chronos.job my-job
|
||||||
|
'''
|
||||||
|
jobs = _jobs()
|
||||||
|
if name in jobs:
|
||||||
|
return {'job': jobs[name]}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def update_job(name, config):
|
||||||
|
'''
|
||||||
|
Update the specified job with the given configuration.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt chronos-minion-id chronos.update_job my-job '<config yaml>'
|
||||||
|
'''
|
||||||
|
if 'name' not in config:
|
||||||
|
config['name'] = name
|
||||||
|
data = json.dumps(config)
|
||||||
|
try:
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/scheduler/iso8601".format(_base_url()),
|
||||||
|
method='POST',
|
||||||
|
data=data,
|
||||||
|
header_dict={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
log.debug('update response: %s', response)
|
||||||
|
return {'success': True}
|
||||||
|
except Exception as ex:
|
||||||
|
log.error('unable to update chronos job: %s', ex.message)
|
||||||
|
return {
|
||||||
|
'exception': {
|
||||||
|
'message': ex.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rm_job(name):
|
||||||
|
'''
|
||||||
|
Remove the specified job from the server.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt chronos-minion-id chronos.rm_job my-job
|
||||||
|
'''
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/scheduler/job/{1}".format(_base_url(), name),
|
||||||
|
method='DELETE',
|
||||||
|
)
|
||||||
|
return True
|
153
salt/modules/marathon.py
Normal file
153
salt/modules/marathon.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Module providing a simple management interface to a marathon cluster.
|
||||||
|
|
||||||
|
Currently this only works when run through a proxy minion.
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import salt.utils
|
||||||
|
import salt.utils.http
|
||||||
|
|
||||||
|
|
||||||
|
__proxyenabled__ = ['marathon']
|
||||||
|
log = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
# only valid in proxy minions for now
|
||||||
|
return salt.utils.is_proxy() and 'proxy' in __opts__
|
||||||
|
|
||||||
|
|
||||||
|
def _base_url():
|
||||||
|
'''
|
||||||
|
Return the proxy configured base url.
|
||||||
|
'''
|
||||||
|
base_url = "http://locahost:8080"
|
||||||
|
if 'proxy' in __opts__:
|
||||||
|
base_url = __opts__['proxy'].get('base_url', base_url)
|
||||||
|
return base_url
|
||||||
|
|
||||||
|
|
||||||
|
def _app_id(app_id):
|
||||||
|
'''
|
||||||
|
Make sure the app_id is in the correct format.
|
||||||
|
'''
|
||||||
|
if app_id[0] != '/':
|
||||||
|
app_id = '/{0}'.format(app_id)
|
||||||
|
return app_id
|
||||||
|
|
||||||
|
|
||||||
|
def apps():
|
||||||
|
'''
|
||||||
|
Return a list of the currently installed app ids.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt marathon-minion-id marathon.apps
|
||||||
|
'''
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/v2/apps".format(_base_url()),
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
return {'apps': [app['id'] for app in response['dict']['apps']]}
|
||||||
|
|
||||||
|
|
||||||
|
def has_app(id):
|
||||||
|
'''
|
||||||
|
Return whether the given app id is currently configured.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt marathon-minion-id marathon.has_app my-app
|
||||||
|
'''
|
||||||
|
return _app_id(id) in apps()['apps']
|
||||||
|
|
||||||
|
|
||||||
|
def app(id):
|
||||||
|
'''
|
||||||
|
Return the current server configuration for the specified app.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt marathon-minion-id marathon.app my-app
|
||||||
|
'''
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/v2/apps/{1}".format(_base_url(), id),
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
return response['dict']
|
||||||
|
|
||||||
|
|
||||||
|
def update_app(id, config):
|
||||||
|
'''
|
||||||
|
Update the specified app with the given configuration.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt marathon-minion-id marathon.update_app my-app '<config yaml>'
|
||||||
|
'''
|
||||||
|
if 'id' not in config:
|
||||||
|
config['id'] = id
|
||||||
|
config.pop('version', None)
|
||||||
|
data = json.dumps(config)
|
||||||
|
try:
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/v2/apps/{1}?force=true".format(_base_url(), id),
|
||||||
|
method='PUT',
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
data=data,
|
||||||
|
header_dict={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
log.debug('update response: %s', response)
|
||||||
|
return response['dict']
|
||||||
|
except Exception as ex:
|
||||||
|
log.error('unable to update marathon app: %s', ex.message)
|
||||||
|
return {
|
||||||
|
'exception': {
|
||||||
|
'message': ex.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rm_app(id):
|
||||||
|
'''
|
||||||
|
Remove the specified app from the server.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt marathon-minion-id marathon.rm_app my-app
|
||||||
|
'''
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/v2/apps/{1}".format(_base_url(), id),
|
||||||
|
method='DELETE',
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
return response['dict']
|
||||||
|
|
||||||
|
|
||||||
|
def info():
|
||||||
|
'''
|
||||||
|
Return configuration and status information about the marathon instance.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
.. code-block:: bash
|
||||||
|
salt marathon-minion-id marathon.info
|
||||||
|
'''
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/v2/info".format(_base_url()),
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
return response['dict']
|
83
salt/proxy/chronos.py
Normal file
83
salt/proxy/chronos.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Chronos
|
||||||
|
========
|
||||||
|
|
||||||
|
Proxy minion for managing a Chronos cluster.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
- :doc:`chronos execution module (salt.modules.chronos) </ref/modules/all/salt.modules.chronos>`
|
||||||
|
|
||||||
|
Pillar
|
||||||
|
------
|
||||||
|
|
||||||
|
The chronos proxy configuration requires a 'base_url' property that points to
|
||||||
|
the chronos endpoint:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
proxytype: chronos
|
||||||
|
base_url: http://my-chronos-master.mydomain.com:4400
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import salt.utils.http
|
||||||
|
|
||||||
|
|
||||||
|
__proxyenabled__ = ['chronos']
|
||||||
|
CONFIG = {}
|
||||||
|
CONFIG_BASE_URL = 'base_url'
|
||||||
|
log = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def init(opts):
|
||||||
|
'''
|
||||||
|
Perform any needed setup.
|
||||||
|
'''
|
||||||
|
if CONFIG_BASE_URL in opts['proxy']:
|
||||||
|
CONFIG[CONFIG_BASE_URL] = opts['proxy'][CONFIG_BASE_URL]
|
||||||
|
else:
|
||||||
|
log.error('missing proxy property %s', CONFIG_BASE_URL)
|
||||||
|
log.debug('CONFIG: %s', CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def ping():
|
||||||
|
'''
|
||||||
|
Is the chronos api responding?
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/scheduler/jobs".format(CONFIG[CONFIG_BASE_URL]),
|
||||||
|
decode_type='json',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
log.debug(
|
||||||
|
'chronos.info returned succesfully: %s',
|
||||||
|
response,
|
||||||
|
)
|
||||||
|
if 'dict' in response:
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
log.error(
|
||||||
|
'error pinging chronos with base_url %s: %s',
|
||||||
|
CONFIG[CONFIG_BASE_URL],
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown(opts):
|
||||||
|
'''
|
||||||
|
For this proxy shutdown is a no-op
|
||||||
|
'''
|
||||||
|
log.debug('chronos proxy shutdown() called...')
|
83
salt/proxy/marathon.py
Normal file
83
salt/proxy/marathon.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Marathon
|
||||||
|
========
|
||||||
|
|
||||||
|
Proxy minion for managing a Marathon cluster.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
- :doc:`marathon execution module (salt.modules.marathon) </ref/modules/all/salt.modules.marathon>`
|
||||||
|
|
||||||
|
Pillar
|
||||||
|
------
|
||||||
|
|
||||||
|
The marathon proxy configuration requires a 'base_url' property that points to
|
||||||
|
the marathon endpoint:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
proxytype: marathon
|
||||||
|
base_url: http://my-marathon-master.mydomain.com:8080
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import salt.utils.http
|
||||||
|
|
||||||
|
|
||||||
|
__proxyenabled__ = ['marathon']
|
||||||
|
CONFIG = {}
|
||||||
|
CONFIG_BASE_URL = 'base_url'
|
||||||
|
log = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def init(opts):
|
||||||
|
'''
|
||||||
|
Perform any needed setup.
|
||||||
|
'''
|
||||||
|
if CONFIG_BASE_URL in opts['proxy']:
|
||||||
|
CONFIG[CONFIG_BASE_URL] = opts['proxy'][CONFIG_BASE_URL]
|
||||||
|
else:
|
||||||
|
log.error('missing proxy property %s', CONFIG_BASE_URL)
|
||||||
|
log.debug('CONFIG: %s', CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def ping():
|
||||||
|
'''
|
||||||
|
Is the marathon api responding?
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
response = salt.utils.http.query(
|
||||||
|
"{0}/ping".format(CONFIG[CONFIG_BASE_URL]),
|
||||||
|
decode_type='plain',
|
||||||
|
decode=True,
|
||||||
|
)
|
||||||
|
log.debug(
|
||||||
|
'marathon.info returned succesfully: %s',
|
||||||
|
response,
|
||||||
|
)
|
||||||
|
if 'text' in response and response['text'].strip() == 'pong':
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
log.error(
|
||||||
|
'error calling marathon.info with base_url %s: %s',
|
||||||
|
CONFIG[CONFIG_BASE_URL],
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown(opts):
|
||||||
|
'''
|
||||||
|
For this proxy shutdown is a no-op
|
||||||
|
'''
|
||||||
|
log.debug('marathon proxy shutdown() called...')
|
138
salt/states/chronos_job.py
Normal file
138
salt/states/chronos_job.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Configure Chronos jobs via a salt proxy.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
my_job:
|
||||||
|
chronos_job.config:
|
||||||
|
- config:
|
||||||
|
schedule: "R//PT2S"
|
||||||
|
command: "echo 'hi'"
|
||||||
|
owner: "me@mycompany.com"
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import salt.utils.configcomparer
|
||||||
|
|
||||||
|
__proxyenabled__ = ['chronos']
|
||||||
|
log = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def config(name, config):
|
||||||
|
'''
|
||||||
|
Ensure that the chronos job with the given name is present and is configured
|
||||||
|
to match the given config values.
|
||||||
|
|
||||||
|
:param name: The job name
|
||||||
|
:param config: The configuration to apply (dict)
|
||||||
|
:return: A standard Salt changes dictionary
|
||||||
|
'''
|
||||||
|
# setup return structure
|
||||||
|
ret = {
|
||||||
|
'name': name,
|
||||||
|
'changes': {},
|
||||||
|
'result': False,
|
||||||
|
'comment': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# get existing config if job is present
|
||||||
|
existing_config = None
|
||||||
|
if __salt__['chronos.has_job'](name):
|
||||||
|
existing_config = __salt__['chronos.job'](name)['job']
|
||||||
|
|
||||||
|
# compare existing config with defined config
|
||||||
|
if existing_config:
|
||||||
|
update_config = copy.deepcopy(existing_config)
|
||||||
|
salt.utils.configcomparer.compare_and_update_config(
|
||||||
|
config,
|
||||||
|
update_config,
|
||||||
|
ret['changes'],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# the job is not configured--we need to create it from scratch
|
||||||
|
ret['changes']['job'] = {
|
||||||
|
'new': config,
|
||||||
|
'old': None,
|
||||||
|
}
|
||||||
|
update_config = config
|
||||||
|
|
||||||
|
if ret['changes']:
|
||||||
|
# if the only change is in schedule, check to see if patterns are equivalent
|
||||||
|
if 'schedule' in ret['changes'] and len(ret['changes']) == 1:
|
||||||
|
if 'new' in ret['changes']['schedule'] and 'old' in ret['changes']['schedule']:
|
||||||
|
new = ret['changes']['schedule']['new']
|
||||||
|
log.debug('new schedule: %s', new)
|
||||||
|
old = ret['changes']['schedule']['old']
|
||||||
|
log.debug('old schedule: %s', old)
|
||||||
|
if new and old:
|
||||||
|
_new = new.split('/')
|
||||||
|
log.debug('_new schedule: %s', _new)
|
||||||
|
_old = old.split('/')
|
||||||
|
log.debug('_old schedule: %s', _old)
|
||||||
|
if len(_new) == 3 and len(_old) == 3:
|
||||||
|
log.debug('_new[0] == _old[0]: %s', str(_new[0]) == str(_old[0]))
|
||||||
|
log.debug('_new[2] == _old[2]: %s', str(_new[2]) == str(_old[2]))
|
||||||
|
if str(_new[0]) == str(_old[0]) and str(_new[2]) == str(_old[2]):
|
||||||
|
log.debug('schedules match--no need for changes')
|
||||||
|
ret['changes'] = {}
|
||||||
|
|
||||||
|
# update the config if we registered any changes
|
||||||
|
log.debug('schedules match--no need for changes')
|
||||||
|
if ret['changes']:
|
||||||
|
# if test report there will be an update
|
||||||
|
if __opts__['test']:
|
||||||
|
ret['result'] = None
|
||||||
|
ret['comment'] = 'Chronos job {0} is set to be updated'.format(
|
||||||
|
name
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
update_result = __salt__['chronos.update_job'](name, update_config)
|
||||||
|
if 'exception' in update_result:
|
||||||
|
ret['result'] = False
|
||||||
|
ret['comment'] = 'Failed to update job config for {0}: {1}'.format(
|
||||||
|
name,
|
||||||
|
update_result['exception'],
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'Updated job config for {0}'.format(name)
|
||||||
|
return ret
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'Chronos job {0} configured correctly'.format(name)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def absent(name):
|
||||||
|
'''
|
||||||
|
Ensure that the chronos job with the given name is not present.
|
||||||
|
|
||||||
|
:param name: The app name
|
||||||
|
:return: A standard Salt changes dictionary
|
||||||
|
'''
|
||||||
|
ret = {'name': name,
|
||||||
|
'changes': {},
|
||||||
|
'result': False,
|
||||||
|
'comment': ''}
|
||||||
|
if not __salt__['chronos.has_job'](name):
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'Job {0} already absent'.format(name)
|
||||||
|
return ret
|
||||||
|
if __opts__['test']:
|
||||||
|
ret['result'] = None
|
||||||
|
ret['comment'] = 'Job {0} is set to be removed'.format(name)
|
||||||
|
return ret
|
||||||
|
if __salt__['chronos.rm_job'](name):
|
||||||
|
ret['changes'] = {'job': name}
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'Removed job {0}'.format(name)
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
ret['result'] = False
|
||||||
|
ret['comment'] = 'Failed to remove job {0}'.format(name)
|
||||||
|
return ret
|
118
salt/states/marathon_app.py
Normal file
118
salt/states/marathon_app.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
Configure Marathon apps via a salt proxy.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
my_app:
|
||||||
|
marathon_app.config:
|
||||||
|
- config:
|
||||||
|
cmd: "while [ true ] ; do echo 'Hello Marathon' ; sleep 5 ; done"
|
||||||
|
cpus: 0.1
|
||||||
|
mem: 10
|
||||||
|
instances: 3
|
||||||
|
|
||||||
|
.. versionadded:: 2015.8.2
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import salt.utils.configcomparer
|
||||||
|
|
||||||
|
__proxyenabled__ = ['marathon']
|
||||||
|
log = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def config(name, config):
|
||||||
|
'''
|
||||||
|
Ensure that the marathon app with the given id is present and is configured
|
||||||
|
to match the given config values.
|
||||||
|
|
||||||
|
:param name: The app name/id
|
||||||
|
:param config: The configuration to apply (dict)
|
||||||
|
:return: A standard Salt changes dictionary
|
||||||
|
'''
|
||||||
|
# setup return structure
|
||||||
|
ret = {
|
||||||
|
'name': name,
|
||||||
|
'changes': {},
|
||||||
|
'result': False,
|
||||||
|
'comment': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# get existing config if app is present
|
||||||
|
existing_config = None
|
||||||
|
if __salt__['marathon.has_app'](name):
|
||||||
|
existing_config = __salt__['marathon.app'](name)['app']
|
||||||
|
|
||||||
|
# compare existing config with defined config
|
||||||
|
if existing_config:
|
||||||
|
update_config = copy.deepcopy(existing_config)
|
||||||
|
salt.utils.configcomparer.compare_and_update_config(
|
||||||
|
config,
|
||||||
|
update_config,
|
||||||
|
ret['changes'],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# the app is not configured--we need to create it from scratch
|
||||||
|
ret['changes']['app'] = {
|
||||||
|
'new': config,
|
||||||
|
'old': None,
|
||||||
|
}
|
||||||
|
update_config = config
|
||||||
|
|
||||||
|
# update the config if we registered any changes
|
||||||
|
if ret['changes']:
|
||||||
|
# if test, report there will be an update
|
||||||
|
if __opts__['test']:
|
||||||
|
ret['result'] = None
|
||||||
|
ret['comment'] = 'Marathon app {0} is set to be updated'.format(
|
||||||
|
name
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
update_result = __salt__['marathon.update_app'](name, update_config)
|
||||||
|
if 'exception' in update_result:
|
||||||
|
ret['result'] = False
|
||||||
|
ret['comment'] = 'Failed to update app config for {0}: {1}'.format(
|
||||||
|
name,
|
||||||
|
update_result['exception'],
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'Updated app config for {0}'.format(name)
|
||||||
|
return ret
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'Marathon app {0} configured correctly'.format(name)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def absent(name):
|
||||||
|
'''
|
||||||
|
Ensure that the marathon app with the given id is not present.
|
||||||
|
|
||||||
|
:param name: The app name/id
|
||||||
|
:return: A standard Salt changes dictionary
|
||||||
|
'''
|
||||||
|
ret = {'name': name,
|
||||||
|
'changes': {},
|
||||||
|
'result': False,
|
||||||
|
'comment': ''}
|
||||||
|
if not __salt__['marathon.has_app'](name):
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'App {0} already absent'.format(name)
|
||||||
|
return ret
|
||||||
|
if __opts__['test']:
|
||||||
|
ret['result'] = None
|
||||||
|
ret['comment'] = 'App {0} is set to be removed'.format(name)
|
||||||
|
return ret
|
||||||
|
if __salt__['marathon.rm_app'](name):
|
||||||
|
ret['changes'] = {'app': name}
|
||||||
|
ret['result'] = True
|
||||||
|
ret['comment'] = 'Removed app {0}'.format(name)
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
ret['result'] = False
|
||||||
|
ret['comment'] = 'Failed to remove app {0}'.format(name)
|
||||||
|
return ret
|
107
salt/utils/configcomparer.py
Normal file
107
salt/utils/configcomparer.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Utilities for comparing and updating configurations while keeping track of
|
||||||
|
changes in a way that can be easily reported in a state.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
|
||||||
|
def compare_and_update_config(config, update_config, changes, namespace=''):
|
||||||
|
'''
|
||||||
|
Recursively compare two configs, writing any needed changes to the
|
||||||
|
update_config and capturing changes in the changes dict.
|
||||||
|
'''
|
||||||
|
if isinstance(config, dict):
|
||||||
|
if not update_config:
|
||||||
|
if config:
|
||||||
|
# the updated config is more valid--report that we are using it
|
||||||
|
changes[namespace] = {
|
||||||
|
'new': config,
|
||||||
|
'old': update_config,
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
elif not isinstance(update_config, dict):
|
||||||
|
# new config is a dict, other isn't--new one wins
|
||||||
|
changes[namespace] = {
|
||||||
|
'new': config,
|
||||||
|
'old': update_config,
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
else:
|
||||||
|
# compare each key in the base config with the values in the
|
||||||
|
# update_config, overwriting the values that are different but
|
||||||
|
# keeping any that are not defined in config
|
||||||
|
for key, value in config.iteritems():
|
||||||
|
_namespace = key
|
||||||
|
if namespace:
|
||||||
|
_namespace = '{0}.{1}'.format(namespace, _namespace)
|
||||||
|
update_config[key] = compare_and_update_config(
|
||||||
|
value,
|
||||||
|
update_config.get(key, None),
|
||||||
|
changes,
|
||||||
|
namespace=_namespace,
|
||||||
|
)
|
||||||
|
return update_config
|
||||||
|
|
||||||
|
elif isinstance(config, list):
|
||||||
|
if not update_config:
|
||||||
|
if config:
|
||||||
|
# the updated config is more valid--report that we are using it
|
||||||
|
changes[namespace] = {
|
||||||
|
'new': config,
|
||||||
|
'old': update_config,
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
elif not isinstance(update_config, list):
|
||||||
|
# new config is a list, other isn't--new one wins
|
||||||
|
changes[namespace] = {
|
||||||
|
'new': config,
|
||||||
|
'old': update_config,
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
else:
|
||||||
|
# iterate through config list, ensuring that each index in the
|
||||||
|
# update_config list is the same
|
||||||
|
for idx, item in enumerate(config):
|
||||||
|
_namespace = '[{0}]'.format(idx)
|
||||||
|
if namespace:
|
||||||
|
_namespace = '{0}{1}'.format(namespace, _namespace)
|
||||||
|
_update = None
|
||||||
|
if len(update_config) > idx:
|
||||||
|
_update = update_config[idx]
|
||||||
|
if _update:
|
||||||
|
update_config[idx] = compare_and_update_config(
|
||||||
|
config[idx],
|
||||||
|
_update,
|
||||||
|
changes,
|
||||||
|
namespace=_namespace,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
changes[_namespace] = {
|
||||||
|
'new': config[idx],
|
||||||
|
'old': _update,
|
||||||
|
}
|
||||||
|
update_config.append(config[idx])
|
||||||
|
|
||||||
|
if len(update_config) > len(config):
|
||||||
|
# trim any items in update_config that are not in config
|
||||||
|
for idx, old_item in enumerate(update_config):
|
||||||
|
if idx < len(config):
|
||||||
|
continue
|
||||||
|
_namespace = '[{0}]'.format(idx)
|
||||||
|
if namespace:
|
||||||
|
_namespace = '{0}{1}'.format(namespace, _namespace)
|
||||||
|
changes[_namespace] = {
|
||||||
|
'new': None,
|
||||||
|
'old': old_item,
|
||||||
|
}
|
||||||
|
del update_config[len(config):]
|
||||||
|
return update_config
|
||||||
|
|
||||||
|
else:
|
||||||
|
if config != update_config:
|
||||||
|
changes[namespace] = {
|
||||||
|
'new': config,
|
||||||
|
'old': update_config,
|
||||||
|
}
|
||||||
|
return config
|
321
tests/unit/utils/configcomparer_test.py
Normal file
321
tests/unit/utils/configcomparer_test.py
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Import python libs
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import copy
|
||||||
|
|
||||||
|
# Import Salt Testing libs
|
||||||
|
from salttesting import TestCase
|
||||||
|
from salttesting.helpers import ensure_in_syspath
|
||||||
|
|
||||||
|
ensure_in_syspath('../../')
|
||||||
|
|
||||||
|
# Import Salt libs
|
||||||
|
from salt.utils import configcomparer
|
||||||
|
|
||||||
|
|
||||||
|
class UtilConfigcomparerTestCase(TestCase):
|
||||||
|
|
||||||
|
base_config = {
|
||||||
|
'attr1': 'value1',
|
||||||
|
'attr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
'item3',
|
||||||
|
],
|
||||||
|
'attr3': [],
|
||||||
|
'attr4': {},
|
||||||
|
'attr5': {
|
||||||
|
'subattr1': 'value1',
|
||||||
|
'subattr2': [
|
||||||
|
'item1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_compare_and_update_config(self):
|
||||||
|
|
||||||
|
# empty config
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual({}, changes)
|
||||||
|
self.assertEqual(self.base_config, to_update)
|
||||||
|
|
||||||
|
# simple, new value
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attrx': 'value1',
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attrx': {
|
||||||
|
'new': 'value1',
|
||||||
|
'old': None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual('value1', to_update['attrx'])
|
||||||
|
self.assertEqual('value1', to_update['attr1'])
|
||||||
|
# simple value
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attr1': 'value2',
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr1': {
|
||||||
|
'new': 'value2',
|
||||||
|
'old': 'value1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual('value2', to_update['attr1'])
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr1': 'value2',
|
||||||
|
'attr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
'item3',
|
||||||
|
],
|
||||||
|
'attr3': [],
|
||||||
|
'attr4': {},
|
||||||
|
'attr5': {
|
||||||
|
'subattr1': 'value1',
|
||||||
|
'subattr2': [
|
||||||
|
'item1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
# empty list
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attr3': [],
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual({}, changes)
|
||||||
|
self.assertEqual(self.base_config, to_update)
|
||||||
|
|
||||||
|
# list value (add)
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
'item3',
|
||||||
|
'item4',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr2[3]': {
|
||||||
|
'new': 'item4',
|
||||||
|
'old': None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr1': 'value1',
|
||||||
|
'attr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
'item3',
|
||||||
|
'item4',
|
||||||
|
],
|
||||||
|
'attr3': [],
|
||||||
|
'attr4': {},
|
||||||
|
'attr5': {
|
||||||
|
'subattr1': 'value1',
|
||||||
|
'subattr2': [
|
||||||
|
'item1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
# list value (remove and modify)
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attr2': [
|
||||||
|
'itemx',
|
||||||
|
'item2',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr2[0]': {
|
||||||
|
'new': 'itemx',
|
||||||
|
'old': 'item1',
|
||||||
|
},
|
||||||
|
'attr2[2]': {
|
||||||
|
'new': None,
|
||||||
|
'old': 'item3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr1': 'value1',
|
||||||
|
'attr2': [
|
||||||
|
'itemx',
|
||||||
|
'item2',
|
||||||
|
],
|
||||||
|
'attr3': [],
|
||||||
|
'attr4': {},
|
||||||
|
'attr5': {
|
||||||
|
'subattr1': 'value1',
|
||||||
|
'subattr2': [
|
||||||
|
'item1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
# empty dict
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attr4': {}
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual({}, changes)
|
||||||
|
self.assertEqual(self.base_config, to_update)
|
||||||
|
|
||||||
|
# dict value (add)
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attr5': {
|
||||||
|
'subattr3': 'value1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr5.subattr3': {
|
||||||
|
'new': 'value1',
|
||||||
|
'old': None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr1': 'value1',
|
||||||
|
'attr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
'item3',
|
||||||
|
],
|
||||||
|
'attr3': [],
|
||||||
|
'attr4': {},
|
||||||
|
'attr5': {
|
||||||
|
'subattr1': 'value1',
|
||||||
|
'subattr2': [
|
||||||
|
'item1',
|
||||||
|
],
|
||||||
|
'subattr3': 'value1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
# dict value (remove and modify)
|
||||||
|
to_update = copy.deepcopy(self.base_config)
|
||||||
|
changes = {}
|
||||||
|
configcomparer.compare_and_update_config(
|
||||||
|
{
|
||||||
|
'attr5': {
|
||||||
|
'subattr1': 'value2',
|
||||||
|
'subattr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr5.subattr1': {
|
||||||
|
'new': 'value2',
|
||||||
|
'old': 'value1',
|
||||||
|
},
|
||||||
|
'attr5.subattr2[1]': {
|
||||||
|
'new': 'item2',
|
||||||
|
'old': None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changes,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'attr1': 'value1',
|
||||||
|
'attr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
'item3',
|
||||||
|
],
|
||||||
|
'attr3': [],
|
||||||
|
'attr4': {},
|
||||||
|
'attr5': {
|
||||||
|
'subattr1': 'value2',
|
||||||
|
'subattr2': [
|
||||||
|
'item1',
|
||||||
|
'item2',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from integration import run_tests
|
||||||
|
run_tests(UtilConfigcomparerTestCase, needs_daemon=False)
|
Loading…
Reference in New Issue
Block a user