|
|
@ -1,9 +1,39 @@
|
|
|
|
|
|
|
|
# Copyright 2017, David Wilson
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
|
|
|
|
|
# modification, are permitted provided that the following conditions are met:
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
|
|
|
|
|
|
# this list of conditions and the following disclaimer.
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
|
|
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
|
|
|
|
|
|
# and/or other materials provided with the distribution.
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
|
|
|
|
|
|
# may be used to endorse or promote products derived from this software without
|
|
|
|
|
|
|
|
# specific prior written permission.
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
|
|
|
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
|
|
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
|
|
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
|
|
|
|
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
|
|
|
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
|
|
|
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
|
|
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
|
|
|
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
|
|
|
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
|
|
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Mitogen extends Ansible's regular host configuration mechanism in several ways
|
|
|
|
Mitogen extends Ansible's target configuration mechanism in several ways that
|
|
|
|
that require quite a lot of care:
|
|
|
|
require some care:
|
|
|
|
|
|
|
|
|
|
|
|
* Some per-task configurables in Ansible like ansible_python_interpreter are
|
|
|
|
* Per-task configurables in Ansible like ansible_python_interpreter are
|
|
|
|
connection-layer configurables in Mitogen. They must be extracted during each
|
|
|
|
connection-layer configurables in Mitogen. They must be extracted during each
|
|
|
|
task execution to form the complete connection-layer configuration.
|
|
|
|
task execution to form the complete connection-layer configuration.
|
|
|
|
|
|
|
|
|
|
|
@ -13,28 +43,31 @@ that require quite a lot of care:
|
|
|
|
|
|
|
|
|
|
|
|
* Mitogen allows connections to be delegated to other machines. Ansible has no
|
|
|
|
* Mitogen allows connections to be delegated to other machines. Ansible has no
|
|
|
|
internal framework for this, and so Mitogen must figure out a delegated
|
|
|
|
internal framework for this, and so Mitogen must figure out a delegated
|
|
|
|
connection configuration all for itself. This means it cannot reuse much of
|
|
|
|
connection configuration all on its own. It cannot reuse much of the Ansible
|
|
|
|
the Ansible machinery for building a connection configuration, as that
|
|
|
|
machinery for building a connection configuration, as that machinery is
|
|
|
|
machinery is deeply spread and out hard-wired to expect Ansible's usual mode
|
|
|
|
deeply spread out and hard-wired to expect Ansible's usual mode of operation.
|
|
|
|
of operation.
|
|
|
|
|
|
|
|
|
|
|
|
For normal and delegate_to connections, Ansible's PlayContext is reused where
|
|
|
|
For delegated connections, Ansible's PlayContext information is reused where
|
|
|
|
possible to maximize compatibility, but for proxy hops, configurations are
|
|
|
|
possible, but for proxy hops, configurations are built up using the HostVars
|
|
|
|
built up using the HostVars magic class to call VariableManager.get_vars()
|
|
|
|
magic class to call VariableManager.get_vars() behind the scenes on our behalf.
|
|
|
|
behind the scenes on our behalf. Where Ansible has multiple sources of a
|
|
|
|
Where Ansible has multiple sources of a configuration item, for example,
|
|
|
|
configuration item, for example, ansible_ssh_extra_args, Mitogen must (ideally
|
|
|
|
ansible_ssh_extra_args, Mitogen must (ideally perfectly) reproduce how Ansible
|
|
|
|
perfectly) reproduce how Ansible arrives at its value, without using mechanisms
|
|
|
|
arrives at its value, without using mechanisms that are hard-wired or change
|
|
|
|
that are hard-wired or change across Ansible versions.
|
|
|
|
across Ansible versions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
That is what this file is for. It exports two spec classes, one that takes all
|
|
|
|
That is what this file is for. It exports two spec classes, one that takes all
|
|
|
|
information from PlayContext, and another that takes (almost) all information
|
|
|
|
information from PlayContext, and another that takes (almost) all information
|
|
|
|
from HostVars.
|
|
|
|
from HostVars.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import abc
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
import ansible.utils.shlex
|
|
|
|
import ansible.utils.shlex
|
|
|
|
import ansible.constants as C
|
|
|
|
import ansible.constants as C
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from ansible.module_utils.six import with_metaclass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import mitogen.core
|
|
|
|
import mitogen.core
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -47,11 +80,201 @@ def parse_python_path(s):
|
|
|
|
return ansible.utils.shlex.shlex_split(s)
|
|
|
|
return ansible.utils.shlex.shlex_split(s)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PlayContextSpec:
|
|
|
|
def optional_secret(value):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Wrap `value` in :class:`mitogen.core.Secret` if it is not :data:`None`,
|
|
|
|
|
|
|
|
otherwise return :data:`None`.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
if value is not None:
|
|
|
|
|
|
|
|
return mitogen.core.Secret(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Spec(with_metaclass(abc.ABCMeta, object)):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
A source for variables that comprise a connection configuration.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def transport(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The name of the Ansible plug-in implementing the connection.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def inventory_name(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The name of the target being connected to as it appears in Ansible's
|
|
|
|
|
|
|
|
inventory.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def remote_addr(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The network address of the target, or for container and other special
|
|
|
|
|
|
|
|
targets, some other unique identifier.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def remote_user(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The username of the login account on the target.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def password(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The password of the login account on the target.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def become(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
:data:`True` if privilege escalation should be active.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def become_method(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The name of the Ansible become method to use.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def become_user(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The username of the target account for become.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def become_pass(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The password of the target account for become.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def port(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The port of the login service on the target machine.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def python_path(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Path to the Python interpreter on the target machine.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def private_key_file(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Path to the SSH private key file to use to login.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def ssh_executable(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Path to the SSH executable.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def timeout(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The generic timeout for all connections.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def ansible_ssh_timeout(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The SSH-specific timeout for a connection.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def ssh_args(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The list of additional arguments that should be included in an SSH
|
|
|
|
|
|
|
|
invocation.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def become_exe(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The path to the executable implementing the become method on the remote
|
|
|
|
|
|
|
|
machine.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def sudo_args(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The list of additional arguments that should be included in a become
|
|
|
|
|
|
|
|
invocation.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
# TODO: split out into sudo_args/become_args.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_via(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The value of the mitogen_via= variable for this connection. Indicates
|
|
|
|
|
|
|
|
the connection should be established via an intermediary.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_kind(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The type of container to use with the "setns" transport.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_docker_path(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The path to the "docker" program for the 'docker' transport.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_kubectl_path(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The path to the "kubectl" program for the 'docker' transport.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_lxc_path(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The path to the "lxc" program for the 'lxd' transport.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_lxc_attach_path(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The path to the "lxc-attach" program for the 'lxc' transport.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_lxc_info_path(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The path to the "lxc-info" program for the 'lxc' transport.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_machinectl_path(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The path to the "machinectl" program for the 'setns' transport.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def mitogen_ssh_debug_level(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
The SSH debug level.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
|
|
|
def extra_args(self):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Connection-specific arguments.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PlayContextSpec(Spec):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Return a dict representing all important connection configuration, allowing
|
|
|
|
PlayContextSpec takes almost all its information as-is from Ansible's
|
|
|
|
the same functions to work regardless of whether configuration came from
|
|
|
|
PlayContext. It is used for normal connections and delegate_to connections,
|
|
|
|
play_context (direct connection) or host vars (mitogen_via=).
|
|
|
|
and should always be accurate.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
def __init__(self, connection, play_context, transport, inventory_name):
|
|
|
|
def __init__(self, connection, play_context, transport, inventory_name):
|
|
|
|
self._connection = connection
|
|
|
|
self._connection = connection
|
|
|
@ -81,10 +304,10 @@ class PlayContextSpec:
|
|
|
|
return self._play_context.become_user
|
|
|
|
return self._play_context.become_user
|
|
|
|
|
|
|
|
|
|
|
|
def become_pass(self):
|
|
|
|
def become_pass(self):
|
|
|
|
return self._play_context.become_pass
|
|
|
|
return optional_secret(self._play_context.become_pass)
|
|
|
|
|
|
|
|
|
|
|
|
def password(self):
|
|
|
|
def password(self):
|
|
|
|
return self._play_context.password
|
|
|
|
return optional_secret(self._play_context.password)
|
|
|
|
|
|
|
|
|
|
|
|
def port(self):
|
|
|
|
def port(self):
|
|
|
|
return self._play_context.port
|
|
|
|
return self._play_context.port
|
|
|
@ -107,7 +330,7 @@ class PlayContextSpec:
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
self._connection.get_task_var('ansible_timeout') or
|
|
|
|
self._connection.get_task_var('ansible_timeout') or
|
|
|
|
self._connection.get_task_var('ansible_ssh_timeout') or
|
|
|
|
self._connection.get_task_var('ansible_ssh_timeout') or
|
|
|
|
self._play_context.timeout
|
|
|
|
self.timeout()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def ssh_args(self):
|
|
|
|
def ssh_args(self):
|
|
|
@ -165,7 +388,23 @@ class PlayContextSpec:
|
|
|
|
return self._connection.get_extra_args()
|
|
|
|
return self._connection.get_extra_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MitogenViaSpec:
|
|
|
|
class MitogenViaSpec(Spec):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
MitogenViaSpec takes most of its information from the HostVars of the
|
|
|
|
|
|
|
|
running task. HostVars is a lightweight wrapper around VariableManager, so
|
|
|
|
|
|
|
|
it is better to say that VariableManager.get_vars() is the ultimate source
|
|
|
|
|
|
|
|
of MitogenViaSpec's information.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Due to this, mitogen_via= hosts must have all their configuration
|
|
|
|
|
|
|
|
information represented as host and group variables. We cannot use any
|
|
|
|
|
|
|
|
per-task configuration, as all that data belongs to the real target host.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Ansible uses all kinds of strange historical logic for calculating
|
|
|
|
|
|
|
|
variables, including making their precedence configurable. MitogenViaSpec
|
|
|
|
|
|
|
|
must ultimately reimplement all of that logic. It is likely that if you are
|
|
|
|
|
|
|
|
having a configruation problem with connection delegation, the answer to
|
|
|
|
|
|
|
|
your problem lies in the method implementations below!
|
|
|
|
|
|
|
|
"""
|
|
|
|
def __init__(self, inventory_name, host_vars,
|
|
|
|
def __init__(self, inventory_name, host_vars,
|
|
|
|
become_method, become_user):
|
|
|
|
become_method, become_user):
|
|
|
|
self._inventory_name = inventory_name
|
|
|
|
self._inventory_name = inventory_name
|
|
|
@ -205,14 +444,14 @@ class MitogenViaSpec:
|
|
|
|
return self._become_user
|
|
|
|
return self._become_user
|
|
|
|
|
|
|
|
|
|
|
|
def become_pass(self):
|
|
|
|
def become_pass(self):
|
|
|
|
return (
|
|
|
|
return optional_secret(
|
|
|
|
# TODO: Might have to come from PlayContext.
|
|
|
|
# TODO: Might have to come from PlayContext.
|
|
|
|
self._host_vars.get('ansible_become_password') or
|
|
|
|
self._host_vars.get('ansible_become_password') or
|
|
|
|
self._host_vars.get('ansible_become_pass')
|
|
|
|
self._host_vars.get('ansible_become_pass')
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def password(self):
|
|
|
|
def password(self):
|
|
|
|
return (
|
|
|
|
return optional_secret(
|
|
|
|
# TODO: Might have to come from PlayContext.
|
|
|
|
# TODO: Might have to come from PlayContext.
|
|
|
|
self._host_vars.get('ansible_ssh_pass') or
|
|
|
|
self._host_vars.get('ansible_ssh_pass') or
|
|
|
|
self._host_vars.get('ansible_password')
|
|
|
|
self._host_vars.get('ansible_password')
|
|
|
@ -225,13 +464,11 @@ class MitogenViaSpec:
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def python_path(self):
|
|
|
|
def python_path(self):
|
|
|
|
s = parse_python_path(
|
|
|
|
return parse_python_path(
|
|
|
|
self._host_vars.get('ansible_python_interpreter')
|
|
|
|
self._host_vars.get('ansible_python_interpreter')
|
|
|
|
# This variable has no default for remote hosts. For local hosts it
|
|
|
|
# This variable has no default for remote hosts. For local hosts it
|
|
|
|
# is sys.executable.
|
|
|
|
# is sys.executable.
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print('hi ho', self.inventory_name(), s)
|
|
|
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def private_key_file(self):
|
|
|
|
def private_key_file(self):
|
|
|
|
# TODO: must come from PlayContext too.
|
|
|
|
# TODO: must come from PlayContext too.
|
|
|
|