Mini Shell
"""
Support for Tomcat
This module uses the manager webapp to manage Apache tomcat webapps.
If the manager webapp is not configured some of the functions won't work.
:configuration:
- Java bin path should be in default path
- If ipv6 is enabled make sure you permit manager access to ipv6 interface
"0:0:0:0:0:0:0:1"
- If you are using tomcat.tar.gz it has to be installed or symlinked under
``/opt``, preferably using name tomcat
- "tomcat.signal start/stop" works but it does not use the startup scripts
The following grains/pillar should be set:
.. code-block:: yaml
tomcat-manager:
user: <username>
passwd: <password>
or the old format:
.. code-block:: yaml
tomcat-manager.user: <username>
tomcat-manager.passwd: <password>
Also configure a user in the conf/tomcat-users.xml file:
.. code-block:: xml
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="manager-script"/>
<user username="tomcat" password="tomcat" roles="manager-script"/>
</tomcat-users>
.. note::
- More information about tomcat manager:
http://tomcat.apache.org/tomcat-7.0-doc/manager-howto.html
- if you use only this module for deployments you've might want to strict
access to the manager only from localhost for more info:
http://tomcat.apache.org/tomcat-7.0-doc/manager-howto.html#Configuring_Manager_Application_Access
- Tested on:
JVM Vendor:
Sun Microsystems Inc.
JVM Version:
1.6.0_43-b01
OS Architecture:
amd64
OS Name:
Linux
OS Version:
2.6.32-358.el6.x86_64
Tomcat Version:
Apache Tomcat/7.0.37
"""
import glob
import hashlib
import logging
import os
import re
import tempfile
import urllib.parse
import urllib.request
import salt.utils.data
import salt.utils.stringutils
log = logging.getLogger(__name__)
__func_alias__ = {"reload_": "reload"}
# Support old-style grains/pillar
# config as well as new.
__valid_configs = {
"user": ["tomcat-manager.user", "tomcat-manager:user"],
"passwd": ["tomcat-manager.passwd", "tomcat-manager:passwd"],
}
def __virtual__():
"""
Only load tomcat if it is installed or if grains/pillar config exists
"""
if __catalina_home() or _auth("dummy"):
return "tomcat"
return (
False,
"Tomcat execution module not loaded: neither Tomcat installed locally nor"
" tomcat-manager credentials set in grains/pillar/config.",
)
def __catalina_home():
"""
Tomcat paths differ depending on packaging
"""
locations = ["/usr/share/tomcat*", "/opt/tomcat"]
for location in locations:
folders = glob.glob(location)
if folders:
for catalina_home in folders:
if os.path.isdir(catalina_home + "/bin"):
return catalina_home
return False
def _get_credentials():
"""
Get the username and password from opts, grains, or pillar
"""
ret = {"user": False, "passwd": False}
# Loop through opts, grains, and pillar
# Return the first acceptable configuration found
for item in ret:
for struct in [__opts__, __grains__, __pillar__]:
# Look for the config key
# Support old-style config format and new
for config_key in __valid_configs[item]:
value = salt.utils.data.traverse_dict_and_list(struct, config_key, None)
if value:
ret[item] = value
break
return ret["user"], ret["passwd"]
def _auth(uri):
"""
returns a authentication handler.
Get user & password from grains, if are not set default to
modules.config.option
If user & pass are missing return False
"""
user, password = _get_credentials()
if user is False or password is False:
return False
basic = urllib.request.HTTPBasicAuthHandler()
basic.add_password(
realm="Tomcat Manager Application", uri=uri, user=user, passwd=password
)
digest = urllib.request.HTTPDigestAuthHandler()
digest.add_password(
realm="Tomcat Manager Application", uri=uri, user=user, passwd=password
)
return urllib.request.build_opener(basic, digest)
def extract_war_version(war):
"""
Extract the version from the war file name. There does not seem to be a
standard for encoding the version into the `war file name`_
.. _`war file name`: https://tomcat.apache.org/tomcat-6.0-doc/deployer-howto.html
Examples:
.. code-block:: bash
/path/salt-2015.8.6.war -> 2015.8.6
/path/V6R2013xD5.war -> None
"""
basename = os.path.basename(war)
war_package = os.path.splitext(basename)[0] # remove '.war'
version = re.findall("-([\\d.-]+)$", war_package) # try semver
return version[0] if version and len(version) == 1 else None # default to none
def _wget(cmd, opts=None, url="http://localhost:8080/manager", timeout=180):
"""
A private function used to issue the command to tomcat via the manager
webapp
cmd
the command to execute
url
The URL of the server manager webapp (example:
http://localhost:8080/manager)
opts
a dict of arguments
timeout
timeout for HTTP request
Return value is a dict in the from of::
{
res: [True|False]
msg: list of lines we got back from the manager
}
"""
ret = {"res": True, "msg": []}
# prepare authentication
auth = _auth(url)
if auth is False:
ret["res"] = False
ret["msg"] = "missing username and password settings (grain/pillar)"
return ret
# prepare URL
if url[-1] != "/":
url += "/"
url6 = url
url += f"text/{cmd}"
url6 += f"{cmd}"
if opts:
url += f"?{urllib.parse.urlencode(opts)}"
url6 += f"?{urllib.parse.urlencode(opts)}"
# Make the HTTP request
urllib.request.install_opener(auth)
try:
# Trying tomcat >= 7 url
ret["msg"] = urllib.request.urlopen(url, timeout=timeout).read().splitlines()
except Exception: # pylint: disable=broad-except
try:
# Trying tomcat6 url
ret["msg"] = (
urllib.request.urlopen(url6, timeout=timeout).read().splitlines()
)
except Exception: # pylint: disable=broad-except
ret["msg"] = "Failed to create HTTP request"
# Force all byte strings to utf-8 strings, for python >= 3.4
for key, value in enumerate(ret["msg"]):
try:
ret["msg"][key] = salt.utils.stringutils.to_unicode(value, "utf-8")
except (UnicodeDecodeError, AttributeError):
pass
if not ret["msg"][0].startswith("OK"):
ret["res"] = False
return ret
def _simple_cmd(cmd, app, url="http://localhost:8080/manager", timeout=180):
"""
Simple command wrapper to commands that need only a path option
"""
try:
opts = {"path": app, "version": ls(url)[app]["version"]}
return "\n".join(_wget(cmd, opts, url, timeout=timeout)["msg"])
except Exception: # pylint: disable=broad-except
return f"FAIL - No context exists for path {app}"
# Functions
def leaks(url="http://localhost:8080/manager", timeout=180):
"""
Find memory leaks in tomcat
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.leaks
"""
return _wget("findleaks", {"statusLine": "true"}, url, timeout=timeout)["msg"]
def status(url="http://localhost:8080/manager", timeout=180):
"""
Used to test if the tomcat manager is up
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.status
salt '*' tomcat.status http://localhost:8080/manager
"""
return _wget("list", {}, url, timeout=timeout)["res"]
def ls(url="http://localhost:8080/manager", timeout=180):
"""
list all the deployed webapps
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.ls
salt '*' tomcat.ls http://localhost:8080/manager
"""
ret = {}
data = _wget("list", "", url, timeout=timeout)
if data["res"] is False:
return {}
data["msg"].pop(0)
for line in data["msg"]:
tmp = line.split(":")
ret[tmp[0]] = {
"mode": tmp[1],
"sessions": tmp[2],
"fullname": tmp[3],
"version": "",
}
sliced = tmp[3].split("##")
if len(sliced) > 1:
ret[tmp[0]]["version"] = sliced[1]
return ret
def stop(app, url="http://localhost:8080/manager", timeout=180):
"""
Stop the webapp
app
the webapp context path
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.stop /jenkins
salt '*' tomcat.stop /jenkins http://localhost:8080/manager
"""
return _simple_cmd("stop", app, url, timeout=timeout)
def start(app, url="http://localhost:8080/manager", timeout=180):
"""
Start the webapp
app
the webapp context path
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.start /jenkins
salt '*' tomcat.start /jenkins http://localhost:8080/manager
"""
return _simple_cmd("start", app, url, timeout=timeout)
def reload_(app, url="http://localhost:8080/manager", timeout=180):
"""
Reload the webapp
app
the webapp context path
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.reload /jenkins
salt '*' tomcat.reload /jenkins http://localhost:8080/manager
"""
return _simple_cmd("reload", app, url, timeout=timeout)
def sessions(app, url="http://localhost:8080/manager", timeout=180):
"""
return the status of the webapp sessions
app
the webapp context path
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.sessions /jenkins
salt '*' tomcat.sessions /jenkins http://localhost:8080/manager
"""
return _simple_cmd("sessions", app, url, timeout=timeout)
def status_webapp(app, url="http://localhost:8080/manager", timeout=180):
"""
return the status of the webapp (stopped | running | missing)
app
the webapp context path
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.status_webapp /jenkins
salt '*' tomcat.status_webapp /jenkins http://localhost:8080/manager
"""
webapps = ls(url, timeout=timeout)
for i in webapps:
if i == app:
return webapps[i]["mode"]
return "missing"
def serverinfo(url="http://localhost:8080/manager", timeout=180):
"""
return details about the server
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.serverinfo
salt '*' tomcat.serverinfo http://localhost:8080/manager
"""
data = _wget("serverinfo", {}, url, timeout=timeout)
if data["res"] is False:
return {"error": data["msg"]}
ret = {}
data["msg"].pop(0)
for line in data["msg"]:
tmp = line.split(":")
ret[tmp[0].strip()] = tmp[1].strip()
return ret
def undeploy(app, url="http://localhost:8080/manager", timeout=180):
"""
Undeploy a webapp
app
the webapp context path
url : http://localhost:8080/manager
the URL of the server manager webapp
timeout : 180
timeout for HTTP request
CLI Examples:
.. code-block:: bash
salt '*' tomcat.undeploy /jenkins
salt '*' tomcat.undeploy /jenkins http://localhost:8080/manager
"""
return _simple_cmd("undeploy", app, url, timeout=timeout)
def deploy_war(
war,
context,
force="no",
url="http://localhost:8080/manager",
saltenv="base",
timeout=180,
temp_war_location=None,
version=True,
):
"""
Deploy a WAR file
war
absolute path to WAR file (should be accessible by the user running
tomcat) or a path supported by the salt.modules.cp.get_file function
context
the context path to deploy
force : False
set True to deploy the webapp even one is deployed in the context
url : http://localhost:8080/manager
the URL of the server manager webapp
saltenv : base
the environment for WAR file in used by salt.modules.cp.get_url
function
timeout : 180
timeout for HTTP request
temp_war_location : None
use another location to temporarily copy to war file
by default the system's temp directory is used
version : ''
Specify the war version. If this argument is provided, it overrides
the version encoded in the war file name, if one is present.
Examples:
.. code-block:: bash
salt '*' tomcat.deploy_war salt://salt-2015.8.6.war version=2015.08.r6
.. versionadded:: 2015.8.6
CLI Examples:
cp module
.. code-block:: bash
salt '*' tomcat.deploy_war salt://application.war /api
salt '*' tomcat.deploy_war salt://application.war /api no
salt '*' tomcat.deploy_war salt://application.war /api yes http://localhost:8080/manager
minion local file system
.. code-block:: bash
salt '*' tomcat.deploy_war /tmp/application.war /api
salt '*' tomcat.deploy_war /tmp/application.war /api no
salt '*' tomcat.deploy_war /tmp/application.war /api yes http://localhost:8080/manager
"""
# Decide the location to copy the war for the deployment
tfile = f"salt.{os.path.basename(war)}"
if temp_war_location is not None:
if not os.path.isdir(temp_war_location):
return f'Error - "{temp_war_location}" is not a directory'
tfile = os.path.join(temp_war_location, tfile)
else:
tfile = os.path.join(tempfile.gettempdir(), tfile)
# Copy file name if needed
cache = False
if not os.path.isfile(war):
cache = True
cached = __salt__["cp.get_url"](war, tfile, saltenv)
if not cached:
return "FAIL - could not cache the WAR file"
try:
__salt__["file.set_mode"](cached, "0644")
except KeyError:
pass
else:
tfile = war
# Prepare options
opts = {
"war": f"file:{tfile}",
"path": context,
}
# If parallel versions are desired or not disabled
if version:
# Set it to defined version or attempt extract
version = extract_war_version(war) if version is True else version
if isinstance(version, str):
# Only pass version to Tomcat if not undefined
opts["version"] = version
if force == "yes":
opts["update"] = "true"
# Deploy
deployed = _wget("deploy", opts, url, timeout=timeout)
res = "\n".join(deployed["msg"])
# Cleanup
if cache:
__salt__["file.remove"](tfile)
return res
def passwd(passwd, user="", alg="sha1", realm=None):
"""
This function replaces the $CATALINA_HOME/bin/digest.sh script
convert a clear-text password to the $CATALINA_BASE/conf/tomcat-users.xml
format
CLI Examples:
.. code-block:: bash
salt '*' tomcat.passwd secret
salt '*' tomcat.passwd secret tomcat sha1
salt '*' tomcat.passwd secret tomcat sha1 'Protected Realm'
"""
# pylint: disable=no-value-for-parameter
# we call the first parameter the same as the function!
# Shouldn't it be SHA265 instead of SHA1?
digest = getattr(hashlib, alg, None)
if digest is not None:
if realm:
digest.update(f"{user}:{realm}:{passwd}")
else:
digest.update(passwd)
return digest and digest.hexdigest() or False
# Non-Manager functions
def version():
"""
Return server version from catalina.sh version
CLI Example:
.. code-block:: bash
salt '*' tomcat.version
"""
cmd = __catalina_home() + "/bin/catalina.sh version"
out = __salt__["cmd.run"](cmd).splitlines()
for line in out:
if not line:
continue
if "Server version" in line:
comps = line.split(": ")
return comps[1]
def fullversion():
"""
Return all server information from catalina.sh version
CLI Example:
.. code-block:: bash
salt '*' tomcat.fullversion
"""
cmd = __catalina_home() + "/bin/catalina.sh version"
ret = {}
out = __salt__["cmd.run"](cmd).splitlines()
for line in out:
if not line:
continue
if ": " in line:
comps = line.split(": ")
ret[comps[0]] = comps[1].lstrip()
return ret
def signal(signal=None):
"""
Signals catalina to start, stop, securestart, forcestop.
CLI Example:
.. code-block:: bash
salt '*' tomcat.signal start
"""
valid_signals = {
"forcestop": "stop -force",
"securestart": "start -security",
"start": "start",
"stop": "stop",
}
if signal not in valid_signals:
return
cmd = f"{__catalina_home()}/bin/catalina.sh {valid_signals[signal]}"
__salt__["cmd.run"](cmd)
if __name__ == "__main__":
# Allow testing from the CLI
__opts__ = {}
__grains__ = {}
__pillar__ = {
"tomcat-manager.user": "foobar",
"tomcat-manager.passwd": "barfoo1!",
}
old_format_creds = _get_credentials()
__pillar__ = {"tomcat-manager": {"user": "foobar", "passwd": "barfoo1!"}}
new_format_creds = _get_credentials()
if old_format_creds == new_format_creds:
log.info("Config backwards compatible")
else:
log.ifno("Config not backwards compatible")
Zerion Mini Shell 1.0