mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge remote-tracking branch 'upstream/2015.8' into merge-forward-develop
Conflicts: salt/minion.py salt/modules/rest_service.py salt/states/dockerng.py salt/utils/cloud.py
This commit is contained in:
commit
41cad702e3
41 changed files with 720 additions and 580 deletions
|
@ -123,8 +123,9 @@ MOCK_MODULES = [
|
|||
for mod_name in MOCK_MODULES:
|
||||
sys.modules[mod_name] = Mock()
|
||||
|
||||
# Define a fake version attribute for libcloud so docs build as supposed
|
||||
# Define a fake version attribute for the following libs.
|
||||
sys.modules['libcloud'].__version__ = '0.0.0'
|
||||
sys.modules['pymongo'].version = '0.0.0'
|
||||
|
||||
|
||||
# -- Add paths to PYTHONPATH ---------------------------------------------------
|
||||
|
|
|
@ -163,81 +163,126 @@ it can be verified with Salt:
|
|||
GCE Specific Settings
|
||||
=====================
|
||||
Consult the sample profile below for more information about GCE specific
|
||||
settings. Some of them are mandatory and are properly labeled below but
|
||||
settings. Some of them are mandatory and are properly labeled below but
|
||||
typically also include a hard-coded default.
|
||||
|
||||
Initial Profile
|
||||
---------------
|
||||
Set up an initial profile at ``/etc/salt/cloud.profiles`` or
|
||||
``/etc/salt/cloud.profiles.d/gce.conf``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
my-gce-profile:
|
||||
|
||||
# Image is used to define what Operating System image should be used
|
||||
# to for the instance. Examples are Debian 7 (wheezy) and CentOS 6.
|
||||
#
|
||||
# MANDATORY
|
||||
#
|
||||
image: centos-6
|
||||
|
||||
# A 'size', in GCE terms, refers to the instance's 'machine type'. See
|
||||
# the on-line documentation for a complete list of GCE machine types.
|
||||
#
|
||||
# MANDATORY
|
||||
#
|
||||
size: n1-standard-1
|
||||
|
||||
# A 'location', in GCE terms, refers to the instance's 'zone'. GCE
|
||||
# has the notion of both Regions (e.g. us-central1, europe-west1, etc)
|
||||
# and Zones (e.g. us-central1-a, us-central1-b, etc).
|
||||
#
|
||||
# MANDATORY
|
||||
#
|
||||
location: europe-west1-b
|
||||
|
||||
# Use this setting to define the network resource for the instance.
|
||||
# All GCE projects contain a network named 'default' but it's possible
|
||||
# to use this setting to create instances belonging to a different
|
||||
# network resource.
|
||||
#
|
||||
network: default
|
||||
|
||||
# GCE supports instance/network tags and this setting allows you to
|
||||
# set custom tags. It should be a list of strings and must be
|
||||
# parse-able by the python ast.literal_eval() function to convert it
|
||||
# to a python list.
|
||||
#
|
||||
tags: '["one", "two", "three"]'
|
||||
|
||||
# GCE supports instance metadata and this setting allows you to
|
||||
# set custom metadata. It should be a hash of key/value strings and
|
||||
# parse-able by the python ast.literal_eval() function to convert it
|
||||
# to a python dictionary.
|
||||
#
|
||||
metadata: '{"one": "1", "2": "two"}'
|
||||
|
||||
# Use this setting to ensure that when new instances are created,
|
||||
# they will use a persistent disk to preserve data between instance
|
||||
# terminations and re-creations.
|
||||
#
|
||||
use_persistent_disk: True
|
||||
|
||||
# In the event that you wish the boot persistent disk to be permanently
|
||||
# deleted when you destroy an instance, set delete_boot_pd to True.
|
||||
#
|
||||
delete_boot_pd: False
|
||||
|
||||
# Specify whether to use public or private IP for deploy script.
|
||||
# Valid options are:
|
||||
# private_ips - The salt-master is also hosted with GCE
|
||||
# public_ips - The salt-master is hosted outside of GCE
|
||||
ssh_interface: public_ips
|
||||
|
||||
# Per instance setting: Used a named fixed IP address to this host.
|
||||
# Valid options are:
|
||||
# ephemeral - The host will use a GCE ephemeral IP
|
||||
# None - No external IP will be configured on this host.
|
||||
# Optionally, pass the name of a GCE address to use a fixed IP address.
|
||||
# If the address does not already exist, it will be created.
|
||||
external_ip: "ephemeral"
|
||||
|
||||
image
|
||||
-----
|
||||
|
||||
Image is used to define what Operating System image should be used
|
||||
to for the instance. Examples are Debian 7 (wheezy) and CentOS 6. Required.
|
||||
|
||||
size
|
||||
----
|
||||
|
||||
A 'size', in GCE terms, refers to the instance's 'machine type'. See
|
||||
the on-line documentation for a complete list of GCE machine types. Required.
|
||||
|
||||
location
|
||||
--------
|
||||
|
||||
A 'location', in GCE terms, refers to the instance's 'zone'. GCE
|
||||
has the notion of both Regions (e.g. us-central1, europe-west1, etc)
|
||||
and Zones (e.g. us-central1-a, us-central1-b, etc). Required.
|
||||
|
||||
network
|
||||
-------
|
||||
|
||||
Use this setting to define the network resource for the instance.
|
||||
All GCE projects contain a network named 'default' but it's possible
|
||||
to use this setting to create instances belonging to a different
|
||||
network resource.
|
||||
|
||||
tags
|
||||
----
|
||||
|
||||
GCE supports instance/network tags and this setting allows you to
|
||||
set custom tags. It should be a list of strings and must be
|
||||
parse-able by the python ast.literal_eval() function to convert it
|
||||
to a python list.
|
||||
|
||||
metadata
|
||||
--------
|
||||
|
||||
GCE supports instance metadata and this setting allows you to
|
||||
set custom metadata. It should be a hash of key/value strings and
|
||||
parse-able by the python ast.literal_eval() function to convert it
|
||||
to a python dictionary.
|
||||
|
||||
use_persistent_disk
|
||||
-------------------
|
||||
|
||||
Use this setting to ensure that when new instances are created,
|
||||
they will use a persistent disk to preserve data between instance
|
||||
terminations and re-creations.
|
||||
|
||||
delete_boot_pd
|
||||
--------------
|
||||
|
||||
In the event that you wish the boot persistent disk to be permanently
|
||||
deleted when you destroy an instance, set delete_boot_pd to True.
|
||||
|
||||
ssh_interface
|
||||
-------------
|
||||
|
||||
Specify whether to use public or private IP for deploy script.
|
||||
|
||||
Valid options are:
|
||||
* private_ips: The salt-master is also hosted with GCE
|
||||
* public_ips: The salt-master is hosted outside of GCE
|
||||
|
||||
external_ip
|
||||
-----------
|
||||
|
||||
Per instance setting: Used a named fixed IP address to this host.
|
||||
|
||||
Valid options are:
|
||||
* ephemeral - The host will use a GCE ephemeral IP
|
||||
* None - No external IP will be configured on this host.
|
||||
|
||||
Optionally, pass the name of a GCE address to use a fixed IP address.
|
||||
If the address does not already exist, it will be created.
|
||||
|
||||
ex_disk_type
|
||||
------------
|
||||
|
||||
GCE supports two different disk types, ``pd-standard`` and ``pd-ssd``.
|
||||
The default disk type setting is ``pd-standard``. To specify using an SSD
|
||||
disk, set ``pd-ssd`` as the value.
|
||||
|
||||
.. versionadded:: 2014.7.0
|
||||
|
||||
ip_forwarding
|
||||
-------------
|
||||
|
||||
GCE instances can be enabled to use IP Forwarding. When set to ``True``,
|
||||
this options allows the instance to send/receive non-matching src/dst
|
||||
packets. Default is ``False``.
|
||||
|
||||
.. versionadded:: 2015.8.1
|
||||
|
||||
SSH Remote Access
|
||||
=================
|
||||
|
||||
GCE instances do not allow remote access to the root user by default.
|
||||
Instead, another user must be used to run the deploy script using sudo.
|
||||
Append something like this to ``/etc/salt/cloud.profiles`` or
|
||||
|
|
|
@ -288,6 +288,9 @@ with labels.
|
|||
``Story``
|
||||
The issue is used by a SaltStack engineer to track progress on multiple related issues in a single place.
|
||||
|
||||
``Stretch``
|
||||
The issue is an optional goal for the current sprint but may not be delivered.
|
||||
|
||||
``ZD``
|
||||
The issue is related to a Zendesk customer support ticket.
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ configuration is needed to pull from these repositories.
|
|||
|
||||
These packages are usually available within a few days of upstream release.
|
||||
|
||||
.. _freebsd-upstream:
|
||||
|
||||
SaltStack repo
|
||||
==============
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ from the file /srv/pillar/p8000.sls (if you have not changed your default pillar
|
|||
---------
|
||||
|
||||
proxy:
|
||||
proxymodule: rest_sample
|
||||
proxytype: rest_sample
|
||||
url: http://<IP your REST listens on>:port
|
||||
|
||||
In other words, if your REST service is listening on port 8000 on 127.0.0.1
|
||||
|
|
|
@ -19,7 +19,7 @@ See the following links for instructions:
|
|||
- :ref:`Red Hat / CentOS 5, 6, 7 <installation-rhel-repo>`
|
||||
- :ref:`Debian 8 <installation-debian-repo>`
|
||||
- :ref:`Windows <windows-installer>`
|
||||
- FreeBSD
|
||||
- :ref:`FreeBSD <freebsd-upstream>`
|
||||
|
||||
Send Event on State Completion
|
||||
==============================
|
||||
|
|
|
@ -6,3 +6,11 @@ Version 2015.8.1 is a bugfix release for :doc:`2015.8.0
|
|||
</topics/releases/2015.8.0>`.
|
||||
|
||||
Changes:
|
||||
|
||||
- Some issues with proxy minions were corrected.
|
||||
|
||||
Known Issues:
|
||||
|
||||
- Proxy minions currently cannot execute a highstate because of the way
|
||||
the proxymodule is being loaded internally. This will be fixed in a
|
||||
future release.
|
||||
|
|
|
@ -6,7 +6,7 @@ Extended changelog courtesy of Todd Stansell (https://github.com/tjstansell/salt
|
|||
*Generated at: 2015-09-09T18:15:43Z*
|
||||
|
||||
This list includes all pull requests merged into the 2015.8 branch between the
|
||||
forking of the branch from develop and the release of 12015.8.0.
|
||||
forking of the branch from develop and the release of 2015.8.0.
|
||||
|
||||
Statistics:
|
||||
|
||||
|
|
|
@ -42,6 +42,8 @@ The information which can be stored in a roster `target` is the following:
|
|||
# Optional parameters
|
||||
port: # The target system's ssh port number
|
||||
sudo: # Boolean to run command via sudo
|
||||
tty: # Boolean: Set this option to True if sudo is also set to
|
||||
# True and requiretty is also set on the target system
|
||||
priv: # File path to ssh private key, defaults to salt-ssh.rsa
|
||||
timeout: # Number of seconds to wait for response when establishing
|
||||
# an SSH connection
|
||||
|
|
|
@ -113,34 +113,34 @@ FunctionEnd
|
|||
Function updateMinionConfig
|
||||
|
||||
ClearErrors
|
||||
FileOpen $0 "$INSTDIR\conf\minion" "r" ; open target file for reading
|
||||
GetTempFileName $R0 ; get new temp file name
|
||||
FileOpen $1 $R0 "w" ; open temp file for writing
|
||||
FileOpen $0 "$INSTDIR\conf\minion" "r" ; open target file for reading
|
||||
GetTempFileName $R0 ; get new temp file name
|
||||
FileOpen $1 $R0 "w" ; open temp file for writing
|
||||
loop:
|
||||
FileRead $0 $2 ; read line from target file
|
||||
FileRead $0 $2 ; read line from target file
|
||||
IfErrors done
|
||||
${If} $MasterHost_State != ""
|
||||
${AndIf} $MasterHost_State != "salt" ; check if end of file reached
|
||||
StrCmp $2 "#master: salt$\r$\n" 0 +2 ; compare line with search string with CR/LF
|
||||
StrCpy $2 "master: $MasterHost_State$\r$\n" ; change line
|
||||
StrCmp $2 "#master: salt" 0 +2 ; compare line with search string without CR/LF (at the end of the file)
|
||||
StrCpy $2 "master: $MasterHost_State" ; change line
|
||||
${AndIf} $MasterHost_State != "salt" ; check if end of file reached
|
||||
StrCmp $2 "#master: salt$\r$\n" 0 +2 ; compare line with search string with CR/LF
|
||||
StrCpy $2 "master: $MasterHost_State$\r$\n" ; change line
|
||||
StrCmp $2 "#master: salt" 0 +2 ; compare line with search string without CR/LF (at the end of the file)
|
||||
StrCpy $2 "master: $MasterHost_State" ; change line
|
||||
${EndIf}
|
||||
${If} $MinionName_State != ""
|
||||
${AndIf} $MinionName_State != "hostname"
|
||||
StrCmp $2 "#id:$\r$\n" 0 +2 ; compare line with search string with CR/LF
|
||||
StrCpy $2 "id: $MinionName_State$\r$\n" ; change line
|
||||
StrCmp $2 "#id:" 0 +2 ; compare line with search string without CR/LF (at the end of the file)
|
||||
StrCpy $2 "id: $MinionName_State" ; change line
|
||||
StrCmp $2 "#id:$\r$\n" 0 +2 ; compare line with search string with CR/LF
|
||||
StrCpy $2 "id: $MinionName_State$\r$\n" ; change line
|
||||
StrCmp $2 "#id:" 0 +2 ; compare line with search string without CR/LF (at the end of the file)
|
||||
StrCpy $2 "id: $MinionName_State" ; change line
|
||||
${EndIf}
|
||||
FileWrite $1 $2 ; write changed or unchanged line to temp file
|
||||
FileWrite $1 $2 ; write changed or unchanged line to temp file
|
||||
Goto loop
|
||||
|
||||
done:
|
||||
FileClose $0 ; close target file
|
||||
FileClose $1 ; close temp file
|
||||
Delete "$INSTDIR\conf\minion" ; delete target file
|
||||
CopyFiles /SILENT $R0 "$INSTDIR\conf\minion" ; copy temp file to target file
|
||||
FileClose $0 ; close target file
|
||||
FileClose $1 ; close temp file
|
||||
Delete "$INSTDIR\conf\minion" ; delete target file
|
||||
CopyFiles /SILENT $R0 "$INSTDIR\conf\minion" ; copy temp file to target file
|
||||
Delete $R0
|
||||
|
||||
FunctionEnd
|
||||
|
@ -156,40 +156,6 @@ ShowUnInstDetails show
|
|||
|
||||
Section "MainSection" SEC01
|
||||
|
||||
; Remove previous version of salt, but don't remove conf and key
|
||||
; Service must be stopped to delete files that are in use
|
||||
ExecWait "net stop salt-minion"
|
||||
; Remove the service in case we're installing over an older version
|
||||
; It will be recreated later
|
||||
ExecWait "sc delete salt-minion"
|
||||
|
||||
; Delete everything except conf and var
|
||||
ClearErrors
|
||||
FindFirst $0 $1 $INSTDIR\*
|
||||
|
||||
loop:
|
||||
IfFileExists "$INSTDIR\$1\*.*" IsDir IsFile
|
||||
|
||||
IsDir:
|
||||
${IfNot} $1 == "."
|
||||
${AndIfNot} $1 == ".."
|
||||
${AndIfNot} $1 == "conf"
|
||||
${AndIfNot} $1 == "var"
|
||||
RMDir /r "$INSTDIR\$1"
|
||||
${EndIf}
|
||||
|
||||
IsFile:
|
||||
DELETE "$INSTDIR\$1"
|
||||
|
||||
FindNext $0 $1
|
||||
IfErrors done
|
||||
|
||||
Goto loop
|
||||
|
||||
done:
|
||||
FindClose $0
|
||||
|
||||
Sleep 3000
|
||||
SetOutPath "$INSTDIR\"
|
||||
SetOverwrite try
|
||||
CreateDirectory $INSTDIR\conf\pki\minion
|
||||
|
@ -239,6 +205,34 @@ FunctionEnd
|
|||
|
||||
Function .onInit
|
||||
|
||||
; Check for existing installation
|
||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString"
|
||||
StrCmp $R0 "" confFind
|
||||
|
||||
; Found existing installation, prompt to uninstall
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "${PRODUCT_NAME} is already installed. $\n$\nClick `OK` to remove the existing installation." /SD IDOK IDOK uninst
|
||||
Abort
|
||||
|
||||
uninst:
|
||||
; Make sure we're in the right directory
|
||||
${If} $INSTDIR == "c:\salt\bin\Scripts"
|
||||
StrCpy $INSTDIR "C:\salt"
|
||||
${EndIf}
|
||||
|
||||
; Stop and remove the salt-minion service
|
||||
ExecWait "net stop salt-minion"
|
||||
ExecWait "sc delete salt-minion"
|
||||
|
||||
; Remove salt binaries and batch files
|
||||
Delete "$INSTDIR\uninst.exe"
|
||||
Delete "$INSTDIR\nssm.exe"
|
||||
Delete "$INSTDIR\salt*"
|
||||
RMDir /r "$INSTDIR\bin"
|
||||
|
||||
; Remove registry entries
|
||||
DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
|
||||
DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}"
|
||||
|
||||
confFind:
|
||||
IfFileExists "$INSTDIR\conf\minion" confFound confNotFound
|
||||
|
||||
|
@ -339,7 +333,7 @@ Section Uninstall
|
|||
Delete "$INSTDIR\uninst.exe"
|
||||
Delete "$INSTDIR\nssm.exe"
|
||||
Delete "$INSTDIR\salt*"
|
||||
Delete "$INSTDIR\bin"
|
||||
RMDir /r "$INSTDIR\bin"
|
||||
|
||||
${If} $INSTDIR != 'Program Files'
|
||||
${AndIf} $INSTDIR != 'Program Files (x86)'
|
||||
|
|
|
@ -1281,7 +1281,13 @@ def _modify_eni_properties(eni_id, properties=None):
|
|||
while retries > 0:
|
||||
retries = retries - 1
|
||||
|
||||
result = query(params, return_root=True)
|
||||
result = aws.query(params,
|
||||
return_root=True,
|
||||
location=get_location(),
|
||||
provider=get_provider(),
|
||||
opts=__opts__,
|
||||
sigver='4')
|
||||
|
||||
if isinstance(result, dict) and result.get('error'):
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
|
|
@ -2030,8 +2030,6 @@ def request_instance(vm_):
|
|||
|
||||
if LIBCLOUD_VERSION_INFO > (0, 15, 1):
|
||||
|
||||
# This only exists in current trunk of libcloud and should be in next
|
||||
# release
|
||||
kwargs.update({
|
||||
'ex_disk_type': config.get_cloud_config_value(
|
||||
'ex_disk_type', vm_, __opts__, default='pd-standard'),
|
||||
|
@ -2040,7 +2038,10 @@ def request_instance(vm_):
|
|||
'ex_disks_gce_struct': config.get_cloud_config_value(
|
||||
'ex_disks_gce_struct', vm_, __opts__, default=None),
|
||||
'ex_service_accounts': config.get_cloud_config_value(
|
||||
'ex_service_accounts', vm_, __opts__, default=None)
|
||||
'ex_service_accounts', vm_, __opts__, default=None),
|
||||
'ex_can_ip_forward': config.get_cloud_config_value(
|
||||
'ip_forwarding', vm_, __opts__, default=False
|
||||
)
|
||||
})
|
||||
if kwargs.get('ex_disk_type') not in ('pd-standard', 'pd-ssd'):
|
||||
raise SaltCloudSystemExit(
|
||||
|
|
|
@ -220,7 +220,7 @@ def list_nodes(full=False, call=None):
|
|||
|
||||
for node in nodes:
|
||||
ret[node] = {}
|
||||
for item in 'id', 'image', 'size', 'public_ips', 'private_ips', 'state':
|
||||
for item in ('id', 'image', 'size', 'public_ips', 'private_ips', 'state'):
|
||||
ret[node][item] = nodes[node][item]
|
||||
|
||||
return ret
|
||||
|
|
|
@ -222,7 +222,7 @@ def list_nodes(conn=None, call=None):
|
|||
nodes = list_nodes_full(conn, call)
|
||||
for node in nodes:
|
||||
ret[node] = {}
|
||||
for prop in 'id', 'image', 'name', 'size', 'state', 'private_ips', 'public_ips':
|
||||
for prop in ('id', 'image', 'name', 'size', 'state', 'private_ips', 'public_ips'):
|
||||
ret[node][prop] = nodes[node][prop]
|
||||
return ret
|
||||
|
||||
|
|
|
@ -512,8 +512,7 @@ class Client(object):
|
|||
ret.sort()
|
||||
return ret
|
||||
|
||||
def get_url(self, url, dest, makedirs=False, saltenv='base',
|
||||
env=None, no_cache=False):
|
||||
def get_url(self, url, dest, makedirs=False, saltenv='base', env=None, no_cache=False):
|
||||
'''
|
||||
Get a single file from a URL.
|
||||
'''
|
||||
|
@ -598,78 +597,21 @@ class Client(object):
|
|||
|
||||
destfp = None
|
||||
try:
|
||||
if no_cache:
|
||||
result = []
|
||||
|
||||
def on_chunk(chunk):
|
||||
result.append(chunk)
|
||||
else:
|
||||
dest_tmp = "{0}.part".format(dest)
|
||||
destfp = salt.utils.fopen(dest_tmp, 'wb')
|
||||
|
||||
def on_chunk(chunk):
|
||||
destfp.write(chunk)
|
||||
|
||||
query = salt.utils.http.query(
|
||||
fixed_url,
|
||||
stream=True,
|
||||
streaming_callback=on_chunk,
|
||||
username=url_data.username,
|
||||
password=url_data.password,
|
||||
**get_kwargs
|
||||
)
|
||||
|
||||
if 'handle' not in query:
|
||||
raise MinionError('Error: {0}'.format(query['error']))
|
||||
|
||||
try:
|
||||
content_length = int(query['handle'].headers['Content-Length'])
|
||||
except (AttributeError, KeyError, ValueError):
|
||||
# Shouldn't happen but don't let this raise an exception.
|
||||
# Instead, just don't do content length checking below.
|
||||
log.warning(
|
||||
'No Content-Length header in HTTP response from fetch of '
|
||||
'{0}, or Content-Length is non-numeric'.format(fixed_url)
|
||||
)
|
||||
content_length = None
|
||||
|
||||
if no_cache:
|
||||
content = ''.join(result)
|
||||
if content_length is not None \
|
||||
and len(content) > content_length:
|
||||
return content[-content_length:]
|
||||
else:
|
||||
return content
|
||||
return query['handle'].body
|
||||
else:
|
||||
destfp.close()
|
||||
destfp = None
|
||||
dest_tmp_size = os.path.getsize(dest_tmp)
|
||||
if content_length is not None \
|
||||
and dest_tmp_size > content_length:
|
||||
log.warning(
|
||||
'Size of file downloaded from {0} ({1}) does not '
|
||||
'match the Content-Length ({2}). This is probably due '
|
||||
'to an upstream bug in tornado '
|
||||
'(https://github.com/tornadoweb/tornado/issues/1518). '
|
||||
'Re-writing the file to correct this.'.format(
|
||||
fixed_url,
|
||||
dest_tmp_size,
|
||||
content_length
|
||||
)
|
||||
)
|
||||
dest_tmp_bak = dest_tmp + '.bak'
|
||||
salt.utils.files.rename(dest_tmp, dest_tmp_bak)
|
||||
with salt.utils.fopen(dest_tmp_bak, 'rb') as fp_bak:
|
||||
fp_bak.seek(dest_tmp_size - content_length)
|
||||
with salt.utils.fopen(dest_tmp, 'wb') as fp_new:
|
||||
while True:
|
||||
chunk = fp_bak.read(
|
||||
self.opts['file_buffer_size']
|
||||
)
|
||||
if not chunk:
|
||||
break
|
||||
fp_new.write(chunk)
|
||||
os.remove(dest_tmp_bak)
|
||||
dest_tmp = "{0}.part".format(dest)
|
||||
with salt.utils.fopen(dest_tmp, 'wb') as destfp:
|
||||
destfp.write(query['handle'].body)
|
||||
salt.utils.files.rename(dest_tmp, dest)
|
||||
return dest
|
||||
except HTTPError as exc:
|
||||
|
|
|
@ -43,11 +43,11 @@ def defaults():
|
|||
|
||||
|
||||
def facts():
|
||||
log.debug('----------- Trying to get facts')
|
||||
if 'proxymodule' in __opts__:
|
||||
facts = __opts__['proxymodule']['junos.facts']()
|
||||
facts['version_info'] = 'override'
|
||||
return facts
|
||||
if 'junos.facts' in __opts__['proxymodule']:
|
||||
facts = __opts__['proxymodule']['junos.facts']()
|
||||
facts['version_info'] = 'override'
|
||||
return facts
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ def kernel():
|
|||
|
||||
|
||||
def os():
|
||||
return {'os': 'proxy'}
|
||||
return {'os': 'RestExampleOS'}
|
||||
|
||||
|
||||
def location():
|
||||
|
|
|
@ -186,6 +186,8 @@ def minion_mods(
|
|||
loaded_base_name=loaded_base_name,
|
||||
static_modules=static_modules)
|
||||
|
||||
ret.pack['__salt__'] = ret
|
||||
|
||||
# Load any provider overrides from the configuration file providers option
|
||||
# Note: Providers can be pkg, service, user or group - not to be confused
|
||||
# with cloud providers.
|
||||
|
@ -195,7 +197,7 @@ def minion_mods(
|
|||
# sometimes providers opts is not to diverge modules but
|
||||
# for other configuration
|
||||
try:
|
||||
funcs = raw_mod(opts, providers[mod], ret.items())
|
||||
funcs = raw_mod(opts, providers[mod], ret)
|
||||
except TypeError:
|
||||
break
|
||||
else:
|
||||
|
@ -204,7 +206,6 @@ def minion_mods(
|
|||
f_key = '{0}{1}'.format(mod, func[func.rindex('.'):])
|
||||
ret[f_key] = funcs[func]
|
||||
|
||||
ret.pack['__salt__'] = ret
|
||||
if notify:
|
||||
evt = salt.utils.event.get_event('minion', opts=opts, listen=False)
|
||||
evt.fire_event({'complete': True}, tag='/salt/minion/minion_mod_complete')
|
||||
|
@ -254,6 +255,7 @@ def proxy(opts, functions, whitelist=None, loaded_base_name=None):
|
|||
'''
|
||||
Returns the proxy module for this salt-proxy-minion
|
||||
'''
|
||||
dirs = _module_dirs(opts, 'proxy', 'proxy')
|
||||
return LazyLoader(_module_dirs(opts, 'proxy', 'proxy'),
|
||||
opts,
|
||||
tag='proxy',
|
||||
|
|
|
@ -803,7 +803,7 @@ class Minion(MinionBase):
|
|||
just return the value of the return_retry_timer.
|
||||
'''
|
||||
msg = 'Minion return retry timer set to {0} seconds'
|
||||
if self.opts['return_retry_random']:
|
||||
if self.opts.get('return_retry_random'):
|
||||
try:
|
||||
random_retry = randint(1, self.opts['return_retry_timer'])
|
||||
except ValueError:
|
||||
|
@ -818,8 +818,8 @@ class Minion(MinionBase):
|
|||
log.debug(msg.format(random_retry) + ' (randomized)')
|
||||
return random_retry
|
||||
else:
|
||||
log.debug(msg.format(self.opts['return_retry_timer']))
|
||||
return self.opts['return_retry_timer']
|
||||
log.debug(msg.format(self.opts.get('return_retry_timer')))
|
||||
return self.opts.get('return_retry_timer')
|
||||
|
||||
def _prep_mod_opts(self):
|
||||
'''
|
||||
|
@ -2548,9 +2548,18 @@ class ProxyMinion(Minion):
|
|||
|
||||
self.opts['proxymodule'] = salt.loader.proxy(self.opts, None, loaded_base_name=fq_proxyname)
|
||||
self.functions, self.returners, self.function_errors, self.executors = self._load_modules()
|
||||
proxy_fn = self.opts['proxymodule'].loaded_base_name + '.init'
|
||||
self.opts['proxymodule'][proxy_fn](self.opts)
|
||||
|
||||
if ('{0}.init'.format(fq_proxyname) not in self.opts['proxymodule']
|
||||
or '{0}.shutdown'.format(fq_proxyname) not in self.opts['proxymodule']):
|
||||
log.error('Proxymodule {0} is missing an init() or a shutdown() or both.'.format(fq_proxyname))
|
||||
log.error('Check your proxymodule. Salt-proxy aborted.')
|
||||
self._running = False
|
||||
raise SaltSystemExit(code=-1)
|
||||
|
||||
proxy_fn = self.opts['proxymodule'].loaded_base_name + '.init'
|
||||
|
||||
self.opts['proxymodule'][proxy_fn](self.opts)
|
||||
# reload ?!?
|
||||
self.serial = salt.payload.Serial(self.opts)
|
||||
self.mod_opts = self._prep_mod_opts()
|
||||
self.matcher = Matcher(self.opts, self.functions)
|
||||
|
|
|
@ -52,9 +52,28 @@ def _get_build_env(env):
|
|||
return env_override
|
||||
|
||||
|
||||
def _get_repo_env(env):
|
||||
def _get_repo_options_env(env):
|
||||
'''
|
||||
Get repo environment overrides dictionary to use in repo process
|
||||
Get repo environment overrides dictionary to use in repo options process
|
||||
|
||||
env
|
||||
A dictionary of variables to define the repository options
|
||||
Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- env:
|
||||
- OPTIONS : 'ask-passphrase'
|
||||
|
||||
.. warning::
|
||||
|
||||
The above illustrates a common PyYAML pitfall, that **yes**,
|
||||
**no**, **on**, **off**, **true**, and **false** are all loaded as
|
||||
boolean ``True`` and ``False`` values, and must be enclosed in
|
||||
quotes to be used as strings. More info on this (and other) PyYAML
|
||||
idiosyncrasies can be found :doc:`here
|
||||
</topics/troubleshooting/yaml_idiosyncrasies>`.
|
||||
|
||||
'''
|
||||
env_options = ''
|
||||
if env is None:
|
||||
|
@ -64,10 +83,96 @@ def _get_repo_env(env):
|
|||
'\'env\' must be a Python dictionary'
|
||||
)
|
||||
for key, value in env.items():
|
||||
env_options += '{0}\n'.format(value)
|
||||
if key == 'OPTIONS':
|
||||
env_options += '{0}\n'.format(value)
|
||||
return env_options
|
||||
|
||||
|
||||
def _get_repo_dists_env(env):
|
||||
'''
|
||||
Get repo environment overrides dictionary to use in repo distributions process
|
||||
|
||||
env
|
||||
A dictionary of variables to define the repository distributions
|
||||
Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- env:
|
||||
- ORIGIN : 'jessie'
|
||||
- LABEL : 'salt debian'
|
||||
- SUITE : 'main'
|
||||
- VERSION : '8.1'
|
||||
- CODENAME : 'jessie'
|
||||
- ARCHS : 'amd64 i386 source'
|
||||
- COMPONENTS : 'main'
|
||||
- DESCRIPTION : 'SaltStack Debian package repo'
|
||||
|
||||
.. warning::
|
||||
|
||||
The above illustrates a common PyYAML pitfall, that **yes**,
|
||||
**no**, **on**, **off**, **true**, and **false** are all loaded as
|
||||
boolean ``True`` and ``False`` values, and must be enclosed in
|
||||
quotes to be used as strings. More info on this (and other) PyYAML
|
||||
idiosyncrasies can be found :doc:`here
|
||||
</topics/troubleshooting/yaml_idiosyncrasies>`.
|
||||
|
||||
'''
|
||||
# env key with tuple of control information for handling input env dictionary
|
||||
# 0 | M - Mandatory, O - Optional, I - Ignore
|
||||
# 1 | 'text string for repo field'
|
||||
# 2 | 'default value'
|
||||
dflts_dict = {
|
||||
'OPTIONS': ('I', '', 'processed by _get_repo_options_env'),
|
||||
'ORIGIN': ('O', 'Origin', 'SaltStack'),
|
||||
'LABEL': ('O', 'Label', 'salt_debian'),
|
||||
'SUITE': ('O', 'Suite', 'stable'),
|
||||
'VERSION': ('O', 'Version', '8.1'),
|
||||
'CODENAME': ('M', 'Codename', 'jessie'),
|
||||
'ARCHS': ('M', 'Architectures', 'i386 amd64 source'),
|
||||
'COMPONENTS': ('M', 'Components', 'main'),
|
||||
'DESCRIPTION': ('O', 'Description', 'SaltStack debian package repo'),
|
||||
}
|
||||
|
||||
env_dists = ''
|
||||
codename = ''
|
||||
dflts_keys = list(dflts_dict.keys())
|
||||
if env is None:
|
||||
for key, value in dflts_dict.items():
|
||||
if dflts_dict[key][0] == 'M':
|
||||
env_dists += '{0}: {1}\n'.format(dflts_dict[key][1], dflts_dict[key][2])
|
||||
if key == 'CODENAME':
|
||||
codename = dflts_dict[key][2]
|
||||
return (codename, env_dists)
|
||||
|
||||
if not isinstance(env, dict):
|
||||
raise SaltInvocationError(
|
||||
'\'env\' must be a Python dictionary'
|
||||
)
|
||||
|
||||
env_man_seen = []
|
||||
for key, value in env.items():
|
||||
if key in dflts_keys:
|
||||
if dflts_dict[key][0] == 'M':
|
||||
env_man_seen.append(key)
|
||||
if key == 'CODENAME':
|
||||
codename = value
|
||||
if dflts_dict[key][0] != 'I':
|
||||
env_dists += '{0}: {1}\n'.format(dflts_dict[key][1], value)
|
||||
else:
|
||||
env_dists += '{0}: {1}\n'.format(key, value)
|
||||
|
||||
## ensure mandatories are included
|
||||
env_keys = list(env.keys())
|
||||
for key in env_keys:
|
||||
if key in dflts_keys and dflts_dict[key][0] == 'M' and key not in env_man_seen:
|
||||
env_dists += '{0}: {1}\n'.format(dflts_dict[key][1], dflts_dict[key][2])
|
||||
if key == 'CODENAME':
|
||||
codename = value
|
||||
|
||||
return (codename, env_dists)
|
||||
|
||||
|
||||
def _create_pbuilders(env):
|
||||
'''
|
||||
Create the .pbuilder family of files in user's home directory
|
||||
|
@ -90,72 +195,13 @@ def _create_pbuilders(env):
|
|||
idiosyncrasies can be found :doc:`here
|
||||
</topics/troubleshooting/yaml_idiosyncrasies>`.
|
||||
|
||||
|
||||
'''
|
||||
hook_text = '''#!/bin/sh
|
||||
set -e
|
||||
cat > "/etc/apt/preferences" << EOF
|
||||
|
||||
Package: python-alabaster
|
||||
Pin: release a=testing
|
||||
Pin-Priority: 950
|
||||
|
||||
Package: python-sphinx
|
||||
Pin: release a=testing
|
||||
Pin-Priority: 900
|
||||
|
||||
Package: sphinx-common
|
||||
Pin: release a=testing
|
||||
Pin-Priority: 900
|
||||
|
||||
Package: *
|
||||
Pin: release a=jessie-backports
|
||||
Pin-Priority: 750
|
||||
|
||||
Package: *
|
||||
Pin: release a=stable
|
||||
Pin-Priority: 700
|
||||
|
||||
Package: *
|
||||
Pin: release a=testing
|
||||
Pin-Priority: 650
|
||||
|
||||
Package: *
|
||||
Pin: release a=unstable
|
||||
Pin-Priority: 600
|
||||
|
||||
Package: *
|
||||
Pin: release a=experimental
|
||||
Pin-Priority: 550
|
||||
|
||||
EOF
|
||||
'''
|
||||
|
||||
pbldrc_text = '''DIST="jessie"
|
||||
if [ -n "${DIST}" ]; then
|
||||
TMPDIR=/tmp
|
||||
BASETGZ="`dirname $BASETGZ`/$DIST-base.tgz"
|
||||
DISTRIBUTION=$DIST
|
||||
APTCACHE="/var/cache/pbuilder/$DIST/aptcache"
|
||||
fi
|
||||
HOOKDIR="${HOME}/.pbuilder-hooks"
|
||||
OTHERMIRROR="deb http://ftp.us.debian.org/debian/ stable main contrib non-free | deb http://ftp.us.debian.org/debian/ testing main contrib non-free | deb http://ftp.us.debian.org/debian/ unstable main contrib non-free"
|
||||
'''
|
||||
home = os.path.expanduser('~')
|
||||
pbuilder_hooksdir = os.path.join(home, '.pbuilder-hooks')
|
||||
if not os.path.isdir(pbuilder_hooksdir):
|
||||
os.makedirs(pbuilder_hooksdir)
|
||||
|
||||
g05hook = os.path.join(pbuilder_hooksdir, 'G05apt-preferences')
|
||||
with salt.utils.fopen(g05hook, 'w') as fow:
|
||||
fow.write('{0}'.format(hook_text))
|
||||
|
||||
cmd = 'chmod 755 {0}'.format(g05hook)
|
||||
__salt__['cmd.run'](cmd)
|
||||
|
||||
pbuilderrc = os.path.join(home, '.pbuilderrc')
|
||||
with salt.utils.fopen(pbuilderrc, 'w') as fow:
|
||||
fow.write('{0}'.format(pbldrc_text))
|
||||
if not os.path.isfile(pbuilderrc):
|
||||
raise SaltInvocationError(
|
||||
'pbuilderrc environment is incorrectly setup'
|
||||
)
|
||||
|
||||
env_overrides = _get_build_env(env)
|
||||
if env_overrides and not env_overrides.isspace():
|
||||
|
@ -342,37 +388,29 @@ def make_repo(repodir, keyid=None, env=None):
|
|||
|
||||
salt '*' pkgbuild.make_repo /var/www/html/
|
||||
'''
|
||||
repocfg_text = '''Origin: SaltStack
|
||||
Label: salt_debian
|
||||
Suite: unstable
|
||||
Codename: jessie
|
||||
Architectures: i386 amd64 source
|
||||
Components: contrib
|
||||
Description: SaltStack debian package repo
|
||||
Pull: jessie
|
||||
'''
|
||||
repoconf = os.path.join(repodir, 'conf')
|
||||
if not os.path.isdir(repoconf):
|
||||
os.makedirs(repoconf)
|
||||
|
||||
codename, repocfg_dists = _get_repo_dists_env(env)
|
||||
repoconfdist = os.path.join(repoconf, 'distributions')
|
||||
with salt.utils.fopen(repoconfdist, 'w') as fow:
|
||||
fow.write('{0}'.format(repocfg_text))
|
||||
fow.write('{0}'.format(repocfg_dists))
|
||||
|
||||
if keyid is not None:
|
||||
with salt.utils.fopen(repoconfdist, 'a') as fow:
|
||||
fow.write('SignWith: {0}\n'.format(keyid))
|
||||
|
||||
repocfg_opts = _get_repo_env(env)
|
||||
repocfg_opts = _get_repo_options_env(env)
|
||||
repoconfopts = os.path.join(repoconf, 'options')
|
||||
with salt.utils.fopen(repoconfopts, 'w') as fow:
|
||||
fow.write('{0}'.format(repocfg_opts))
|
||||
|
||||
for debfile in os.listdir(repodir):
|
||||
if debfile.endswith('.changes'):
|
||||
cmd = 'reprepro -Vb . include jessie {0}'.format(os.path.join(repodir, debfile))
|
||||
cmd = 'reprepro -Vb . include {0} {1}'.format(codename, os.path.join(repodir, debfile))
|
||||
__salt__['cmd.run'](cmd, cwd=repodir)
|
||||
|
||||
if debfile.endswith('.deb'):
|
||||
cmd = 'reprepro -Vb . includedeb jessie {0}'.format(os.path.join(repodir, debfile))
|
||||
cmd = 'reprepro -Vb . includedeb {0} {1}'.format(codename, os.path.join(repodir, debfile))
|
||||
__salt__['cmd.run'](cmd, cwd=repodir)
|
||||
|
|
|
@ -5,7 +5,7 @@ Management of Docker Containers
|
|||
.. versionadded:: 2014.1.0
|
||||
|
||||
.. deprecated:: 2015.8.0
|
||||
Future feature development will be done only in :mod:`docker-ng
|
||||
Future feature development will be done only in :mod:`dockerng
|
||||
<salt.modules.dockerng>`. See the documentation for this module for
|
||||
information on the deprecation path.
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ Management of Docker Containers
|
|||
.. versionadded:: 2015.8.0
|
||||
|
||||
|
||||
Why Make a Second Docker Module?
|
||||
--------------------------------
|
||||
Why Make a Second Docker Execution Module?
|
||||
------------------------------------------
|
||||
|
||||
We have received a lot of feedback on our Docker support. In the process of
|
||||
implementing recommended improvements, it became obvious that major changes
|
||||
|
@ -19,7 +19,7 @@ option. This will give users a couple release cycles to modify their scripts,
|
|||
SLS files, etc. to use the new functionality, rather than forcing users to
|
||||
change everything immediately.
|
||||
|
||||
In the **Carbon** release of Salt (due early 2016), this execution module will
|
||||
In the **Carbon** release of Salt (due in 2016), this execution module will
|
||||
take the place of the default Docker execution module, and backwards-compatible
|
||||
naming will be maintained for a couple releases after that to allow users time
|
||||
to replace references to ``dockerng`` with ``docker``.
|
||||
|
@ -28,13 +28,13 @@ to replace references to ``dockerng`` with ``docker``.
|
|||
Installation Prerequisites
|
||||
--------------------------
|
||||
|
||||
This execution module requires at least version 1.0.0 of both docker-py_ and
|
||||
This execution module requires at least version 1.4.0 of both docker-py_ and
|
||||
Docker_. docker-py can easily be installed using :py:func:`pip.install
|
||||
<salt.modules.pip.install>`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt myminion pip.install docker-py
|
||||
salt myminion pip.install docker-py>=1.4.0
|
||||
|
||||
.. _docker-py: https://pypi.python.org/pypi/docker-py
|
||||
.. _Docker: https://www.docker.com/
|
||||
|
@ -234,6 +234,7 @@ import distutils.version # pylint: disable=import-error,no-name-in-module,unuse
|
|||
import fnmatch
|
||||
import functools
|
||||
import gzip
|
||||
import inspect as inspect_module
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
@ -256,6 +257,7 @@ import salt.ext.six as six
|
|||
# pylint: disable=import-error
|
||||
try:
|
||||
import docker
|
||||
import docker.utils
|
||||
HAS_DOCKER_PY = True
|
||||
except ImportError:
|
||||
HAS_DOCKER_PY = False
|
||||
|
@ -287,7 +289,7 @@ __func_alias__ = {
|
|||
|
||||
# Minimum supported versions
|
||||
MIN_DOCKER = (1, 0, 0)
|
||||
MIN_DOCKER_PY = (1, 0, 0)
|
||||
MIN_DOCKER_PY = (1, 4, 0)
|
||||
|
||||
VERSION_RE = r'([\d.]+)'
|
||||
|
||||
|
@ -343,7 +345,7 @@ argument name:
|
|||
'''
|
||||
|
||||
VALID_CREATE_OPTS = {
|
||||
'cmd': {
|
||||
'command': {
|
||||
'path': 'Config:Cmd',
|
||||
},
|
||||
'hostname': {
|
||||
|
@ -1104,25 +1106,25 @@ def _validate_input(action,
|
|||
raise SaltInvocationError(key + ' must be a list of strings')
|
||||
|
||||
# Custom validation functions for container creation options
|
||||
def _valid_cmd(): # pylint: disable=unused-variable
|
||||
def _valid_command(): # pylint: disable=unused-variable
|
||||
'''
|
||||
Must be either a string or a list of strings. Value will be translated
|
||||
to a list of strings
|
||||
'''
|
||||
if kwargs.get('cmd') is None:
|
||||
if kwargs.get('command') is None:
|
||||
# No need to validate
|
||||
return
|
||||
if isinstance(kwargs['cmd'], six.string_types):
|
||||
if isinstance(kwargs['command'], six.string_types):
|
||||
# Translate command into a list of strings
|
||||
try:
|
||||
kwargs['cmd'] = shlex.split(kwargs['cmd'])
|
||||
kwargs['command'] = shlex.split(kwargs['command'])
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
_valid_stringlist('cmd')
|
||||
_valid_stringlist('command')
|
||||
except SaltInvocationError:
|
||||
raise SaltInvocationError(
|
||||
'cmd must be a string or list of strings'
|
||||
'command/cmd must be a string or list of strings'
|
||||
)
|
||||
|
||||
def _valid_user(): # pylint: disable=unused-variable
|
||||
|
@ -2515,10 +2517,10 @@ def create(image,
|
|||
image
|
||||
Image from which to create the container
|
||||
|
||||
cmd or command
|
||||
command or cmd
|
||||
Command to run in the container
|
||||
|
||||
Example: ``cmd=bash`` or ``command=bash``
|
||||
Example: ``command=bash`` or ``cmd=bash``
|
||||
|
||||
.. versionchanged:: 2015.8.1
|
||||
``cmd`` is now also accepted
|
||||
|
@ -2654,13 +2656,13 @@ def create(image,
|
|||
# Create a CentOS 7 container that will stay running once started
|
||||
salt myminion dockerng.create centos:7 name=mycent7 interactive=True tty=True command=bash
|
||||
'''
|
||||
if 'command' in kwargs:
|
||||
if 'cmd' in kwargs:
|
||||
if 'cmd' in kwargs:
|
||||
if 'command' in kwargs:
|
||||
raise SaltInvocationError(
|
||||
'Only one of \'cmd\' and \'command\' can be used. Both '
|
||||
'Only one of \'command\' and \'cmd\' can be used. Both '
|
||||
'arguments are equivalent.'
|
||||
)
|
||||
kwargs['cmd'] = kwargs.pop('command')
|
||||
kwargs['command'] = kwargs.pop('cmd')
|
||||
|
||||
try:
|
||||
# Try to inspect the image, if it fails then we know we need to pull it
|
||||
|
@ -2689,14 +2691,11 @@ def create(image,
|
|||
# Added to manage api change in 1.19.
|
||||
# mem_limit and memswap_limit must be provided in host_config object
|
||||
if salt.utils.version_cmp(version()['ApiVersion'], '1.18') == 1:
|
||||
create_kwargs['host_config'] = docker.utils.create_host_config(
|
||||
mem_limit=create_kwargs.get('mem_limit'),
|
||||
memswap_limit=create_kwargs.get('memswap_limit')
|
||||
client = __context__['docker.client']
|
||||
host_config_args = inspect_module.getargspec(docker.utils.create_host_config).args
|
||||
create_kwargs['host_config'] = client.create_host_config(
|
||||
**dict((arg, create_kwargs.pop(arg, None)) for arg in host_config_args if arg != 'version')
|
||||
)
|
||||
if 'mem_limit' in create_kwargs:
|
||||
del create_kwargs['mem_limit']
|
||||
if 'memswap_limit' in create_kwargs:
|
||||
del create_kwargs['memswap_limit']
|
||||
|
||||
log.debug(
|
||||
'dockerng.create is using the following kwargs to create '
|
||||
|
|
|
@ -80,11 +80,14 @@ def _config_getter(get_opt,
|
|||
|
||||
|
||||
def _expand_path(cwd, user):
|
||||
'''
|
||||
Expand home directory
|
||||
'''
|
||||
try:
|
||||
to_expand = '~' + user if user else '~'
|
||||
except TypeError:
|
||||
# Users should never be numeric but if we don't account for this then
|
||||
# we're going to get a traceback
|
||||
# we're going to get a traceback if someone passes this invalid input.
|
||||
to_expand = '~' + str(user) if user else '~'
|
||||
try:
|
||||
return os.path.join(os.path.expanduser(to_expand), cwd)
|
||||
|
@ -246,6 +249,9 @@ def _get_toplevel(path, user=None):
|
|||
|
||||
|
||||
def _git_config(cwd, user):
|
||||
'''
|
||||
Helper to retrive git config options
|
||||
'''
|
||||
contextkey = 'git.config.' + cwd
|
||||
if contextkey not in __context__:
|
||||
git_dir = rev_parse(cwd,
|
||||
|
@ -958,7 +964,6 @@ config_get_regex = config_get_regexp
|
|||
|
||||
def config_set(key,
|
||||
value=None,
|
||||
add=False,
|
||||
multivar=None,
|
||||
cwd=None,
|
||||
user=None,
|
||||
|
@ -1029,6 +1034,7 @@ def config_set(key,
|
|||
salt myminion git.config_set user.email foo@bar.com global=True
|
||||
'''
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
add_ = kwargs.pop('add', False)
|
||||
global_ = kwargs.pop('global', False)
|
||||
is_global = kwargs.pop('is_global', False)
|
||||
if kwargs:
|
||||
|
@ -1080,7 +1086,7 @@ def config_set(key,
|
|||
|
||||
if value is not None:
|
||||
command = copy.copy(command_prefix)
|
||||
if add:
|
||||
if add_:
|
||||
command.append('--add')
|
||||
else:
|
||||
command.append('--replace-all')
|
||||
|
@ -1830,8 +1836,8 @@ def merge(cwd,
|
|||
rev=None,
|
||||
opts='',
|
||||
user=None,
|
||||
branch=None,
|
||||
ignore_retcode=False):
|
||||
ignore_retcode=False,
|
||||
**kwargs):
|
||||
'''
|
||||
Interface to `git-merge(1)`_
|
||||
|
||||
|
@ -1883,14 +1889,19 @@ def merge(cwd,
|
|||
# .. or merge another rev
|
||||
salt myminion git.merge /path/to/repo rev=upstream/foo
|
||||
'''
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
branch_ = kwargs.pop('branch', None)
|
||||
if kwargs:
|
||||
salt.utils.invalid_kwargs(kwargs)
|
||||
|
||||
cwd = _expand_path(cwd, user)
|
||||
if branch:
|
||||
if branch_:
|
||||
salt.utils.warn_until(
|
||||
'Nitrogen',
|
||||
'The \'branch\' argument to git.merge has been deprecated, please '
|
||||
'use \'rev\' instead.'
|
||||
)
|
||||
rev = branch
|
||||
rev = branch_
|
||||
command = ['git', 'merge']
|
||||
command.extend(_format_opts(opts))
|
||||
if rev:
|
||||
|
@ -2201,7 +2212,7 @@ def push(cwd,
|
|||
user=None,
|
||||
identity=None,
|
||||
ignore_retcode=False,
|
||||
branch=None):
|
||||
**kwargs):
|
||||
'''
|
||||
Interface to `git-push(1)`_
|
||||
|
||||
|
@ -2270,14 +2281,19 @@ def push(cwd,
|
|||
# Delete remote branch 'upstream/temp'
|
||||
salt myminion git.push /path/to/repo upstream :temp
|
||||
'''
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
branch_ = kwargs.pop('branch', None)
|
||||
if kwargs:
|
||||
salt.utils.invalid_kwargs(kwargs)
|
||||
|
||||
cwd = _expand_path(cwd, user)
|
||||
if branch:
|
||||
if branch_:
|
||||
salt.utils.warn_until(
|
||||
'Nitrogen',
|
||||
'The \'branch\' argument to git.push has been deprecated, please '
|
||||
'use \'ref\' instead.'
|
||||
)
|
||||
ref = branch
|
||||
ref = branch_
|
||||
command = ['git', 'push']
|
||||
command.extend(_format_opts(opts))
|
||||
if not isinstance(remote, six.string_types):
|
||||
|
@ -2393,7 +2409,8 @@ def remote_get(cwd,
|
|||
cwd = _expand_path(cwd, user)
|
||||
all_remotes = remotes(cwd,
|
||||
user=user,
|
||||
redact_auth=redact_auth)
|
||||
redact_auth=redact_auth,
|
||||
ignore_retcode=ignore_retcode)
|
||||
if remote not in all_remotes:
|
||||
raise CommandExecutionError(
|
||||
'Remote \'{0}\' not present in git checkout located at {1}'
|
||||
|
@ -2959,8 +2976,8 @@ def submodule(cwd,
|
|||
opts='',
|
||||
user=None,
|
||||
identity=None,
|
||||
init=False,
|
||||
ignore_retcode=False):
|
||||
ignore_retcode=False,
|
||||
**kwargs):
|
||||
'''
|
||||
.. versionchanged:: 2015.8.0
|
||||
Added the ``command`` argument to allow for operations other than
|
||||
|
@ -3038,8 +3055,13 @@ def submodule(cwd,
|
|||
# Unregister submodule (2015.8.0 and later)
|
||||
salt myminion git.submodule /path/to/repo/sub/repo deinit
|
||||
'''
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
init_ = kwargs.pop('init', False)
|
||||
if kwargs:
|
||||
salt.utils.invalid_kwargs(kwargs)
|
||||
|
||||
cwd = _expand_path(cwd, user)
|
||||
if init:
|
||||
if init_:
|
||||
raise SaltInvocationError(
|
||||
'The \'init\' argument is no longer supported. Either set '
|
||||
'\'command\' to \'init\', or include \'--init\' in the \'opts\' '
|
||||
|
@ -3178,14 +3200,14 @@ def version(versioninfo=False):
|
|||
|
||||
def worktree_add(cwd,
|
||||
worktree_path,
|
||||
branch=None,
|
||||
ref=None,
|
||||
reset_branch=None,
|
||||
force=None,
|
||||
detach=False,
|
||||
opts='',
|
||||
user=None,
|
||||
ignore_retcode=False):
|
||||
ignore_retcode=False,
|
||||
**kwargs):
|
||||
'''
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
|
@ -3254,8 +3276,13 @@ def worktree_add(cwd,
|
|||
salt myminion git.worktree_add /path/to/repo/main ../hotfix ref=origin/master
|
||||
salt myminion git.worktree_add /path/to/repo/main ../hotfix branch=hotfix21 ref=v2.1.9.3
|
||||
'''
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
branch_ = kwargs.pop('branch', None)
|
||||
if kwargs:
|
||||
salt.utils.invalid_kwargs(kwargs)
|
||||
|
||||
cwd = _expand_path(cwd, user)
|
||||
if branch and detach:
|
||||
if branch_ and detach:
|
||||
raise SaltInvocationError(
|
||||
'Only one of \'branch\' and \'detach\' is allowed'
|
||||
)
|
||||
|
@ -3269,9 +3296,9 @@ def worktree_add(cwd,
|
|||
)
|
||||
command.append('--detach')
|
||||
else:
|
||||
if not branch:
|
||||
branch = os.path.basename(worktree_path)
|
||||
command.extend(['-B' if reset_branch else '-b', branch])
|
||||
if not branch_:
|
||||
branch_ = os.path.basename(worktree_path)
|
||||
command.extend(['-B' if reset_branch else '-b', branch_])
|
||||
if force:
|
||||
command.append('--force')
|
||||
command.extend(_format_opts(opts))
|
||||
|
|
|
@ -16,6 +16,9 @@ except ImportError:
|
|||
# Import salt libs
|
||||
import salt.utils
|
||||
|
||||
# Import 3rd-party libs
|
||||
import salt.ext.six as six
|
||||
|
||||
__func_alias__ = {
|
||||
'list_': 'list'
|
||||
}
|
||||
|
@ -67,6 +70,8 @@ def install(pecls, defaults=False, force=False, preferred_state='stable'):
|
|||
|
||||
salt '*' pecl.install fuse
|
||||
'''
|
||||
if isinstance(pecls, six.string_types):
|
||||
pecls = [pecls]
|
||||
preferred_state = '-d preferred_state={0}'.format(_cmd_quote(preferred_state))
|
||||
if force:
|
||||
return _pecl('{0} install -f {1}'.format(preferred_state, _cmd_quote(' '.join(pecls))),
|
||||
|
@ -108,6 +113,8 @@ def uninstall(pecls):
|
|||
|
||||
salt '*' pecl.uninstall fuse
|
||||
'''
|
||||
if isinstance(pecls, six.string_types):
|
||||
pecls = [pecls]
|
||||
return _pecl('uninstall {0}'.format(_cmd_quote(' '.join(pecls))))
|
||||
|
||||
|
||||
|
@ -124,6 +131,8 @@ def update(pecls):
|
|||
|
||||
salt '*' pecl.update fuse
|
||||
'''
|
||||
if isinstance(pecls, six.string_types):
|
||||
pecls = [pecls]
|
||||
return _pecl('install -U {0}'.format(_cmd_quote(' '.join(pecls))))
|
||||
|
||||
|
||||
|
|
|
@ -1,41 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Service support for the REST example
|
||||
Provide the service module for the proxy-minion REST sample
|
||||
'''
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import python libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
__proxyenabled__ = ['rest_sample']
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__proxyenabled__ = ['rest_sample']
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = 'service'
|
||||
|
||||
# Don't shadow built-ins.
|
||||
__func_alias__ = {
|
||||
'list_': 'list'
|
||||
}
|
||||
|
||||
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = 'service'
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only work on RestExampleOS
|
||||
Only work on systems that are a proxy minion
|
||||
'''
|
||||
# Enable on these platforms only.
|
||||
enable = set((
|
||||
'RestExampleOS',
|
||||
'proxy',
|
||||
))
|
||||
if __grains__['os'] in enable:
|
||||
if __grains__['os'] == 'proxy':
|
||||
return __virtualname__
|
||||
return False
|
||||
|
||||
|
||||
def start(name):
|
||||
def get_all():
|
||||
'''
|
||||
Start the specified service
|
||||
Return a list of all available services
|
||||
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.get_all
|
||||
'''
|
||||
proxy_fn = 'rest_sample'+ '.service_list'
|
||||
return __opts__['proxymodule'][proxy_fn]()
|
||||
|
||||
|
||||
def list_():
|
||||
'''
|
||||
Return a list of all available services.
|
||||
|
||||
.. versionadded: 2015.8.1
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.list
|
||||
'''
|
||||
return get_all()
|
||||
|
||||
|
||||
def start(name, sig=None):
|
||||
'''
|
||||
Start the specified service on the rest_sample
|
||||
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
CLI Example:
|
||||
|
||||
|
@ -43,12 +71,16 @@ def start(name):
|
|||
|
||||
salt '*' service.start <service name>
|
||||
'''
|
||||
return __opts__['proxymodule']['rest_sample.service_start'](name)
|
||||
|
||||
proxy_fn = 'rest_sample'+ '.service_start'
|
||||
return __opts__['proxymodule'][proxy_fn](name)
|
||||
|
||||
|
||||
def stop(name):
|
||||
def stop(name, sig=None):
|
||||
'''
|
||||
Stop the specified service
|
||||
Stop the specified service on the rest_sample
|
||||
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
CLI Example:
|
||||
|
||||
|
@ -56,12 +88,15 @@ def stop(name):
|
|||
|
||||
salt '*' service.stop <service name>
|
||||
'''
|
||||
return __opts__['proxymodule']['rest_sample.service_stop'](name)
|
||||
proxy_fn = 'rest_sample'+ '.service_stop'
|
||||
return __opts__['proxymodule'][proxy_fn](name)
|
||||
|
||||
|
||||
def restart(name):
|
||||
def restart(name, sig=None):
|
||||
'''
|
||||
Restart the named service
|
||||
Restart the specified service with rest_sample
|
||||
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
CLI Example:
|
||||
|
||||
|
@ -70,13 +105,16 @@ def restart(name):
|
|||
salt '*' service.restart <service name>
|
||||
'''
|
||||
|
||||
return __opts__['proxymodule']['rest_sample.service_restart'](name)
|
||||
proxy_fn = 'rest_sample'+ '.service_restart'
|
||||
return __opts__['proxymodule'][proxy_fn](name)
|
||||
|
||||
|
||||
def status(name):
|
||||
def status(name, sig=None):
|
||||
'''
|
||||
Return the status for a service, returns a bool whether the service is
|
||||
running.
|
||||
Return the status for a service via rest_sample, returns a bool
|
||||
whether the service is running.
|
||||
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
CLI Example:
|
||||
|
||||
|
@ -84,17 +122,30 @@ def status(name):
|
|||
|
||||
salt '*' service.status <service name>
|
||||
'''
|
||||
return __opts__['proxymodule']['rest_sample.service_status'](name)
|
||||
|
||||
proxy_fn = 'rest_sample' + '.service_status'
|
||||
resp = __opts__['proxymodule'][proxy_fn](name)
|
||||
if resp['comment'] == 'stopped':
|
||||
return False
|
||||
if resp['comment'] == 'running':
|
||||
return True
|
||||
|
||||
|
||||
def list_():
|
||||
def running(name, sig=None):
|
||||
'''
|
||||
List services.
|
||||
Return whether this service is running.
|
||||
|
||||
CLI Example:
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.list <service name>
|
||||
'''
|
||||
return __opts__['proxymodule']['rest_sample.service_list']()
|
||||
return status(name).get(name, False)
|
||||
|
||||
|
||||
def enabled(name, sig=None):
|
||||
'''
|
||||
Only the 'redbull' service is 'enabled' in the test
|
||||
|
||||
.. versionadded:: 2015.8.1
|
||||
|
||||
'''
|
||||
return name == 'redbull'
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Provide the service module for the proxy-minion REST sample
|
||||
'''
|
||||
# Import python libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
__proxyenabled__ = ['rest_sample']
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__func_alias__ = {
|
||||
'reload_': 'reload'
|
||||
}
|
||||
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = 'service'
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only work on systems that are a proxy minion
|
||||
'''
|
||||
if __grains__['kernel'] == 'proxy':
|
||||
return __virtualname__
|
||||
return False
|
||||
|
||||
|
||||
def get_all():
|
||||
'''
|
||||
Return a list of all available services
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.get_all
|
||||
'''
|
||||
proxy_fn = 'rest_sample'+ '.service_list'
|
||||
return __opts__['proxymodule'][proxy_fn]()
|
||||
|
||||
|
||||
def start(name):
|
||||
'''
|
||||
Start the specified service on the rest_sample
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.start <service name>
|
||||
'''
|
||||
|
||||
proxy_fn = 'rest_sample'+ '.service_start'
|
||||
return __opts__['proxymodule'][proxy_fn](name)
|
||||
|
||||
|
||||
def stop(name):
|
||||
'''
|
||||
Stop the specified service on the rest_sample
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.stop <service name>
|
||||
'''
|
||||
proxy_fn = 'rest_sample'+ '.service_stop'
|
||||
return __opts__['proxymodule'][proxy_fn](name)
|
||||
|
||||
|
||||
def restart(name):
|
||||
'''
|
||||
Restart the specified service with rest_sample
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.restart <service name>
|
||||
'''
|
||||
|
||||
proxy_fn = 'rest_sample'+ '.service_restart'
|
||||
return __opts__['proxymodule'][proxy_fn](name)
|
||||
|
||||
|
||||
def status(name, sig):
|
||||
'''
|
||||
Return the status for a service via rest_sample, returns a bool
|
||||
whether the service is running.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' service.status <service name>
|
||||
'''
|
||||
|
||||
proxy_fn = 'rest_sample' + '.service_status'
|
||||
resp = __opts__['proxymodule'][proxy_fn](name)
|
||||
if resp['comment'] == 'stopped':
|
||||
return {name: False}
|
||||
if resp['comment'] == 'running':
|
||||
return {name: True}
|
|
@ -408,8 +408,7 @@ def present(
|
|||
for cfg in launch_config:
|
||||
args.update(cfg)
|
||||
if not __opts__['test']:
|
||||
lc_ret = __salt__['state.single']('boto_lc.present', **args)
|
||||
lc_ret = next(six.itervalues(lc_ret))
|
||||
lc_ret = __states__['boto_lc.present'](**args)
|
||||
if lc_ret['result'] is True and lc_ret['changes']:
|
||||
if 'launch_config' not in ret['changes']:
|
||||
ret['changes']['launch_config'] = {}
|
||||
|
@ -612,8 +611,7 @@ def _alarms_present(name, alarms, alarms_from_pillar, region, key, keyid, profil
|
|||
'keyid': keyid,
|
||||
'profile': profile,
|
||||
}
|
||||
ret = __salt__['state.single']('boto_cloudwatch_alarm.present', **kwargs)
|
||||
results = next(six.itervalues(ret))
|
||||
results = __states__['boto_cloudwatch_alarm.present'](**kwargs)
|
||||
if not results['result']:
|
||||
merged_return_value['result'] = False
|
||||
if results.get('changes', {}) != {}:
|
||||
|
|
|
@ -335,8 +335,7 @@ def present(
|
|||
name, region, key, keyid, profile
|
||||
)
|
||||
for cname in cnames:
|
||||
_ret = __salt__['state.single'](
|
||||
'boto_route53.present',
|
||||
_ret = __states__['boto_route53.present'](
|
||||
name=cname.get('name'),
|
||||
value=lb['dns_name'],
|
||||
zone=cname.get('zone'),
|
||||
|
@ -348,7 +347,6 @@ def present(
|
|||
keyid=keyid,
|
||||
profile=profile
|
||||
)
|
||||
_ret = _ret.values()[0]
|
||||
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
|
||||
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
|
||||
if not _ret['result']:
|
||||
|
@ -382,7 +380,6 @@ def register_instances(name, instances, region=None, key=None, keyid=None,
|
|||
- instance-id2
|
||||
'''
|
||||
ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}
|
||||
ret['name'] = name
|
||||
lb = __salt__['boto_elb.exists'](name, region, key, keyid, profile)
|
||||
if lb:
|
||||
health = __salt__['boto_elb.get_instance_health'](name,
|
||||
|
@ -912,9 +909,8 @@ def _alarms_present(name, alarms, alarms_from_pillar, region, key, keyid, profil
|
|||
"keyid": keyid,
|
||||
"profile": profile,
|
||||
}
|
||||
ret = __salt__["state.single"]('boto_cloudwatch_alarm.present', **kwargs)
|
||||
results = next(six.itervalues(ret))
|
||||
if not results["result"]:
|
||||
results = __states__['boto_cloudwatch_alarm.present'](**kwargs)
|
||||
if not results.get('result'):
|
||||
merged_return_value["result"] = results["result"]
|
||||
if results.get("changes", {}) != {}:
|
||||
merged_return_value["changes"][info["name"]] = results["changes"]
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
Manage Docker containers
|
||||
========================
|
||||
|
||||
.. deprecated:: 2015.8.0
|
||||
Future feature development will be done only in :mod:`dockerng
|
||||
<salt.states.dockerng>`. See the documentation for this module for
|
||||
information on the deprecation path.
|
||||
|
||||
`Docker <https://www.docker.io>`_
|
||||
is a lightweight, portable, self-sufficient software container
|
||||
wrapper. The base supported wrapper type is
|
||||
|
|
|
@ -8,6 +8,27 @@ Management of Docker containers
|
|||
This is the state module to accompany the :mod:`dockerng
|
||||
<salt.modules.dockerng>` execution module.
|
||||
|
||||
|
||||
Why Make a Second Docker State Module?
|
||||
--------------------------------------
|
||||
|
||||
We have received a lot of feedback on our Docker support. In the process of
|
||||
implementing recommended improvements, it became obvious that major changes
|
||||
needed to be made to the functions and return data. In the end, a complete
|
||||
rewrite was done.
|
||||
|
||||
The changes being too significant, it was decided that making a separate
|
||||
execution module and state module (called ``dockerng``) would be the best
|
||||
option. This will give users a couple release cycles to modify their scripts,
|
||||
SLS files, etc. to use the new functionality, rather than forcing users to
|
||||
change everything immediately.
|
||||
|
||||
In the **Carbon** release of Salt (due in 2016), this execution module will
|
||||
take the place of the default Docker execution module, and backwards-compatible
|
||||
naming will be maintained for a couple releases after that to allow users time
|
||||
to replace references to ``dockerng`` with ``docker``.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
To pull from a Docker registry, authentication must be configured. See
|
||||
|
@ -47,7 +68,7 @@ NOTSET = object()
|
|||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only load if the dockerio execution module is available
|
||||
Only load if the dockerng execution module is available
|
||||
'''
|
||||
if 'dockerng.version' in __salt__:
|
||||
global _validate_input # pylint: disable=global-statement
|
||||
|
@ -331,8 +352,8 @@ def _compare(actual, create_kwargs, runtime_kwargs):
|
|||
continue
|
||||
|
||||
elif isinstance(data, list):
|
||||
# Compare two sorted lists of items. Won't work for "cmd" or
|
||||
# "entrypoint" because those are both shell commands and the
|
||||
# Compare two sorted lists of items. Won't work for "command"
|
||||
# or "entrypoint" because those are both shell commands and the
|
||||
# original order matters. It will, however, work for "volumes"
|
||||
# because even though "volumes" is a sub-dict nested within the
|
||||
# "actual" dict sorted(somedict) still just gives you a sorted
|
||||
|
@ -425,17 +446,18 @@ def image_present(name,
|
|||
image = ':'.join(_get_repo_tag(name))
|
||||
all_tags = __salt__['dockerng.list_tags']()
|
||||
|
||||
if image in all_tags and not force:
|
||||
ret['result'] = True
|
||||
ret['comment'] = 'Image \'{0}\' already present'.format(name)
|
||||
return ret
|
||||
elif image in all_tags and force:
|
||||
try:
|
||||
image_info = __salt__['dockerng.inspect_image'](name)
|
||||
except Exception as exc:
|
||||
ret['comment'] = \
|
||||
'Unable to get info for image \'{0}\': {1}'.format(name, exc)
|
||||
if image in all_tags:
|
||||
if not force:
|
||||
ret['result'] = True
|
||||
ret['comment'] = 'Image \'{0}\' already present'.format(name)
|
||||
return ret
|
||||
else:
|
||||
try:
|
||||
image_info = __salt__['dockerng.inspect_image'](name)
|
||||
except Exception as exc:
|
||||
ret['comment'] = \
|
||||
'Unable to get info for image \'{0}\': {1}'.format(name, exc)
|
||||
return ret
|
||||
else:
|
||||
image_info = None
|
||||
|
||||
|
@ -732,7 +754,7 @@ def running(name,
|
|||
|
||||
**CONTAINER CONFIGURATION PARAMETERS**
|
||||
|
||||
cmd or command
|
||||
command or cmd
|
||||
Command to run in the container
|
||||
|
||||
.. code-block:: yaml
|
||||
|
@ -740,7 +762,7 @@ def running(name,
|
|||
foo:
|
||||
dockerng.running:
|
||||
- image: bar/baz:latest
|
||||
- cmd: bash
|
||||
- command: bash
|
||||
|
||||
OR
|
||||
|
||||
|
@ -749,7 +771,7 @@ def running(name,
|
|||
foo:
|
||||
dockerng.running:
|
||||
- image: bar/baz:latest
|
||||
- command: bash
|
||||
- cmd: bash
|
||||
|
||||
.. versionchanged:: 2015.8.1
|
||||
``cmd`` is now also accepted
|
||||
|
@ -1344,15 +1366,15 @@ def running(name,
|
|||
ret['comment'] = 'The \'image\' argument is required'
|
||||
return ret
|
||||
|
||||
if 'command' in kwargs:
|
||||
if 'cmd' in kwargs:
|
||||
if 'cmd' in kwargs:
|
||||
if 'command' in kwargs:
|
||||
ret['comment'] = (
|
||||
'Only one of \'cmd\' and \'command\' can be used. Both '
|
||||
'Only one of \'command\' and \'cmd\' can be used. Both '
|
||||
'arguments are equivalent.'
|
||||
)
|
||||
ret['result'] = False
|
||||
return ret
|
||||
kwargs['cmd'] = kwargs.pop('command')
|
||||
kwargs['command'] = kwargs.pop('cmd')
|
||||
|
||||
try:
|
||||
image = ':'.join(_get_repo_tag(image))
|
||||
|
|
|
@ -1584,10 +1584,11 @@ def config_unset(name,
|
|||
all : False
|
||||
If ``True``, unset all matches
|
||||
|
||||
repo : None
|
||||
An optional location of a git repository for local operations
|
||||
repo
|
||||
Location of the git repository for which the config value should be
|
||||
set. Required unless ``global`` is set to ``True``.
|
||||
|
||||
user : None
|
||||
user
|
||||
Optional name of a user as whom `git config` will be run
|
||||
|
||||
global : False
|
||||
|
@ -1659,11 +1660,12 @@ def config_unset(name,
|
|||
|
||||
# Get matching keys/values
|
||||
pre_matches = __salt__['git.config_get_regexp'](
|
||||
cwd='global' if global_ else repo,
|
||||
cwd=repo,
|
||||
key=key,
|
||||
value_regex=value_regex,
|
||||
user=user,
|
||||
ignore_retcode=True
|
||||
ignore_retcode=True,
|
||||
**{'global': global_}
|
||||
)
|
||||
|
||||
if not pre_matches:
|
||||
|
@ -1706,11 +1708,12 @@ def config_unset(name,
|
|||
# Get all keys matching the key expression, so we can accurately report
|
||||
# on changes made.
|
||||
pre = __salt__['git.config_get_regexp'](
|
||||
cwd='global' if global_ else repo,
|
||||
cwd=repo,
|
||||
key=key,
|
||||
value_regex=None,
|
||||
user=user,
|
||||
ignore_retcode=True
|
||||
ignore_retcode=True,
|
||||
**{'global': global_}
|
||||
)
|
||||
|
||||
failed = []
|
||||
|
@ -1719,11 +1722,12 @@ def config_unset(name,
|
|||
for key_name in pre_matches:
|
||||
try:
|
||||
__salt__['git.config_unset'](
|
||||
cwd='global' if global_ else repo,
|
||||
cwd=repo,
|
||||
key=name,
|
||||
value_regex=value_regex,
|
||||
all=all_,
|
||||
user=user
|
||||
user=user,
|
||||
**{'global': global_}
|
||||
)
|
||||
except CommandExecutionError as exc:
|
||||
msg = 'Failed to unset \'{0}\''.format(key_name)
|
||||
|
@ -1741,11 +1745,12 @@ def config_unset(name,
|
|||
)
|
||||
|
||||
post = __salt__['git.config_get_regexp'](
|
||||
cwd='global' if global_ else repo,
|
||||
cwd=repo,
|
||||
key=key,
|
||||
value_regex=None,
|
||||
user=user,
|
||||
ignore_retcode=True
|
||||
ignore_retcode=True,
|
||||
**{'global': global_}
|
||||
)
|
||||
|
||||
for key_name, values in six.iteritems(pre):
|
||||
|
@ -1759,11 +1764,12 @@ def config_unset(name,
|
|||
post_matches = post
|
||||
else:
|
||||
post_matches = __salt__['git.config_get_regexp'](
|
||||
cwd='global' if global_ else repo,
|
||||
cwd=repo,
|
||||
key=key,
|
||||
value_regex=value_regex,
|
||||
user=user,
|
||||
ignore_retcode=True
|
||||
ignore_retcode=True,
|
||||
**{'global': global_}
|
||||
)
|
||||
|
||||
if post_matches:
|
||||
|
@ -1809,10 +1815,11 @@ def config_set(name,
|
|||
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
repo : None
|
||||
An optional location of a git repository for local operations
|
||||
repo
|
||||
Location of the git repository for which the config value should be
|
||||
set. Required unless ``global`` is set to ``True``.
|
||||
|
||||
user : None
|
||||
user
|
||||
Optional name of a user as whom `git config` will be run
|
||||
|
||||
global : False
|
||||
|
@ -1920,11 +1927,11 @@ def config_set(name,
|
|||
|
||||
# Get current value
|
||||
pre = __salt__['git.config_get'](
|
||||
cwd='global' if global_ else repo,
|
||||
cwd=repo,
|
||||
key=name,
|
||||
user=user,
|
||||
ignore_retcode=True,
|
||||
**{'all': True}
|
||||
**{'all': True, 'global': global_}
|
||||
)
|
||||
|
||||
if desired == pre:
|
||||
|
@ -1948,11 +1955,12 @@ def config_set(name,
|
|||
try:
|
||||
# Set/update config value
|
||||
post = __salt__['git.config_set'](
|
||||
cwd='global' if global_ else repo,
|
||||
cwd=repo,
|
||||
key=name,
|
||||
value=value,
|
||||
multivar=multivar,
|
||||
user=user
|
||||
user=user,
|
||||
**{'global': global_}
|
||||
)
|
||||
except CommandExecutionError as exc:
|
||||
return _fail(
|
||||
|
|
|
@ -318,7 +318,8 @@ class AsyncZeroMQPubChannel(salt.transport.mixins.auth.AESPubClientMixin, salt.t
|
|||
# TODO: Optionally call stream.close() on newer pyzmq? Its broken on some
|
||||
self._stream.io_loop.remove_handler(self._stream.socket)
|
||||
self._stream.socket.close(0)
|
||||
self.context.term()
|
||||
if hasattr(self, 'context'):
|
||||
self.context.term()
|
||||
|
||||
def __del__(self):
|
||||
self.destroy()
|
||||
|
|
|
@ -86,6 +86,6 @@ class SyncWrapper(object):
|
|||
'''
|
||||
On deletion of the async wrapper, make sure to clean up the async stuff
|
||||
'''
|
||||
self.io_loop.close()
|
||||
if hasattr(self, 'async'):
|
||||
del self.async
|
||||
self.io_loop.close()
|
||||
|
|
|
@ -93,14 +93,22 @@ def copyfile(source, dest, backup_mode='', cachedir=''):
|
|||
def rename(src, dst):
|
||||
'''
|
||||
On Windows, os.rename() will fail with a WindowsError exception if a file
|
||||
exists at the destination path. This function removes the destination path
|
||||
first before attempting the os.rename().
|
||||
exists at the destination path. This function checks for this error and if
|
||||
found, it deletes the destination path first.
|
||||
'''
|
||||
try:
|
||||
os.remove(dst)
|
||||
os.rename(src, dst)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise MinionError(
|
||||
'Error: Unable to remove {0}: {1}'.format(dst, exc.strerror)
|
||||
)
|
||||
os.rename(src, dst)
|
||||
if exc.errno != errno.EEXIST:
|
||||
raise
|
||||
try:
|
||||
os.remove(dst)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise MinionError(
|
||||
'Error: Unable to remove {0}: {1}'.format(
|
||||
dst,
|
||||
exc.strerror
|
||||
)
|
||||
)
|
||||
os.rename(src, dst)
|
||||
|
|
|
@ -742,7 +742,7 @@ class Pygit2(GitProvider):
|
|||
for line in subprocess.Popen(
|
||||
'git ls-remote origin',
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
close_fds=not salt.utils.is_windows(),
|
||||
cwd=self.repo.workdir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT).communicate()[0].splitlines():
|
||||
|
@ -1508,7 +1508,7 @@ class GitBase(object):
|
|||
self.cache_root = os.path.join(self.opts['cachedir'], self.role)
|
||||
self.env_cache = os.path.join(self.cache_root, 'envs.p')
|
||||
self.hash_cachedir = os.path.join(
|
||||
self.cache_root, self.role, 'hash')
|
||||
self.cache_root, 'hash')
|
||||
self.file_list_cachedir = os.path.join(
|
||||
self.opts['cachedir'], 'file_lists', self.role)
|
||||
|
||||
|
@ -1609,7 +1609,7 @@ class GitBase(object):
|
|||
pass
|
||||
to_remove = []
|
||||
for item in cachedir_ls:
|
||||
if item in ('gitfs', 'refs'):
|
||||
if item in ('hash', 'refs'):
|
||||
continue
|
||||
path = os.path.join(self.cache_root, item)
|
||||
if os.path.isdir(path):
|
||||
|
|
|
@ -559,9 +559,9 @@ def dependency_information(include_salt_cloud=False):
|
|||
('cffi', 'cffi', '__version__'),
|
||||
('pycparser', 'pycparser', '__version__'),
|
||||
('gitdb', 'gitdb', '__version__'),
|
||||
('gitpython', 'gitpython', '__version__'),
|
||||
('python-gnupg', 'python-gnupg', '__version__'),
|
||||
('mysql-python', 'mysql-python', '__version__'),
|
||||
('gitpython', 'git', '__version__'),
|
||||
('python-gnupg', 'gnupg', '__version__'),
|
||||
('mysql-python', 'MySQLdb', '__version__'),
|
||||
('cherrypy', 'cherrypy', '__version__'),
|
||||
]
|
||||
|
||||
|
|
|
@ -35,10 +35,7 @@ import integration
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _worktrees_supported():
|
||||
'''
|
||||
Check if the git version is 2.5.0 or later
|
||||
'''
|
||||
def _git_version():
|
||||
git_version = subprocess.Popen(
|
||||
['git', '--version'],
|
||||
shell=False,
|
||||
|
@ -49,9 +46,16 @@ def _worktrees_supported():
|
|||
log.debug('Git not installed')
|
||||
return False
|
||||
log.debug('Detected git version ' + git_version)
|
||||
return LooseVersion(git_version.split()[-1])
|
||||
|
||||
|
||||
def _worktrees_supported():
|
||||
'''
|
||||
Check if the git version is 2.5.0 or later
|
||||
'''
|
||||
try:
|
||||
return LooseVersion(git_version.split()[-1]) >= LooseVersion('2.5.0')
|
||||
except Exception:
|
||||
return _git_version() >= LooseVersion('2.5.0')
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
|
@ -894,6 +898,13 @@ class GitModuleTest(integration.ModuleCase):
|
|||
This tests git.worktree_add, git.is_worktree, git.list_worktrees,
|
||||
git.worktree_rm, and git.worktree_prune
|
||||
'''
|
||||
# We don't need to enclose this comparison in a try/except, since the
|
||||
# decorator would skip this test if git is not installed and we'd never
|
||||
# get here in the first place.
|
||||
if _git_version() >= LooseVersion('2.6.0'):
|
||||
worktree_add_prefix = 'Preparing '
|
||||
else:
|
||||
worktree_add_prefix = 'Enter '
|
||||
worktree_path = tempfile.mkdtemp(dir=integration.TMP)
|
||||
worktree_basename = os.path.basename(worktree_path)
|
||||
worktree_path2 = tempfile.mkdtemp(dir=integration.TMP)
|
||||
|
@ -902,11 +913,11 @@ class GitModuleTest(integration.ModuleCase):
|
|||
ret = self.run_function(
|
||||
'git.worktree_add', [self.repo, worktree_path],
|
||||
)
|
||||
self.assertTrue('Enter ' + worktree_path in ret)
|
||||
self.assertTrue(worktree_add_prefix + worktree_path in ret)
|
||||
ret = self.run_function(
|
||||
'git.worktree_add', [self.repo, worktree_path2]
|
||||
)
|
||||
self.assertTrue('Enter ' + worktree_path2 in ret)
|
||||
self.assertTrue(worktree_add_prefix + worktree_path2 in ret)
|
||||
# Check if this new path is a worktree
|
||||
self.assertTrue(self.run_function('git.is_worktree', [worktree_path]))
|
||||
# Check if the main repo is a worktree
|
||||
|
|
|
@ -26,6 +26,10 @@ dockerng_mod.__context__ = {'docker.docker_version': ''}
|
|||
dockerng_mod.__salt__ = {}
|
||||
|
||||
|
||||
def _docker_py_version():
|
||||
return dockerng_mod.docker.version_info if dockerng_mod.HAS_DOCKER_PY else None
|
||||
|
||||
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class DockerngTestCase(TestCase):
|
||||
'''
|
||||
|
@ -78,6 +82,63 @@ class DockerngTestCase(TestCase):
|
|||
mine_send.assert_called_with('dockerng.ps', verbose=True, all=True,
|
||||
host=True)
|
||||
|
||||
@skipIf(_docker_py_version() < (1, 4, 0),
|
||||
'docker module must be installed to run this test or is too old. >=1.4.0')
|
||||
@patch.object(dockerng_mod, 'images', MagicMock())
|
||||
@patch.object(dockerng_mod, 'inspect_image')
|
||||
@patch.object(dockerng_mod, 'version', Mock(return_value={'ApiVersion': '1.19'}))
|
||||
def test_create_with_arg_cmd(self, *args):
|
||||
'''
|
||||
When cmd argument is passed check it is renamed to command.
|
||||
'''
|
||||
__salt__ = {
|
||||
'config.get': Mock(),
|
||||
'mine.send': Mock(),
|
||||
}
|
||||
host_config = {}
|
||||
client = Mock()
|
||||
client.api_version = '1.19'
|
||||
client.create_host_config.return_value = host_config
|
||||
client.create_container.return_value = {}
|
||||
with patch.dict(dockerng_mod.__dict__,
|
||||
{'__salt__': __salt__}):
|
||||
with patch.dict(dockerng_mod.__context__,
|
||||
{'docker.client': client}):
|
||||
dockerng_mod.create('image', cmd='ls', name='ctn')
|
||||
client.create_container.assert_called_once_with(
|
||||
command='ls',
|
||||
host_config=host_config,
|
||||
image='image',
|
||||
name='ctn')
|
||||
|
||||
@skipIf(_docker_py_version() < (1, 4, 0),
|
||||
'docker module must be installed to run this test or is too old. >=1.4.0')
|
||||
@patch.object(dockerng_mod, 'images', MagicMock())
|
||||
@patch.object(dockerng_mod, 'inspect_image')
|
||||
@patch.object(dockerng_mod, 'version', Mock(return_value={'ApiVersion': '1.19'}))
|
||||
def test_create_send_host_config(self, *args):
|
||||
'''
|
||||
Check host_config object is passed to create_container.
|
||||
'''
|
||||
__salt__ = {
|
||||
'config.get': Mock(),
|
||||
'mine.send': Mock(),
|
||||
}
|
||||
host_config = {'PublishAllPorts': True}
|
||||
client = Mock()
|
||||
client.api_version = '1.19'
|
||||
client.create_host_config.return_value = host_config
|
||||
client.create_container.return_value = {}
|
||||
with patch.dict(dockerng_mod.__dict__,
|
||||
{'__salt__': __salt__}):
|
||||
with patch.dict(dockerng_mod.__context__,
|
||||
{'docker.client': client}):
|
||||
dockerng_mod.create('image', name='ctn', publish_all_ports=True)
|
||||
client.create_container.assert_called_once_with(
|
||||
host_config=host_config,
|
||||
image='image',
|
||||
name='ctn')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from integration import run_tests
|
||||
|
|
|
@ -23,6 +23,7 @@ from salt.states import boto_elb
|
|||
|
||||
boto_elb.__salt__ = {}
|
||||
boto_elb.__opts__ = {}
|
||||
boto_elb.__states__ = {}
|
||||
|
||||
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
|
@ -59,9 +60,7 @@ class BotoElbTestCase(TestCase):
|
|||
mock_true_bool = MagicMock(return_value=True)
|
||||
mock_attributes = MagicMock(return_value=attrs)
|
||||
mock_health_check = MagicMock(return_value=health_check)
|
||||
mock_ret = MagicMock(return_value={'myelb': ret1})
|
||||
|
||||
mock = MagicMock(return_value={})
|
||||
with patch.dict(boto_elb.__salt__,
|
||||
{'config.option': mock,
|
||||
'boto_elb.exists': mock_false_bool,
|
||||
|
@ -78,61 +77,56 @@ class BotoElbTestCase(TestCase):
|
|||
self.assertFalse(ret['result'])
|
||||
|
||||
mock = MagicMock(return_value={})
|
||||
mock_ret = MagicMock(return_value={'myelb': ret1})
|
||||
with patch.dict(boto_elb.__salt__,
|
||||
{'config.option': mock,
|
||||
'boto_elb.exists': mock_false_bool,
|
||||
'boto_elb.create': mock_true_bool,
|
||||
'boto_elb.get_attributes': mock_attributes,
|
||||
'boto_elb.get_health_check': mock_health_check,
|
||||
'boto_elb.get_elb_config': mock,
|
||||
'state.single': mock_ret}):
|
||||
'boto_elb.get_elb_config': mock}):
|
||||
with patch.dict(boto_elb.__opts__, {'test': False}):
|
||||
ret = boto_elb.present(
|
||||
name,
|
||||
listeners,
|
||||
availability_zones=avail_zones,
|
||||
health_check=health_check,
|
||||
alarms=alarms
|
||||
)
|
||||
self.assertTrue(boto_elb.__salt__['boto_elb.exists'].called)
|
||||
self.assertTrue(boto_elb.__salt__['boto_elb.create'].called)
|
||||
self.assertTrue(boto_elb.__salt__['state.single'].called)
|
||||
self.assertFalse(
|
||||
boto_elb.__salt__['boto_elb.get_attributes'].called
|
||||
)
|
||||
self.assertTrue(
|
||||
boto_elb.__salt__['boto_elb.get_health_check'].called
|
||||
)
|
||||
self.assertIn('ELB myelb created.', ret['comment'])
|
||||
self.assertTrue(ret['result'])
|
||||
with patch.dict(boto_elb.__states__, {'boto_cloudwatch_alarm.present': MagicMock(return_value=ret1)}):
|
||||
ret = boto_elb.present(
|
||||
name,
|
||||
listeners,
|
||||
availability_zones=avail_zones,
|
||||
health_check=health_check,
|
||||
alarms=alarms
|
||||
)
|
||||
self.assertTrue(boto_elb.__salt__['boto_elb.exists'].called)
|
||||
self.assertTrue(boto_elb.__salt__['boto_elb.create'].called)
|
||||
self.assertTrue(boto_elb.__states__['boto_cloudwatch_alarm.present'].called)
|
||||
self.assertFalse(
|
||||
boto_elb.__salt__['boto_elb.get_attributes'].called
|
||||
)
|
||||
self.assertTrue(
|
||||
boto_elb.__salt__['boto_elb.get_health_check'].called
|
||||
)
|
||||
self.assertIn('ELB myelb created.', ret['comment'])
|
||||
self.assertTrue(ret['result'])
|
||||
|
||||
mock = MagicMock(return_value={})
|
||||
mock_elb = MagicMock(return_value={'dns_name': 'myelb.amazon.com'})
|
||||
mock_ret = MagicMock(return_value={'myelb': ret1})
|
||||
with patch.dict(boto_elb.__salt__,
|
||||
{'config.option': mock,
|
||||
'boto_elb.exists': mock_false_bool,
|
||||
'boto_elb.create': mock_true_bool,
|
||||
'boto_elb.get_attributes': mock_attributes,
|
||||
'boto_elb.get_health_check': mock_health_check,
|
||||
'boto_elb.get_elb_config': mock_elb,
|
||||
'state.single': mock_ret}):
|
||||
'boto_elb.get_elb_config': mock_elb}):
|
||||
with patch.dict(boto_elb.__opts__, {'test': False}):
|
||||
ret = boto_elb.present(
|
||||
name,
|
||||
listeners,
|
||||
availability_zones=avail_zones,
|
||||
health_check=health_check,
|
||||
cnames=cnames
|
||||
)
|
||||
self.assertTrue(boto_elb.__salt__['state.single'].called)
|
||||
cname_call = boto_elb.__salt__['state.single'].mock_calls[0]
|
||||
self.assertEqual(
|
||||
cname_call[1][0],
|
||||
'boto_route53.present'
|
||||
)
|
||||
self.assertTrue(ret['result'])
|
||||
with patch.dict(boto_elb.__states__, {'boto_route53.present': MagicMock(return_value=ret1)}):
|
||||
ret = boto_elb.present(
|
||||
name,
|
||||
listeners,
|
||||
availability_zones=avail_zones,
|
||||
health_check=health_check,
|
||||
cnames=cnames
|
||||
)
|
||||
mock_changes = {'new': {'elb': 'myelb'}, 'old': {'elb': None}}
|
||||
self.assertTrue(boto_elb.__states__['boto_route53.present'].called)
|
||||
self.assertEqual(mock_changes, ret['changes'])
|
||||
self.assertTrue(ret['result'])
|
||||
|
||||
# 'register_instances' function tests: 1
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import salt.transport.client
|
|||
import salt.exceptions
|
||||
|
||||
# Import Salt Testing libs
|
||||
from salttesting import TestCase
|
||||
from salttesting import TestCase, skipIf
|
||||
from salttesting.helpers import ensure_in_syspath
|
||||
ensure_in_syspath('../')
|
||||
import integration
|
||||
|
@ -178,6 +178,7 @@ class BaseTCPPubCase(AsyncTestCase):
|
|||
raise Exception('FDs still attached to the IOLoop: {0}'.format(failures))
|
||||
|
||||
|
||||
@skipIf(True, 'Skip until we can devote time to fix this test')
|
||||
class AsyncPubChannelTest(BaseTCPPubCase, PubChannelMixin):
|
||||
'''
|
||||
Tests around the publish system
|
||||
|
|
|
@ -24,7 +24,7 @@ import salt.transport.client
|
|||
import salt.exceptions
|
||||
|
||||
# Import Salt Testing libs
|
||||
from salttesting import TestCase
|
||||
from salttesting import TestCase, skipIf
|
||||
from salttesting.helpers import ensure_in_syspath
|
||||
ensure_in_syspath('../')
|
||||
|
||||
|
@ -183,6 +183,7 @@ class BaseZMQPubCase(AsyncTestCase):
|
|||
raise Exception('FDs still attached to the IOLoop: {0}'.format(failures))
|
||||
|
||||
|
||||
@skipIf(True, 'Skip until we can devote time to fix this test')
|
||||
class AsyncPubChannelTest(BaseZMQPubCase, PubChannelMixin):
|
||||
'''
|
||||
Tests around the publish system
|
||||
|
|
Loading…
Add table
Reference in a new issue