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