|
|
|
@ -196,7 +196,13 @@ shell:
|
|
|
|
|
sample: tmsh
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from BytesIO import BytesIO
|
|
|
|
|
except ImportError:
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
|
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
|
from ansible.module_utils.basic import env_fallback
|
|
|
|
@ -212,6 +218,7 @@ try:
|
|
|
|
|
from library.module_utils.network.f5.common import exit_json
|
|
|
|
|
from library.module_utils.network.f5.common import fail_json
|
|
|
|
|
from library.module_utils.network.f5.icontrol import tmos_version
|
|
|
|
|
from library.module_utils.network.f5.icontrol import upload_file
|
|
|
|
|
except ImportError:
|
|
|
|
|
from ansible.module_utils.network.f5.bigip import F5RestClient
|
|
|
|
|
from ansible.module_utils.network.f5.common import F5ModuleError
|
|
|
|
@ -221,6 +228,36 @@ except ImportError:
|
|
|
|
|
from ansible.module_utils.network.f5.common import exit_json
|
|
|
|
|
from ansible.module_utils.network.f5.common import fail_json
|
|
|
|
|
from ansible.module_utils.network.f5.icontrol import tmos_version
|
|
|
|
|
from ansible.module_utils.network.f5.icontrol import upload_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Crypto is used specifically for changing the root password via
|
|
|
|
|
# tmsh over REST.
|
|
|
|
|
#
|
|
|
|
|
# We utilize the crypto library to encrypt the contents of a file
|
|
|
|
|
# before we upload it, and then decrypt it on-box to change the
|
|
|
|
|
# password.
|
|
|
|
|
#
|
|
|
|
|
# To accomplish such a process, we need to be able to encrypt the
|
|
|
|
|
# temporary file with the public key found on the box.
|
|
|
|
|
#
|
|
|
|
|
# These libraries are used to do the encryption.
|
|
|
|
|
#
|
|
|
|
|
# Note that, if these are not available, the ability to change the
|
|
|
|
|
# root password is disabled and the user will be notified as such
|
|
|
|
|
# by a failure of the module.
|
|
|
|
|
#
|
|
|
|
|
# These libraries *should* be available on most Ansible controllers
|
|
|
|
|
# by default though as crypto is a dependency of Ansible.
|
|
|
|
|
#
|
|
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
|
|
|
from cryptography.hazmat.primitives import serialization
|
|
|
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
|
|
|
from cryptography.hazmat.primitives import hashes
|
|
|
|
|
HAS_CRYPTO = True
|
|
|
|
|
except ImportError:
|
|
|
|
|
HAS_CRYPTO = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Parameters(AnsibleF5Parameters):
|
|
|
|
@ -297,6 +334,14 @@ class Parameters(AnsibleF5Parameters):
|
|
|
|
|
result.append(value)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def temp_upload_file(self):
|
|
|
|
|
if self._values['temp_upload_file'] is None:
|
|
|
|
|
f = tempfile.NamedTemporaryFile()
|
|
|
|
|
name = os.path.basename(f.name)
|
|
|
|
|
self._values['temp_upload_file'] = name
|
|
|
|
|
return self._values['temp_upload_file']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApiParameters(Parameters):
|
|
|
|
|
@property
|
|
|
|
@ -784,6 +829,12 @@ class PartitionedManager(BaseManager):
|
|
|
|
|
|
|
|
|
|
class RootUserManager(BaseManager):
|
|
|
|
|
def exec_module(self):
|
|
|
|
|
if not HAS_CRYPTO:
|
|
|
|
|
raise F5ModuleError(
|
|
|
|
|
"An installed and up-to-date python 'cryptography' package is "
|
|
|
|
|
"required to change the 'root' password."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
changed = False
|
|
|
|
|
result = dict()
|
|
|
|
|
state = self.want.state
|
|
|
|
@ -806,15 +857,126 @@ class RootUserManager(BaseManager):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
|
public_key = self.get_public_key_from_device()
|
|
|
|
|
public_key = self.extract_key(public_key)
|
|
|
|
|
encrypted = self.encrypt_password_change_file(
|
|
|
|
|
public_key, self.want.password_credential
|
|
|
|
|
)
|
|
|
|
|
self.upload_to_device(encrypted, self.want.temp_upload_file)
|
|
|
|
|
result = self.update_on_device()
|
|
|
|
|
self.remove_uploaded_file_from_device(self.want.temp_upload_file)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def encrypt_password_change_file(self, public_key, password):
|
|
|
|
|
# This function call requires that the public_key be expressed in bytes
|
|
|
|
|
pub = serialization.load_pem_public_key(
|
|
|
|
|
bytes(public_key, 'utf-8'),
|
|
|
|
|
backend=default_backend()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
message = bytes("{0}\n{0}\n".format(password), 'utf-8')
|
|
|
|
|
ciphertext = pub.encrypt(
|
|
|
|
|
message,
|
|
|
|
|
|
|
|
|
|
# OpenSSL craziness
|
|
|
|
|
#
|
|
|
|
|
# Using this padding because it is the only one that works with
|
|
|
|
|
# the OpenSSL on BIG-IP at this time.
|
|
|
|
|
padding.PKCS1v15(),
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# OAEP is the recommended padding to use for encrypting, however, two
|
|
|
|
|
# things are wrong with it on BIG-IP.
|
|
|
|
|
#
|
|
|
|
|
# The first is that one of the parameters required to decrypt the data
|
|
|
|
|
# is not supported by the OpenSSL version on BIG-IP. A "parameter setting"
|
|
|
|
|
# error is raised when you attempt to use the OAEP parameters to specify
|
|
|
|
|
# hashing algorithms.
|
|
|
|
|
#
|
|
|
|
|
# This is validated by this thread here
|
|
|
|
|
#
|
|
|
|
|
# https://mta.openssl.org/pipermail/openssl-dev/2017-September/009745.html
|
|
|
|
|
#
|
|
|
|
|
# Were is supported, we could use OAEP, but the second problem is that OAEP
|
|
|
|
|
# is not the default mode of the ``openssl`` command. Therefore, we need
|
|
|
|
|
# to adjust the command we use to decrypt the encrypted file when it is
|
|
|
|
|
# placed on BIG-IP.
|
|
|
|
|
#
|
|
|
|
|
# The correct (and recommended if BIG-IP ever upgrades OpenSSL) code is
|
|
|
|
|
# shown below.
|
|
|
|
|
#
|
|
|
|
|
# padding.OAEP(
|
|
|
|
|
# mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
|
|
|
# algorithm=hashes.SHA256(),
|
|
|
|
|
# label=None
|
|
|
|
|
# )
|
|
|
|
|
#
|
|
|
|
|
# Additionally, the code in ``update_on_device()`` would need to be changed
|
|
|
|
|
# to pass the correct command line arguments to decrypt the file.
|
|
|
|
|
)
|
|
|
|
|
return BytesIO(ciphertext)
|
|
|
|
|
|
|
|
|
|
def extract_key(self, content):
|
|
|
|
|
"""Extracts the public key from the openssl command output over REST
|
|
|
|
|
|
|
|
|
|
The REST output includes some extra output that is not relevant to the
|
|
|
|
|
public key. This function attempts to only return the valid public key
|
|
|
|
|
data from the openssl output
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
content: The output from the REST API command to view the public key.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
string: The discovered public key
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
lines = content.split("\n")
|
|
|
|
|
start = lines.index('-----BEGIN PUBLIC KEY-----')
|
|
|
|
|
end = lines.index('-----END PUBLIC KEY-----')
|
|
|
|
|
result = "\n".join(lines[start:end + 1])
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def update_on_device(self):
|
|
|
|
|
escape_patterns = r'([$' + "'])"
|
|
|
|
|
errors = ['Bad password', 'password change canceled', 'based on a dictionary word']
|
|
|
|
|
content = "{0}\n{0}\n".format(self.want.password_credential)
|
|
|
|
|
command = re.sub(escape_patterns, r'\\\1', content)
|
|
|
|
|
cmd = '-c "printf \\\"{0}\\\" | tmsh modify auth password root"'.format(command)
|
|
|
|
|
|
|
|
|
|
# Decrypting logic
|
|
|
|
|
#
|
|
|
|
|
# The following commented out command will **not** work on BIG-IP versions
|
|
|
|
|
# utilizing OpenSSL 1.0.11-fips (15 Jan 2015).
|
|
|
|
|
#
|
|
|
|
|
# The reason is because that version of OpenSSL does not support the various
|
|
|
|
|
# ``-pkeyopt`` parameters shown below.
|
|
|
|
|
#
|
|
|
|
|
# Nevertheless, I am including it here as a possible future enhancement in
|
|
|
|
|
# case the method currently in use stops working.
|
|
|
|
|
#
|
|
|
|
|
# This command overrides defaults provided by OpenSSL because I am not
|
|
|
|
|
# sure how long the defaults will remain the defaults. Probably as long
|
|
|
|
|
# as it took OpenSSL to reach 1.0...
|
|
|
|
|
#
|
|
|
|
|
# openssl = [
|
|
|
|
|
# 'openssl', 'pkeyutl', '-in', '/var/config/rest/downloads/{0}'.format(self.want.temp_upload_file),
|
|
|
|
|
# '-decrypt', '-inkey', '/config/ssl/ssl.key/default.key',
|
|
|
|
|
# '-pkeyopt', 'rsa_padding_mode:oaep', '-pkeyopt', 'rsa_oaep_md:sha256',
|
|
|
|
|
# '-pkeyopt', 'rsa_mgf1_md:sha256'
|
|
|
|
|
# ]
|
|
|
|
|
#
|
|
|
|
|
# The command we actually use is (while not recommended) also the only one
|
|
|
|
|
# that works. It forgoes the usage of OAEP and uses the defaults that come
|
|
|
|
|
# with OpenSSL (PKCS1v15)
|
|
|
|
|
#
|
|
|
|
|
# See this link for information on the parameters used
|
|
|
|
|
#
|
|
|
|
|
# https://www.openssl.org/docs/manmaster/man1/pkeyutl.html
|
|
|
|
|
#
|
|
|
|
|
# If you change the command below, you will need to additionally change
|
|
|
|
|
# how the encryption is done in ``encrypt_password_change_file()``.
|
|
|
|
|
#
|
|
|
|
|
openssl = [
|
|
|
|
|
'openssl', 'pkeyutl', '-in', '/var/config/rest/downloads/{0}'.format(self.want.temp_upload_file),
|
|
|
|
|
'-decrypt', '-inkey', '/config/ssl/ssl.key/default.key',
|
|
|
|
|
]
|
|
|
|
|
cmd = '-c "{0} | tmsh modify auth password root"'.format(' '.join(openssl))
|
|
|
|
|
|
|
|
|
|
params = dict(
|
|
|
|
|
command='run',
|
|
|
|
@ -839,6 +1001,74 @@ class RootUserManager(BaseManager):
|
|
|
|
|
raise F5ModuleError(resp.content)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def upload_to_device(self, content, name):
|
|
|
|
|
"""Uploads a file-like object via the REST API to a given filename
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
content: The file-like object whose content to upload
|
|
|
|
|
name: The remote name of the file to store the content in. The
|
|
|
|
|
final location of the file will be in /var/config/rest/downloads.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
void
|
|
|
|
|
"""
|
|
|
|
|
url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format(
|
|
|
|
|
self.client.provider['server'],
|
|
|
|
|
self.client.provider['server_port']
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
upload_file(self.client, url, content, name)
|
|
|
|
|
except F5ModuleError:
|
|
|
|
|
raise F5ModuleError(
|
|
|
|
|
"Failed to upload the file."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def remove_uploaded_file_from_device(self, name):
|
|
|
|
|
filepath = '/var/config/rest/downloads/{0}'.format(name)
|
|
|
|
|
params = {
|
|
|
|
|
"command": "run",
|
|
|
|
|
"utilCmdArgs": filepath
|
|
|
|
|
}
|
|
|
|
|
uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format(
|
|
|
|
|
self.client.provider['server'],
|
|
|
|
|
self.client.provider['server_port']
|
|
|
|
|
)
|
|
|
|
|
resp = self.client.api.post(uri, json=params)
|
|
|
|
|
try:
|
|
|
|
|
response = resp.json()
|
|
|
|
|
except ValueError as ex:
|
|
|
|
|
raise F5ModuleError(str(ex))
|
|
|
|
|
if 'code' in response and response['code'] in [400, 403]:
|
|
|
|
|
if 'message' in response:
|
|
|
|
|
raise F5ModuleError(response['message'])
|
|
|
|
|
else:
|
|
|
|
|
raise F5ModuleError(resp.content)
|
|
|
|
|
|
|
|
|
|
def get_public_key_from_device(self):
|
|
|
|
|
cmd = '-c "openssl rsa -in /config/ssl/ssl.key/default.key -pubout"'
|
|
|
|
|
|
|
|
|
|
params = dict(
|
|
|
|
|
command='run',
|
|
|
|
|
utilCmdArgs=cmd
|
|
|
|
|
)
|
|
|
|
|
uri = "https://{0}:{1}/mgmt/tm/util/bash".format(
|
|
|
|
|
self.client.provider['server'],
|
|
|
|
|
self.client.provider['server_port']
|
|
|
|
|
)
|
|
|
|
|
resp = self.client.api.post(uri, json=params)
|
|
|
|
|
try:
|
|
|
|
|
response = resp.json()
|
|
|
|
|
except ValueError as ex:
|
|
|
|
|
raise F5ModuleError(str(ex))
|
|
|
|
|
if 'code' in response and response['code'] in [400, 403]:
|
|
|
|
|
if 'message' in response:
|
|
|
|
|
raise F5ModuleError(response['message'])
|
|
|
|
|
else:
|
|
|
|
|
raise F5ModuleError(resp.content)
|
|
|
|
|
if 'commandResult' in response:
|
|
|
|
|
return response['commandResult']
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ArgumentSpec(object):
|
|
|
|
|
def __init__(self):
|
|
|
|
|